From 22742ff31bbb9991d36d8862ccd9345b575d82f0 Mon Sep 17 00:00:00 2001 From: Konstantin Belyalov Date: Tue, 23 Oct 2018 21:14:41 -0700 Subject: [PATCH] Introduce user friendly topic filter. Add more unittests for recently added functionality Better error handling during startup. --- .gitignore | 3 +- README.md | 26 ++++++++ docker/Dockerfile | 1 + docker/run_ozw-mqtt | 3 +- src/mqtt.cpp | 93 ++++++++++++++++++---------- src/options.cpp | 69 ++++++++++++++++----- src/options.h | 22 ++++++- src/ozw-mqtt.cpp | 93 +++++++++++++++------------- src/process_notification.cpp | 1 - test/test_mqtt.cpp | 117 ++++++++++++++++++++++++++++++++++- test/test_options.cpp | 47 ++++++++++---- 11 files changed, 364 insertions(+), 111 deletions(-) diff --git a/.gitignore b/.gitignore index aad5d18..9b50490 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ open-zwave-mqtt.sublime-workspace build build-ubuntu -zwcfg_0xe4822929.xml +zwcfg_*.xml options.xml +OZW_Log.txt diff --git a/README.md b/README.md index 0ee93b9..78601c3 100644 --- a/README.md +++ b/README.md @@ -79,6 +79,31 @@ Now all your ZWave messages are replicated into MQTT network, e.g. let's say you ``` Whenever sensor detects movement a MQTT message will be sent to topic `living/motion/sensor_binary/sensor` with `True` or `False` string payload. +## Topic Map +By default application will publish **all** topics from your OpenZWave configuration. However, sometimes you may want to filter some topics our / rename them. +That could be done by creating simple text file with mappings, like: +``` +# Comments start with "#" + +# Master bedroom +home/master/lights/alarm/burglar = home/master/motion +home/master/lights/switch_multilevel/1/level = home/master/lights +home/master/wall_lights/switch_multilevel/1/level = home/master/wall_lights +home/master/window/sensor/battery/battery_level = home/master/window/battery +home/master/window/sensor/sensor_binary/sensor = home/master/window/state + +# ^ empty lines are ignored. Spaces between topics are ignored too + +# You can just specify topic, without mapping, like: +home/living/window1/sensor/battery/battery_level + +``` +Then simply run: +```bash +$ ./ozw-mqtt --topic-filter-file mytopiclist.txt +``` +... And you'll see only topics defined in map! :-) + ## Options #### Mandatory parameters * `-d [--device]` - ZWave Device location (defaults to `/dev/zwave`) @@ -95,3 +120,4 @@ Whenever sensor detects movement a MQTT message will be sent to topic `living/mo * `--log-level` - Set OpenZWave library log level (can be `error`, `warning`, `info`, `debug`). Defaults to `info`. * `--mqtt-no-name-topics` - Disables subscribe / publish to name-based topics (like `home/room/light`) * `--mqtt-no-id-topics` - Disables subscribe / publish on id-based topics (like `1/2/33`). +* `--topic-filter-file` - Specifies file contains topic map/filter. diff --git a/docker/Dockerfile b/docker/Dockerfile index bd87a98..f271613 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -34,6 +34,7 @@ ENV DEVICE "/dev/zwave" ENV OZW_SYSTEM_CONFIG "/usr/local/etc/openzwave" ENV OZW_USER_CONFIG "/config" ENV LOG_LEVEL "info" +ENV TOPIC_FILTER_FILE "" # User config (home config data) VOLUME /config diff --git a/docker/run_ozw-mqtt b/docker/run_ozw-mqtt index abf4217..971b88d 100755 --- a/docker/run_ozw-mqtt +++ b/docker/run_ozw-mqtt @@ -9,4 +9,5 @@ --mqtt-prefix "$MQTT_PREFIX" \ --mqtt-user "$MQTT_USER" \ --mqtt-passwd "$MQTT_PASSWD" \ - --log-level "$LOG_LEVEL" + --log-level "$LOG_LEVEL" \ + --topic-filter-file "$TOPIC_FILTER_FILE" diff --git a/src/mqtt.cpp b/src/mqtt.cpp index f680b6a..fa65e9c 100644 --- a/src/mqtt.cpp +++ b/src/mqtt.cpp @@ -74,7 +74,8 @@ mqtt_message_callback(struct mosquitto* mosq, void* userdata, const struct mosqu // Create MQTT client connect void -mqtt_connect(const string& client_id, const string& host, const uint16_t port, const string& user, const string& passwd) +mqtt_connect(const string& client_id, const string& host, const uint16_t port, + const string& user, const string& passwd) { // Init MQTT library - mosquitto mosquitto_lib_init(); @@ -145,10 +146,20 @@ make_value_path(const string& prefix, const OpenZWave::ValueID& v) return make_pair(name_path, id_path); } +void publish_impl(const string& topic, const string& value) +{ + int res = mosquitto_publish(mqtt_client, NULL, topic.c_str(), + value.size(), value.c_str(), 0, true); + if (res != 0) { + Log::Write(LogLevel_Error, "MQTT publish to '%s' FAILED (%d)", topic.c_str(), res); + } else { + Log::Write(LogLevel_Info, "MQTT PUBLISH: %s -> %s", topic.c_str(), value.c_str()); + } +} + void mqtt_publish(const options* opts, const OpenZWave::ValueID& v) { - int res; string value; if (!OpenZWave::Manager::Get()->GetValueAsString(v, &value)) { @@ -156,38 +167,49 @@ mqtt_publish(const options* opts, const OpenZWave::ValueID& v) return; } - // Publish value to MQTT + // Do not publish empty messages if (value.empty()) { return; } + // Make 2 topic variations: + // 1. Name based + // 2. ID based auto topics = make_value_path(opts->mqtt_prefix, v); + // If name/id topic found in the filter list - publish + // only to overridden destination + auto override = opts->topic_overrides.find(topics.first); + if (override != opts->topic_overrides.end()) { + publish_impl(override->second, value); + return; + } + override = opts->topic_overrides.find(topics.second); + if (override != opts->topic_overrides.end()) { + publish_impl(override->second, value); + return; + } + + // Publish to auto-generated topic name(s) if (opts->mqtt_name_topics) { - res = mosquitto_publish(mqtt_client, NULL, topics.first.c_str(), - value.size(), value.c_str(), 0, true); - if (res != 0) { - Log::Write(LogLevel_Error, v.GetNodeId(), - "Error while publishing message to MQTT topic '%s'", topics.first.c_str()); - } else { - Log::Write(LogLevel_Debug, v.GetNodeId(), "MQTT PUBLISH: %s -> %s", - topics.first.c_str(), value.c_str()); - } + publish_impl(topics.first, value); } if (opts->mqtt_id_topics) { - res = mosquitto_publish(mqtt_client, NULL, topics.second.c_str(), - value.size(), value.c_str(), 0, true); - if (res != 0) { - Log::Write(LogLevel_Error, v.GetNodeId(), - "Error while publishing message to MQTT topic '%s'", topics.second.c_str()); - } else { - Log::Write(LogLevel_Debug, v.GetNodeId(), "MQTT PUBLISH: %s -> %s", - topics.second.c_str(), value.c_str()); - } + publish_impl(topics.second, value); } } +void subscribe_impl(const string& topic, const OpenZWave::ValueID& v) +{ + string ep = topic + "/set"; + int res = mosquitto_subscribe(mqtt_client, NULL, ep.c_str(), 0); + if (res != 0) { + throw runtime_error("mosquitto_subscribe() failed"); + } + endpoints.insert(make_pair(ep, v)); +} + void mqtt_subscribe(const options* opts, const OpenZWave::ValueID& v) { @@ -197,26 +219,29 @@ mqtt_subscribe(const options* opts, const OpenZWave::ValueID& v) } // Make string representation of changeable parameter - auto paths = make_value_path(opts->mqtt_prefix, v); + auto topics = make_value_path(opts->mqtt_prefix, v); + + // If name/id topic found in the filter list - publish + // only to overridden destination + auto override = opts->topic_overrides.find(topics.first); + if (override != opts->topic_overrides.end()) { + subscribe_impl(override->second, v); + return; + } + override = opts->topic_overrides.find(topics.second); + if (override != opts->topic_overrides.end()) { + subscribe_impl(override->second, v); + return; + } // Subscribe to name based topic, if enabled if (opts->mqtt_name_topics) { - string ep = paths.first + "/set"; - int res = mosquitto_subscribe(mqtt_client, NULL, ep.c_str(), 0); - if (res != 0) { - throw runtime_error("mosquitto_subscribe failed"); - } - endpoints.insert(make_pair(ep, v)); + subscribe_impl(topics.first, v); } // Subscribe to id based topic, if enabled if (opts->mqtt_id_topics) { - string ep = paths.second + "/set"; - int res = mosquitto_subscribe(mqtt_client, NULL, ep.c_str(), 0); - if (res != 0) { - throw runtime_error("mosquitto_subscribe failed"); - } - endpoints.insert(make_pair(ep, v)); + subscribe_impl(topics.second, v); } } diff --git a/src/options.cpp b/src/options.cpp index 18af553..df93ef1 100644 --- a/src/options.cpp +++ b/src/options.cpp @@ -1,6 +1,7 @@ #include #include +#include #include "options.h" using namespace std; @@ -28,6 +29,7 @@ void print_help() printf("\t --mqtt-no-name-topics\t Do not subscribe/publish to name-based topics\n"); printf("\t --mqtt-no-id-topics\t Do not subscribe/publish to id-based topics\n"); printf("\t --system-config\t OpenZWave library system config dir (default /usr/local/etc/openzwave)\n"); + printf("\t --topic-filter-file\t Publish only to topics from file separated by new line\n"); printf("\t --log-level\t\t Set log level (error, warning, info, debug) (default info)\n"); printf("\t --help\t\t\t Print this message\n"); printf("\n"); @@ -46,18 +48,18 @@ options::options(): { } -bool +void options::parse_argv(int argc, const char* argv[]) { for (int i = 1; i < argc; i++) { string k(argv[i]); - // for convience replace _ with - + // for convenience - replace '_' with '-'' std::replace(k.begin(), k.end(), '_', '-'); - // parameters without arguments + // Parameters without arguments if (k == "--help") { print_help(); - return false; + exit(1); } else if (k == "--mqtt-no-name-topics") { mqtt_name_topics = false; continue; @@ -66,22 +68,22 @@ options::parse_argv(int argc, const char* argv[]) continue; } - // next parameters requires value + // Next parameters requires value if (i + 1 >= argc) { // no value provided - printf("Value required for '%s'\n", k.c_str()); - return false; + throw param_error("Value required", k); } string v = argv[++i]; if (v.size() > 2 && v.substr(0, 2) == "--") { - printf("Value required for '%s'\n", k.c_str()); - return false; + throw param_error("Value required", k); } if (k == "--system-config") { system_config = v; } else if (k == "--config" || k == "-c") { user_config = v; + } else if (k == "--topic-filter-file") { + topics_file = v; } else if (k == "--device" || k == "-d") { device = v; } else if (k == "--mqtt-host" || k =="-h") { @@ -97,19 +99,56 @@ options::parse_argv(int argc, const char* argv[]) } else if (k == "--mqtt-passwd" || k == "-p") { mqtt_passwd = v; } else if (k == "--log-level") { - // error, warninig, info, debug + // error, warning, info, debug if (v == "error") log_level = LogLevel_Error; else if (v == "warning") log_level = LogLevel_Warning; else if (v == "info") log_level = LogLevel_Info; else if (v == "debug") log_level = LogLevel_Debug; else { - printf("Unknown log level '%s'.\n", v.c_str()); - return false; + throw param_error("Unknown log level", v); } } else { - printf("Unknown option '%s'\n", k.c_str()); - return false; + throw param_error("Unknown option", k); } } - return true; } + +std::string trim(const std::string& str, + const std::string& whitespace = " \t") +{ + const auto strBegin = str.find_first_not_of(whitespace); + if (strBegin == std::string::npos) + return ""; // no content + + const auto strEnd = str.find_last_not_of(whitespace); + const auto strRange = strEnd - strBegin + 1; + + return str.substr(strBegin, strRange); +} + +void +options::parse_topics_file() +{ + ifstream infile(topics_file); + + for(std::string line; getline(infile, line); ) { + // Skip empty lines + if (line.empty()) { + continue; + } + // And comments - stats with '#' + if (trim(line)[0] == '#') { + continue; + } + size_t pos = line.find("="); + if (pos != string::npos) { + string t1 = trim(line.substr(0, pos)); + string t2 = trim(line.substr(pos + 1)); + topic_overrides.insert(make_pair(t1, t2)); + } else { + // No user friendly topic specified, use the same + topic_overrides.insert(make_pair(line, line)); + } + } +} + diff --git a/src/options.h b/src/options.h index 08962aa..b66c7e2 100644 --- a/src/options.h +++ b/src/options.h @@ -4,22 +4,42 @@ #include #include +class param_error: public std::exception { + std::string msg; +public: + const char * what () const throw () { + return msg.c_str(); + } + param_error(const std::string& _msg, const std::string& _param) { + msg = _msg + " for '" + _param + "'"; + }; +}; + struct options { options(); - bool parse_argv(int argc, const char* argv[]); + void parse_argv(int argc, const char* argv[]); + void parse_topics_file(); std::string system_config; std::string user_config; + std::string topics_file; std::string device; std::string mqtt_host; std::string mqtt_client_id; std::string mqtt_user; std::string mqtt_passwd; std::string mqtt_prefix; + // List of topics allowed to publish to MQTT + // By default - empty which means publish all + // Second element is user-friendly name, if set, like: + // home/switch/switch_multilevel/0/1 -> home/living + std::map topic_overrides; + // MQTT connection parameters uint16_t mqtt_port; bool mqtt_name_topics; bool mqtt_id_topics; + // Log level uint32_t log_level; }; diff --git a/src/ozw-mqtt.cpp b/src/ozw-mqtt.cpp index 6d9399b..7e7ad32 100644 --- a/src/ozw-mqtt.cpp +++ b/src/ozw-mqtt.cpp @@ -34,50 +34,55 @@ save_config(const string& value) int main(int argc, const char* argv[]) { // Parse command line options - options opt; - bool opt_res = opt.parse_argv(argc, argv); - if (!opt_res) { - exit(1); + try { + options opt; + opt.parse_argv(argc, argv); + + // Parse topics filter list, if desired + if (!opt.topics_file.empty()) { + opt.parse_topics_file(); + printf("Loaded %lu topics to publish to.\n", opt.topic_overrides.size()); + } + + // Setup signal handling + struct sigaction sigIntHandler; + sigIntHandler.sa_handler = signal_handler; + sigemptyset(&sigIntHandler.sa_mask); + sigIntHandler.sa_flags = 0; + sigaction(SIGINT, &sigIntHandler, NULL); + + // Make a connection to MQTT broker + printf("Connecting to MQTT Broker %s:%d...", opt.mqtt_host.c_str(), opt.mqtt_port); + mqtt_connect(opt.mqtt_client_id, opt.mqtt_host, opt.mqtt_port, opt.mqtt_user, opt.mqtt_passwd); + + // Create the OpenZWave Manager. + // The first argument is the path to the config files (where the manufacturer_specific.xml file is located + // The second argument is the path for saved Z-Wave network state and the log file. If you leave it NULL + // the log file will appear in the program's working directory. + printf("Starting OpenZWave...\n"); + Options::Create(opt.system_config, opt.user_config, ""); + Options::Get()->AddOptionInt("SaveLogLevel", opt.log_level); + Options::Get()->AddOptionInt("QueueLogLevel", opt.log_level); + Options::Get()->AddOptionInt("DumpTrigger", LogLevel_Error); + Options::Get()->Lock(); + + Manager::Create(); + + // Add a callback handler to the manager. + Manager::Get()->AddWatcher(process_notification, &opt); + // Add a Z-Wave Driver + Manager::Get()->AddDriver(opt.device); + // Default poll interval is 2s. + // NOTE: only devices explicitly enabled for polling will be polled. + Manager::Get()->SetPollInterval(500, true); + + // Register save config mqtt topic + mqtt_subscribe(opt.mqtt_prefix, "ozw/save_config", save_config); + + // Main loop + mqtt_loop(); + } catch (exception e) { + printf("Error: %s\n", e.what()); } - - // Setup signal handling - struct sigaction sigIntHandler; - sigIntHandler.sa_handler = signal_handler; - sigemptyset(&sigIntHandler.sa_mask); - sigIntHandler.sa_flags = 0; - sigaction(SIGINT, &sigIntHandler, NULL); - - printf("Starting OpenZWave to MQTT bridge.\n"); - - // Make a connection to MQTT broker - printf("Connecting to MQTT Broker %s:%d...", opt.mqtt_host.c_str(), opt.mqtt_port); - mqtt_connect(opt.mqtt_client_id, opt.mqtt_host, opt.mqtt_port, opt.mqtt_user, opt.mqtt_passwd); - - // Create the OpenZWave Manager. - // The first argument is the path to the config files (where the manufacturer_specific.xml file is located - // The second argument is the path for saved Z-Wave network state and the log file. If you leave it NULL - // the log file will appear in the program's working directory. - Options::Create(opt.system_config, opt.user_config, ""); - Options::Get()->AddOptionInt("SaveLogLevel", opt.log_level); - Options::Get()->AddOptionInt("QueueLogLevel", opt.log_level); - Options::Get()->AddOptionInt("DumpTrigger", LogLevel_Error); - Options::Get()->Lock(); - - Manager::Create(); - - // Add a callback handler to the manager. - Manager::Get()->AddWatcher(process_notification, &opt); - // Add a Z-Wave Driver - Manager::Get()->AddDriver(opt.device); - // Default poll interval is 2s. - // NOTE: only devices explicitly enabled for polling will be polled. - Manager::Get()->SetPollInterval(500, true); - - // Register save config mqtt topic - mqtt_subscribe(opt.mqtt_prefix, "ozw/save_config", save_config); - - // Main loop - mqtt_loop(); - return 0; } diff --git a/src/process_notification.cpp b/src/process_notification.cpp index d44983d..49d0966 100644 --- a/src/process_notification.cpp +++ b/src/process_notification.cpp @@ -80,7 +80,6 @@ process_notification(const Notification* n, void* ctx) case Notification::Type_NodeQueriesComplete: { - Log::Write(LogLevel_Info, nid, "Driver ready. Start publishing changed values to MQTT"); home_id = hid; publishing = true; break; diff --git a/test/test_mqtt.cpp b/test/test_mqtt.cpp index 79b1403..6ebe6ee 100644 --- a/test/test_mqtt.cpp +++ b/test/test_mqtt.cpp @@ -124,6 +124,57 @@ TEST_F(mqtt_tests, subscribe) ASSERT_SUBSCRIPTIONS(runs); } +TEST_F(mqtt_tests, subscribe_features_on_off) +{ + auto v1 = ValueID(1, 1, ValueID::ValueGenre_User, 0x20, 1, 1, ValueID::ValueType_Int); + auto v2 = ValueID(1, 2, ValueID::ValueGenre_User, 0x32, 1, 1, ValueID::ValueType_Int); + auto v3 = ValueID(1, 2, ValueID::ValueGenre_User, 0x32, 1, 1, ValueID::ValueType_Int); + + // Enable only ID topics + opts.mqtt_name_topics = false; + opts.mqtt_id_topics = true; + mqtt_subscribe(&opts, v1); + + // Enable only name topics + opts.mqtt_name_topics = true; + opts.mqtt_id_topics = false; + mqtt_subscribe(&opts, v2); + + // Disable everything + opts.mqtt_name_topics = false; + opts.mqtt_id_topics = false; + mqtt_subscribe(&opts, v3); + + // Check subscription history + auto hist = mock_mosquitto_subscribe_history(); + ASSERT_EQ(hist.size(), 2); + ASSERT_EQ(hist[0], "1/32/1/set"); + ASSERT_EQ(hist[1], "location_h1_n2/name_h1_n2/meter/label1/set"); +} + +TEST_F(mqtt_tests, subscribe_with_topic_overrides) +{ + auto v1 = ValueID(1, 1, ValueID::ValueGenre_User, 0x20, 1, 1, ValueID::ValueType_Int); + auto v2 = ValueID(1, 2, ValueID::ValueGenre_User, 0x20, 1, 1, ValueID::ValueType_Int); + auto v3 = ValueID(1, 2, ValueID::ValueGenre_User, 0x32, 1, 1, ValueID::ValueType_Int); + + // Set up overrides for first 2 items + opts.topic_overrides["location_h1_n1/name_h1_n1/basic/label1"] = "home/test1"; + opts.topic_overrides["2/32/1"] = "home/test2"; + + mqtt_subscribe(&opts, v1); + mqtt_subscribe(&opts, v2); + mqtt_subscribe(&opts, v3); + + // Check subscription history + auto hist = mock_mosquitto_subscribe_history(); + ASSERT_EQ(hist.size(), 4); + ASSERT_EQ(hist[0], "home/test1/set"); + ASSERT_EQ(hist[1], "home/test2/set"); + ASSERT_EQ(hist[2], "location_h1_n2/name_h1_n2/meter/label1/set"); + ASSERT_EQ(hist[3], "2/50/1/set"); +} + TEST_F(mqtt_tests, subscribe_escape_value_label) { // path: prefix/node_location/node_name/command_class_name @@ -178,7 +229,7 @@ TEST_F(mqtt_tests, subscribe_readonly) mqtt_subscribe(&opts, it->second); } - // there should be no subscriptions - all values are readonly + // there should be no subscriptions - all values are read-only ASSERT_TRUE(mqtt_get_endpoints().empty()); } @@ -249,9 +300,62 @@ TEST_F(mqtt_tests, publish) ASSERT_PUBLICATIONS(runs); } +TEST_F(mqtt_tests, publish_with_features_on_off) +{ + auto v1 = ValueID(1, 1, ValueID::ValueGenre_User, 0x20, 1, 1, ValueID::ValueType_Int); + auto v2 = ValueID(1, 2, ValueID::ValueGenre_User, 0x32, 1, 1, ValueID::ValueType_Int); + auto v3 = ValueID(1, 2, ValueID::ValueGenre_User, 0x32, 1, 1, ValueID::ValueType_Int); + + // Enable only ID topics + opts.mqtt_name_topics = false; + opts.mqtt_id_topics = true; + mqtt_publish(&opts, v1); + + // Enable only name topics + opts.mqtt_name_topics = true; + opts.mqtt_id_topics = false; + mqtt_publish(&opts, v2); + + // Disable everything + opts.mqtt_name_topics = false; + opts.mqtt_id_topics = false; + mqtt_publish(&opts, v3); + + // Check publish history + auto hist = mock_mosquitto_publish_history(); + ASSERT_EQ(hist.size(), 2); + ASSERT_EQ(hist[0].first, "1/32/1"); + ASSERT_EQ(hist[1].first, "location_h1_n2/name_h1_n2/meter/label1"); +} + +TEST_F(mqtt_tests, publish_with_topic_overrides) +{ + auto v1 = ValueID(1, 1, ValueID::ValueGenre_User, 0x20, 1, 1, ValueID::ValueType_Int); + auto v2 = ValueID(1, 2, ValueID::ValueGenre_User, 0x20, 1, 1, ValueID::ValueType_Int); + auto v3 = ValueID(1, 2, ValueID::ValueGenre_User, 0x32, 1, 1, ValueID::ValueType_Int); + // Set up overrides for first 2 items + opts.topic_overrides["location_h1_n1/name_h1_n1/basic/label1"] = "home/test1"; + opts.topic_overrides["2/32/1"] = "home/test2"; + + // Publish values + mqtt_publish(&opts, v1); + mqtt_publish(&opts, v2); + mqtt_publish(&opts, v3); + + // Check publish history + auto hist = mock_mosquitto_publish_history(); + ASSERT_EQ(hist.size(), 4); + ASSERT_EQ(hist[0].first, "home/test1"); + ASSERT_EQ(hist[1].first, "home/test2"); + ASSERT_EQ(hist[2].first, "location_h1_n2/name_h1_n2/meter/label1"); + ASSERT_EQ(hist[3].first, "2/50/1"); +} + TEST_F(mqtt_tests, incoming_message) { - // mqtt_message_callback(struct mosquitto*, void*, const struct mosquitto_message*); + // Create one more node - just to simplify + node_add(1, 3); + map, const ValueID> runs = { { {"location_h1_n1/name_h1_n1/basic/label1/set", "1/32/1/set"}, @@ -261,8 +365,15 @@ TEST_F(mqtt_tests, incoming_message) {"location_h1_n2/name_h1_n2/meter/label1/set", "2/50/1/set"}, ValueID(1, 2, ValueID::ValueGenre_User, 0x32, 1, 1, ValueID::ValueType_Int) }, + { + {"home/test1/set", "home/test1/set"}, + ValueID(1, 3, ValueID::ValueGenre_User, 0x32, 1, 1, ValueID::ValueType_Int) + }, }; + // Enable topic override for node3 + opts.topic_overrides["location_h1_n3/name_h1_n3/meter/label1"] = "home/test1"; + // add values / subscribe to them for (auto it = runs.begin(); it != runs.end(); ++it) { value_add(it->second); @@ -277,7 +388,7 @@ TEST_F(mqtt_tests, incoming_message) m.payload = (void*) "1"; m.payloadlen = 1; mqtt_message_callback(NULL, NULL, &m); - // second, named topic + // second, id topic (name for overridden) m.topic = (char*) it->first.second.c_str(); m.payload = (void*) "2"; m.payloadlen = 1; diff --git a/test/test_options.cpp b/test/test_options.cpp index af9d866..4c6dc45 100644 --- a/test/test_options.cpp +++ b/test/test_options.cpp @@ -1,3 +1,6 @@ +#include +#include +#include #include #include @@ -20,12 +23,12 @@ TEST(options, full_names) "--mqtt-prefix", "prefix", "--mqtt_user", "junk", "--mqtt-passwd", "secret", - "--log-level", "debug" + "--log-level", "debug", + "--topic-filter-file", "1.txt", }; options opt; - bool res = opt.parse_argv(ARRAY_SIZE(argv), argv); - ASSERT_TRUE(res); + opt.parse_argv(ARRAY_SIZE(argv), argv); ASSERT_EQ("/dir/of/system/config", opt.system_config); ASSERT_EQ("./", opt.user_config); ASSERT_EQ("/dev/my_awesome_device", opt.device); @@ -35,6 +38,7 @@ TEST(options, full_names) ASSERT_EQ("prefix", opt.mqtt_prefix); ASSERT_EQ("junk", opt.mqtt_user); ASSERT_EQ("secret", opt.mqtt_passwd); + ASSERT_EQ("1.txt", opt.topics_file); ASSERT_EQ(OpenZWave::LogLevel_Debug, opt.log_level); } @@ -51,8 +55,7 @@ TEST(options, log_level) for (auto i = runs.begin(); i != runs.end(); ++i) { options opt; argv[2] = i->first.c_str(); - bool res = opt.parse_argv(ARRAY_SIZE(argv), argv); - ASSERT_TRUE(res); + opt.parse_argv(ARRAY_SIZE(argv), argv); ASSERT_EQ(i->second, opt.log_level); } } @@ -68,8 +71,7 @@ TEST(options, short_names) }; options opt; - bool res = opt.parse_argv(ARRAY_SIZE(argv), argv); - ASSERT_TRUE(res); + opt.parse_argv(ARRAY_SIZE(argv), argv); ASSERT_EQ("/dir/of/config", opt.user_config); ASSERT_EQ("/dev/my_awesome_device", opt.device); ASSERT_EQ("localhost", opt.mqtt_host); @@ -82,8 +84,7 @@ TEST(options, invalid_options) const char* argv[] = {"exec_name", "-c", "/dir/of/config", "-Z", "fff"}; options opt; - bool res = opt.parse_argv(ARRAY_SIZE(argv), argv); - ASSERT_FALSE(res); + ASSERT_THROW(opt.parse_argv(ARRAY_SIZE(argv), argv), param_error); ASSERT_EQ("/dir/of/config", opt.user_config); } @@ -92,7 +93,31 @@ TEST(options, value_required) const char* argv[] = {"exec_name", "-c"}; options opt; - bool res = opt.parse_argv(ARRAY_SIZE(argv), argv); - ASSERT_FALSE(res); + ASSERT_THROW(opt.parse_argv(ARRAY_SIZE(argv), argv), param_error); } +TEST(options, topic_overrides) +{ + // Create temp file with 3 topics + ofstream f; + f.open("1.txt"); + f << " # comment\n"; + f << "\n"; + f << "home/1/1=home/living\n"; + f << "garage/1/1\n"; + f << "hall/1 = hall/lights\n"; + f.close(); + + // Load / parse file + options opts; + opts.topics_file = "1.txt"; + opts.parse_topics_file(); + + // Check result + ASSERT_EQ(3, opts.topic_overrides.size()); + ASSERT_EQ(opts.topic_overrides["home/1/1"], "home/living"); + ASSERT_EQ(opts.topic_overrides["hall/1"], "hall/lights"); + ASSERT_EQ(opts.topic_overrides["garage/1/1"], "garage/1/1"); + + unlink("1.txt"); +}