Skip to content

Commit

Permalink
Introduce user friendly topic filter.
Browse files Browse the repository at this point in the history
Add more unittests for recently added functionality
Better error handling during startup.
  • Loading branch information
belyalov committed Oct 26, 2018
1 parent c7e24dd commit 22742ff
Show file tree
Hide file tree
Showing 11 changed files with 364 additions and 111 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
open-zwave-mqtt.sublime-workspace
build
build-ubuntu
zwcfg_0xe4822929.xml
zwcfg_*.xml
options.xml
OZW_Log.txt
26 changes: 26 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`)
Expand All @@ -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.
1 change: 1 addition & 0 deletions docker/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion docker/run_ozw-mqtt
Original file line number Diff line number Diff line change
Expand Up @@ -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"
93 changes: 59 additions & 34 deletions src/mqtt.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -145,49 +146,70 @@ 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)) {
Log::Write(LogLevel_Error, v.GetNodeId(), "GetValueAsString() failed.");
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)
{
Expand All @@ -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);
}
}

Expand Down
69 changes: 54 additions & 15 deletions src/options.cpp
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@

#include <openzwave/platform/Log.h>
#include <algorithm>
#include <fstream>
#include "options.h"

using namespace std;
Expand Down Expand Up @@ -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");
Expand All @@ -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;
Expand All @@ -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") {
Expand All @@ -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));
}
}
}

22 changes: 21 additions & 1 deletion src/options.h
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,42 @@
#include <string>
#include <map>

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<std::string, std::string> topic_overrides;
// MQTT connection parameters
uint16_t mqtt_port;
bool mqtt_name_topics;
bool mqtt_id_topics;
// Log level
uint32_t log_level;
};

Expand Down
Loading

0 comments on commit 22742ff

Please sign in to comment.