add MQTT integration support

This commit is contained in:
John
2022-01-16 19:05:32 +01:00
parent 29e96338da
commit 7e563d5ae9
2 changed files with 679 additions and 146 deletions
+497 -146
View File
@@ -36,7 +36,8 @@ using std::dec;
#define O_PASS (O_USER+1)
#define O_TOPI (O_PASS+1)
#define O_RETA (O_TOPI+1)
#define O_JSON (O_RETA+1)
#define O_INTF (O_RETA+1)
#define O_JSON (O_INTF+1)
#define O_LOGL (O_JSON+1)
#define O_VERS (O_LOGL+1)
#define O_IGIN (O_VERS+1)
@@ -53,13 +54,14 @@ static const struct argp_option g_mqtt_argp_options[] = {
{nullptr, 0, nullptr, 0, "MQTT options:", 1 },
{"mqtthost", O_HOST, "HOST", 0, "Connect to MQTT broker on HOST [localhost]", 0 },
{"mqttport", O_PORT, "PORT", 0, "Connect to MQTT broker on PORT (usually 1883), 0 to disable [0]", 0 },
{"mqttclientid", O_CLID, "ID", 0, "Set client ID for connection to MQTT broker [" PACKAGE_NAME "_"
{"mqttclientid", O_CLID, "ID", 0, "Set client ID for connection to MQTT broker [" PACKAGE_NAME "_"
PACKAGE_VERSION "_<pid>]", 0 },
{"mqttuser", O_USER, "USER", 0, "Connect as USER to MQTT broker (no default)", 0 },
{"mqttpass", O_PASS, "PASSWORD", 0, "Use PASSWORD when connecting to MQTT broker (no default)", 0 },
{"mqtttopic", O_TOPI, "TOPIC", 0,
"Use MQTT TOPIC (prefix before /%circuit/%name or complete format) [ebusd]", 0 },
{"mqttretain", O_RETA, nullptr, 0, "Retain all topics instead of only selected global ones", 0 },
{"mqttint", O_INTF, "FILE", 0, "Read MQTT integration settings from FILE (no default)", 0 },
{"mqttjson", O_JSON, nullptr, 0, "Publish in JSON format instead of strings", 0 },
{"mqttverbose", O_VERB, nullptr, 0, "Publish all available attributes", 0 },
#if (LIBMOSQUITTO_VERSION_NUMBER >= 1003001)
@@ -88,10 +90,8 @@ static uint16_t g_port = 0; //!< optional port of MQTT broker, 0 t
static const char* g_clientId = nullptr; //!< optional clientid override for MQTT broker
static const char* g_username = nullptr; //!< optional user name for MQTT broker (no default)
static const char* g_password = nullptr; //!< optional password for MQTT broker (no default)
/** the MQTT topic string parts. */
static vector<string> g_topicStrs;
/** the MQTT topic field parts. */
static vector<string> g_topicFields;
static MqttReplacer* g_topicReplacer = nullptr; //!< the topic replacer
static const char* g_integrationFile = nullptr; //!< the integration settings file
static bool g_retain = false; //!< whether to retail all topics
static OutputFormat g_publishFormat = OF_NONE; //!< the OutputFormat for publishing messages
#if (LIBMOSQUITTO_VERSION_NUMBER >= 1003001)
@@ -112,8 +112,6 @@ static const char* g_keypass = nullptr; //!< client key file password for TLS
static bool g_insecure = false; //!< whether to allow insecure TLS connection
#endif
bool parseTopic(const string& topic, vector<string>* strs, vector<string>* fields);
static char* replaceSecret(char *arg) {
char* ret = strdup(arg);
int cnt = 0;
@@ -178,7 +176,12 @@ static error_t mqtt_parse_opt(int key, char *arg, struct argp_state *state) {
argp_error(state, "invalid mqtttopic");
return EINVAL;
}
if (!parseTopic(arg, &g_topicStrs, &g_topicFields)) {
if (g_topicReplacer) {
argp_error(state, "duplicate mqtttopic");
return EINVAL;
}
g_topicReplacer = MqttReplacer::create(arg);
if (!g_topicReplacer) {
argp_error(state, "malformed mqtttopic");
}
break;
@@ -187,6 +190,14 @@ static error_t mqtt_parse_opt(int key, char *arg, struct argp_state *state) {
g_retain = true;
break;
case O_INTF: // --mqttint=/etc/ebusd/mqttint.cfg
if (arg == nullptr || arg[0] == 0 || strcmp("/", arg) == 0) {
argp_error(state, "invalid mqttint file");
return EINVAL;
}
g_integrationFile = arg;
break;
case O_JSON: // --mqttjson
g_publishFormat |= OF_JSON|OF_NAMES;
break;
@@ -274,9 +285,6 @@ static const struct argp_child g_mqtt_argp_child = {&g_mqtt_argp, 0, "", 1};
const struct argp_child* mqtthandler_getargs() {
if (g_topicStrs.empty()) {
g_topicStrs.push_back(PACKAGE);
}
return &g_mqtt_argp_child;
}
@@ -326,50 +334,312 @@ static const char* knownFieldNames[] = {
/** the number of known field names. */
static const size_t knownFieldCount = sizeof(knownFieldNames) / sizeof(char*);
/**
* Parse the topic template.
* @param topic the topic template.
* @param strs the @a vector to which the string parts shall be added.
* @param fields the @a vector to which the field parts shall be added.
* @return true on success, false on malformed topic template.
*/
bool parseTopic(const string& topic, vector<string>* strs, vector<string>* fields) {
size_t lastpos = 0;
size_t end = topic.length();
vector<string> columns;
strs->clear();
fields->clear();
for (size_t pos=topic.find('%', lastpos); pos != string::npos; ) {
size_t idx = knownFieldCount;
size_t len = 0;
for (size_t i = 0; i < knownFieldCount; i++) {
len = strlen(knownFieldNames[i]);
if (topic.substr(pos+1, len) == knownFieldNames[i]) {
idx = i;
break;
}
}
if (idx == knownFieldCount) { // TODO could allow custom attributes here
return false;
}
string fieldName = knownFieldNames[idx];
for (const auto& it : *fields) {
if (it == fieldName) {
return false; // duplicate column
}
}
strs->push_back(topic.substr(lastpos, pos-lastpos));
fields->push_back(fieldName);
lastpos = pos+1+len;
pos = topic.find('%', lastpos);
std::pair<string, int> makeField(string name, bool isField) {
if (!isField) {
return {name, -1};
}
if (lastpos < end) {
strs->push_back(topic.substr(lastpos, end-lastpos));
for (int idx = 0; idx < (int)knownFieldCount; idx++) {
if (name==knownFieldNames[idx]) {
return {name, idx};
}
}
return {name, knownFieldCount};
}
MqttReplacer::MqttReplacer(bool fillDefault) {
if (fillDefault) {
ensureDefault();
}
}
bool MqttReplacer::parse(const string& templateStr, bool onlyKnown, bool noKnownDuplicates) {
m_parts.clear();
size_t end = templateStr.length();
bool inField = false;
ostringstream stack;
for (size_t pos = 0; pos < end+1; pos++) {
char ch = pos<end ? templateStr[pos] : '\0';
bool empty = stack.tellp()<=0;
if (ch=='%' || ch=='\0') {
if (inField && empty) { // %% for plain %
inField = false;
stack << ch;
} else {
if (!empty) {
m_parts.push_back(makeField(stack.str(), inField));
stack.str("");
}
inField = true;
}
} else {
if (inField && !((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || ch == '_')) {
// invalid field character
if (stack.tellp()>0) {
m_parts.push_back(makeField(stack.str(), true));
stack.str("");
}
inField = false;
}
stack << ch;
}
}
if (onlyKnown || noKnownDuplicates) {
int foundMask = 0;
int knownCount = knownFieldCount;
for (const auto &it : m_parts) {
if (it.second < 0) {
continue; // unknown field
}
if (onlyKnown && it.second >= knownCount) {
return false;
}
if (noKnownDuplicates && it.second < knownCount) {
int bit = 1<<it.second;
if (foundMask & bit) {
return false; // duplicate known field
}
foundMask |= bit;
}
}
}
return true;
}
MqttReplacer* MqttReplacer::create(const string& templateStr, bool ensureDefault, bool onlyKnown, bool noKnownDuplicates) {
auto ret = new MqttReplacer(false);
if (ret->parse(templateStr, onlyKnown, noKnownDuplicates)) {
if (ensureDefault) {
ret->ensureDefault();
}
return ret;
}
delete ret;
return nullptr;
}
void MqttReplacer::normalize(string& str) {
transform(str.begin(), str.end(), str.begin(), [](unsigned char c){
return isalnum(c) ? c : '_';
});
}
void MqttReplacer::ensureDefault() {
if (m_parts.empty()) {
m_parts.emplace_back(string(PACKAGE) + "/", -1);
} else if (m_parts.size()==1 && m_parts[0].second<0 && m_parts[0].first.find('/')==string::npos) {
m_parts[0] = {m_parts[0].first + "/", -1}; // ensure trailing slash
}
if (!has("circuit")) {
m_parts.emplace_back("circuit", 0); // index of circuit in knownFieldNames
m_parts.emplace_back("/", -1);
}
if (!has("name")) {
m_parts.emplace_back("name", 1); // index of name in knownFieldNames
}
}
bool MqttReplacer::has(const string& field) const {
for (const auto &it: m_parts) {
if (it.second >= 0 && it.first == field) {
return true;
}
}
return false;
}
string MqttReplacer::get(const map<string, string>& values, bool untilFirstEmpty, bool onlyAlphanum) const {
ostringstream ret;
for (const auto &it: m_parts) {
if (it.second < 0) {
ret << it.first;
continue;
}
const auto pos = values.find(it.first);
if (pos==values.cend()) {
if (untilFirstEmpty) {
break;
}
} else if (untilFirstEmpty && pos->second.empty()) {
break;
} else {
ret << pos->second;
}
}
if (!onlyAlphanum) {
return ret.str();
}
string str = ret.str();
normalize(str);
return str;
}
bool MqttReplacer::isReducable(const map<string, string>& values) const {
for (const auto &it: m_parts) {
if (it.second < 0) {
continue;
}
const auto pos = values.find(it.first);
if (pos==values.cend()) {
return false;
}
}
return true;
}
bool MqttReplacer::reduce(const map<string, string>& values, string& result, bool onlyAlphanum) const {
ostringstream ret;
for (const auto &it: m_parts) {
if (it.second < 0) {
ret << it.first;
continue;
}
const auto pos = values.find(it.first);
if (pos==values.cend()) {
result = ret.str();
return false;
}
ret << pos->second;
}
result = ret.str();
if (onlyAlphanum) {
normalize(result);
}
return true;
}
ssize_t MqttReplacer::matchTopic(const string& remain, string* circuit, string* name, string* field) const {
size_t last = 0;
size_t count = m_parts.size();
size_t idx;
for (idx = 0; idx < count; idx++) {
const auto part = m_parts[idx];
if (part.second < 0) {
if (remain.substr(last, part.first.length()) != part.first) {
return static_cast<ssize_t>(idx);
}
last += part.first.length();
continue;
}
string value;
if (idx+1 < count) {
// todo require topic fields to be separated by non-empty string? e.g. %circuit%name is not parseable here
size_t pos = remain.find(m_parts[idx+1].first, last);
if (pos == string::npos) {
// next part not found
return -static_cast<ssize_t>(idx)-1;
}
value = remain.substr(last, pos-last);
} else {
// last part is a field name
if (remain.find('/', last) != string::npos) {
// non-name in remainder found
return -static_cast<ssize_t>(idx)-1;
}
value = remain;
}
last += value.length();
switch (part.second) {
case 0: *circuit = value; break;
case 1: *name = value; break;
case 2: *field = value; break;
default: // unknown field
break;
}
}
return static_cast<ssize_t>(idx);
}
static const string EMPTY = "";
const string& MqttReplacers::operator[](const string& key) const {
auto itc = m_constants.find(key);
if (itc==m_constants.end()) {
return EMPTY;
}
return itc->second;
}
bool MqttReplacers::uses(const string& field) const {
for (const auto &it : m_replacers) {
if (it.second.has(field)) {
return true;
}
}
return false;
}
MqttReplacer& MqttReplacers::get(const string& key) {
return m_replacers[key];
}
string MqttReplacers::get(const string& key, bool untilFirstEmpty, bool onlyAlphanum) const {
auto itc = m_constants.find(key);
if (itc!=m_constants.end()) {
return itc->second;
}
auto itv = m_replacers.find(key);
if (itv==m_replacers.end()) {
return "";
}
return itv->second.get(m_constants, untilFirstEmpty, onlyAlphanum);
}
bool MqttReplacers::set(const string& key, const string& value, bool removeReplacer) {
m_constants[key] = value;
if (removeReplacer) {
m_replacers.erase(key);
}
if (key.find('_')!=string::npos) {
return false;
}
string upper = key;
transform(upper.begin(), upper.end(), upper.begin(), ::toupper);
if (upper==key) {
return false;
}
string val = value;
MqttReplacer::normalize(val);
m_constants[upper] = val;
if (removeReplacer) {
m_replacers.erase(upper);
}
return true;
}
void MqttReplacers::set(const string& key, int value) {
std::ostringstream str;
str << static_cast<signed>(value);
m_constants[key] = str.str();
}
void MqttReplacers::reduce() {
// iterate through variables and reduce as many to constants as possible
bool reduced = false;
do {
reduced = false;
for (auto it = m_replacers.begin(); it != m_replacers.end(); ) {
string str;
if (!it->second.isReducable(m_constants)
|| !it->second.reduce(m_constants, str)) {
++it;
continue;
}
bool restart = set(it->first, str, false);
it = m_replacers.erase(it);
reduced = true;
if (restart) {
string upper = it->first;
transform(upper.begin(), upper.end(), upper.begin(), ::toupper);
if (m_replacers.erase(upper)>0) {
break; // restart as iterator is now invalid
}
}
}
} while (reduced);
}
#if (LIBMOSQUITTO_MAJOR >= 1)
int on_keypassword(char *buf, int size, int rwflag, void *userdata) {
if (!g_keypass) {
@@ -444,32 +714,75 @@ void on_message(
handler->notifyTopic(topic, data);
}
void MqttHandler::parseIntegration(const string& line) {
if (line.empty()) {
return;
}
size_t pos = line.find('=');
if (pos==string::npos) {
return;
}
string key = line.substr(0, pos);
FileReader::trim(&key);
string value = line.substr(pos+1);
FileReader::trim(&value);
if (value.find('%')==string::npos) {
m_replacers.set(key, value); // constant value
} else {
// simple variable
m_replacers.get(key).parse(value, false, false);
}
}
MqttHandler::MqttHandler(UserInfo* userInfo, BusHandler* busHandler, MessageMap* messages)
: DataSink(userInfo, "mqtt"), DataSource(busHandler), WaitThread(), m_messages(messages), m_connected(false),
m_initialConnectFailed(false), m_lastUpdateCheckResult("."), m_lastScanStatus("."), m_lastErrorLogTime(0) {
m_publishByField = false;
m_definitionsSince = 0;
m_mosquitto = nullptr;
if (g_topicFields.empty()) {
if (g_topicStrs.empty()) {
g_topicStrs.push_back("");
if (!g_topicReplacer) {
g_topicReplacer = new MqttReplacer();
}
m_publishByField = g_topicReplacer->has("field");
m_replacers.get("mqtttopic") = *g_topicReplacer;
if (g_integrationFile != nullptr) {
std::ifstream stream;
stream.open(g_integrationFile, std::ifstream::in);
if (!stream.is_open()) {
logOtherError("mqtt", "unable to open integration file %s", g_integrationFile);
} else {
string str = g_topicStrs[0];
if (str.empty() || str[str.length()-1] != '/') {
g_topicStrs[0] = str+"/";
}
}
g_topicFields.push_back("circuit");
g_topicStrs.push_back("/");
g_topicFields.push_back("name");
} else {
for (size_t i = 0; i < g_topicFields.size(); i++) {
if (g_topicFields[i] == "field") {
m_publishByField = true;
break;
m_replacers.set("version", PACKAGE_VERSION);
string line = m_replacers.get("mqtttopic", true);
m_replacers.set("prefix", line);
size_t pos = line.find_last_not_of("/_");
m_replacers.set("prefixn", line.substr(0, pos+1));
string last;
while (stream.peek() != EOF && getline(stream, line)) {
if (line.empty()) {
parseIntegration(last);
last = "";
continue;
}
if (line[0] == '#') {
// only ignore it to allow commented lines in the middle of e.g. payload
continue;
}
if (last.empty()) {
last = line;
} else if (line[0] == '\t' || line[0] == ' ') { // continuation
last += "\n" + line;
} else {
parseIntegration(last);
last = line;
}
}
parseIntegration(last);
m_replacers.reduce();
}
}
m_hasDefinitionTopic = !m_replacers.get("definition_topic", false, false).empty();
m_hasDefinitionFieldsPayload = m_replacers.uses("fields_payload");
m_subscribeConfigRestartTopic = m_replacers.get("config_restart_topic", false, false);
m_subscribeConfigRestartPayload = m_replacers.get("config_restart_payload", false, false);
m_globalTopic = getTopic(nullptr, "global/");
m_subscribeTopic = getTopic(nullptr, "#");
if (check(mosquitto_lib_init(), "unable to initialize")) {
@@ -576,6 +889,9 @@ void MqttHandler::notifyConnected() {
publishTopic(m_globalTopic+"version", sep + (PACKAGE_STRING "." REVISION) + sep, true);
publishTopic(m_globalTopic+"running", "true", true);
check(mosquitto_subscribe(m_mosquitto, nullptr, m_subscribeTopic.c_str(), 0), "subscribe");
if (!m_subscribeConfigRestartTopic.empty()) {
check(mosquitto_subscribe(m_mosquitto, nullptr, m_subscribeConfigRestartTopic.c_str(), 0), "subscribe definition");
}
}
}
@@ -584,6 +900,12 @@ void MqttHandler::notifyTopic(const string& topic, const string& data) {
if (pos == string::npos) {
return;
}
if (!m_subscribeConfigRestartTopic.empty() && topic == m_subscribeConfigRestartTopic) {
if (m_subscribeConfigRestartPayload.empty() || data == m_subscribeConfigRestartPayload) {
m_definitionsSince = 0;
}
return;
}
string direction = topic.substr(pos+1);
if (direction.empty()) {
return;
@@ -595,60 +917,10 @@ void MqttHandler::notifyTopic(const string& topic, const string& data) {
}
logOtherDebug("mqtt", "received topic %s with data %s", topic.c_str(), data.c_str());
string remain = topic.substr(0, pos);
size_t last = 0;
string circuit, name;
bool finalField = false;
for (size_t idx = 0; idx < g_topicStrs.size()+1 && !finalField; idx++) {
string field;
string chk;
if (idx < g_topicStrs.size()) {
chk = g_topicStrs[idx];
pos = remain.find(chk, last);
if (pos == string::npos) {
if (!isList) {
return;
}
if (idx == 0 && remain+"/" == chk) { // check for only first prefix, e.g. "ebusd/"
break;
}
pos = remain.size();
finalField = true;
}
} else if (idx-1 < g_topicFields.size()) {
pos = remain.size();
} else if (last < remain.size()) {
if (!isList) {
return;
}
break;
} else {
break;
}
field = remain.substr(last, pos-last);
last = pos+chk.size();
if (idx == 0) {
if (pos > 0) {
return;
}
} else {
if (field.empty()) {
if (!isList) {
return;
}
continue;
}
string fieldName = g_topicFields[idx-1];
if (fieldName == "circuit") {
circuit = field;
} else if (fieldName == "name") {
name = field;
} else if (fieldName == "field") {
// field = field; // TODO add support for writing a single field
} else {
return;
}
}
string circuit, name, field;
ssize_t match = g_topicReplacer->matchTopic(topic.substr(0, pos), &circuit, &name, &field);
if (match<0 && !isList) { // TODO
logOtherError("mqtt", "received unmatchable topic %s", topic.c_str());
}
if (isList) {
logOtherInfo("mqtt", "received list topic for %s %s", circuit.c_str(), name.c_str());
@@ -664,7 +936,7 @@ void MqttHandler::notifyTopic(const string& topic, const string& data) {
m_messages->findAll(circuit, name, m_levels, !(circuitPrefix || namePrefix), true, true,
true, true, true, 0, 0, false, &messages);
bool onlyWithData = !data.empty();
for (const auto message : messages) {
for (const auto& message : messages) {
if ((circuitPrefix && (
message->getCircuit().substr(0, circuit.length()) != circuit
|| (!namePrefix && name.length() > 0 && message->getName() != name)))
@@ -767,12 +1039,100 @@ void MqttHandler::run() {
lastTaskRun = now;
} else if (now > lastTaskRun+15) {
allowReconnect = true;
sendSignal = true;
time_t uptime = now-start;
updates.str("");
updates.clear();
updates << dec << static_cast<unsigned>(uptime);
publishTopic(uptimeTopic, updates.str());
if (m_connected) {
sendSignal = true;
time_t uptime = now - start;
updates.str("");
updates.clear();
updates << dec << static_cast<unsigned>(uptime);
publishTopic(uptimeTopic, updates.str());
}
if (m_connected && m_hasDefinitionTopic) {
deque<Message*> messages;
m_messages->findAll("", "", "", false, true, true, true, true, true, 0, 0, false, &messages);
for (const auto& message : messages) {
if (message->getCreateTime() <= m_definitionsSince) { // only newer defined
continue;
}
MqttReplacers msgValues = m_replacers; // need a copy here as the contents are manipulated
msgValues.set("circuit", message->getCircuit());
msgValues.set("name", message->getName());
if (!m_publishByField) {
msgValues.set("topic", getTopic(message, "", "")); // TODO already present?
}
msgValues.reduce();
ostringstream fields;
for (size_t index = 0; index < message->getFieldCount(); index++) {
const SingleDataField* field = message->getField(index);
if (!field || field->isIgnored()) {
continue;
}
const DataType* dataType = field->getDataType();
string typeSuffix;
if (dataType->isNumeric()) {
typeSuffix = "number";
} else {
typeSuffix = "string";
}
//TODO valuelists => binary_sensor, date/time => device_class=date/timestamp, energy, power, current, gas, humidity, power_factor, pressure, temperature, voltage
string str = msgValues.get("type_"+typeSuffix, false, false);
if (str.empty()) {
continue;
}
MqttReplacers values = msgValues; // need a copy here as the contents are manipulated
values.set("type", str);
values.set("index", static_cast<signed>(index));
string fieldName = message->getFieldName(index);
values.set("field", fieldName);
values.set("fieldcomment", field->getAttribute("comment"));
values.set("unit", field->getAttribute("unit"));
if (dataType->isNumeric()) {
auto dt = dynamic_cast<const NumberDataType*>(dataType);
values.set("min", static_cast<signed>(dt->getMinValue()));
values.set("max", static_cast<signed>(dt->getMaxValue()));
}
values.reduce();
str = values.get("type_part_"+typeSuffix, false, false);
values.set("type_part", str);
//TODO Valuelist: => select.options
if (m_publishByField) {
values.set("topic", getTopic(message, "", fieldName)); // TODO already present?
}
values.reduce();
if (m_hasDefinitionFieldsPayload) {
string value = values["field_payload"];
if (!value.empty()) {
if (fields.tellp()>0) {
fields << values["field_separator"];
}
fields << value;
// TODO str << ",\"value_template\":\"{{value_json." << fieldName << ".value}}\"";
}
continue;
}
string topic = values.get("definition_topic", false, false);
if (!topic.empty()) {
values.set("definition_topic", topic);
string payload = values.get("definition_payload", false, false);
string retainStr = values.get("definition_retain", false, false);
bool retain = !retainStr.empty() && !(retainStr=="0" || retainStr=="no" || retainStr=="false");
publishTopic(topic, payload, retain);
}
}
if (fields.tellp()>0) {
msgValues.set("fields_payload", fields.str());
string topic = msgValues.get("definition_topic", false, false);
if (!topic.empty()) {
msgValues.set("definition_topic", topic);
string payload = msgValues.get("definition_payload", false, false);
string retainStr = msgValues.get("definition_retain", false, false);
bool retain = !retainStr.empty() && !(retainStr == "0" || retainStr == "no" || retainStr == "false");
publishTopic(topic, payload, retain);
}
}
}
time(&m_definitionsSince);
}
time(&lastTaskRun);
}
if (sendSignal) {
@@ -795,7 +1155,7 @@ void MqttHandler::run() {
for (auto it = m_updatedMessages.begin(); it != m_updatedMessages.end(); ) {
const vector<Message*>* messages = m_messages->getByKey(it->first);
if (messages) {
for (auto message : *messages) {
for (const auto& message : *messages) {
if (message->getLastChangeTime() > 0 && message->isAvailable()
&& (!g_onlyChanges || message->getLastChangeTime() > lastUpdates)) {
updates.str("");
@@ -871,24 +1231,15 @@ bool MqttHandler::handleTraffic(bool allowReconnect) {
}
string MqttHandler::getTopic(const Message* message, const string& suffix, const string& fieldName) {
ostringstream ret;
for (size_t i = 0; i < g_topicStrs.size(); i++) {
ret << g_topicStrs[i];
if (!message) {
break;
}
if (i < g_topicFields.size()) {
if (g_topicFields[i] == "field") {
ret << fieldName;
} else {
message->dumpField(g_topicFields[i], false, OF_NONE, &ret);
}
map <string, string> values;
if (message) {
values["circuit"] = message->getCircuit();
values["name"] = message->getName();
if (!fieldName.empty()) {
values["field"] = fieldName;
}
}
if (!suffix.empty()) {
ret << suffix;
}
return ret.str();
return g_topicReplacer->get(values) + suffix;
}
void MqttHandler::publishMessage(const Message* message, ostringstream* updates, bool includeWithoutData) {
+182
View File
@@ -55,6 +55,166 @@ const struct argp_child* mqtthandler_getargs();
bool mqtthandler_register(UserInfo* userInfo, BusHandler* busHandler, MessageMap* messages,
list<DataHandler*>* handlers);
/**
* Helper class for replacing a template string with real values.
*/
class MqttReplacer {
public:
/**
* Constructor.
* @param fillDefault true to fill in the default topic.
*/
explicit MqttReplacer(bool fillDefault = true);
/**
* Create a new replacer.
* @param templateStr the template string.
* @param ensureDefault true to ensure the default topic parts are present.
* @param onlyKnown true to allow only known field names from @a knownFieldNames.
* @param noKnownDuplicates true to now allow duplicates from @a knownFieldNames.
* @return the replacer, or null if the parameters are invalid.
*/
static MqttReplacer* create(const string& templateStr, bool ensureDefault = true, bool onlyKnown = true, bool noKnownDuplicates = true);
/**
* Normalize the string to contain only alpha numeric characters plus underscore by replacing other characters with
* an underscore.
* @param str the string to normalize.
*/
static void normalize(string& str);
/**
* Parse the template string.
* @param templateStr the template string.
* @param onlyKnown true to allow only known field names from @a knownFieldNames.
* @param noKnownDuplicates true to now allow duplicates from @a knownFieldNames.
* @return true on success, false on malformed template string.
*/
bool parse(const string& templateStr, bool onlyKnown = true, bool noKnownDuplicates = true);
private:
/**
* Ensure the default topic parts are present (circuit and message).
*/
void ensureDefault();
public:
/**
* Return whether the specified field is used.
* @param field the field name to check.
* @return true when the specified field is used
*/
bool has(const string& field) const;
/**
* Get the replaced template string.
* @param values the named values for replacement.
* @param untilFirstEmpty true to only return the prefix before the first empty field.
* @param onlyAlphanum whether to only allow alpha numeric characters plus underscore.
* @return the replaced template string.
*/
string get(const map<string, string>& values, bool untilFirstEmpty = true, bool onlyAlphanum = false) const;
/**
* Check if the fields can be reduced to a constant value.
* @param values the named values for replacement.
* @return true if the result is final.
*/
bool isReducable(const map<string, string>& values) const;
/**
* Reduce the fields to a constant value if possible.
* @param values the named values for replacement.
* @param result the string to store the result in.
* @param onlyAlphanum whether to only allow alpha numeric characters plus underscore.
* @return true if the result is final.
*/
bool reduce(const map<string, string>& values, string& result, bool onlyAlphanum = false) const;
/**
*
* @param remain
* @param circuit
* @param name
* @return the index of the last unmatched part, or the negative index minus one for extra non-matched non-field parts.
*/
ssize_t matchTopic(const string& remain, string* circuit, string* name, string* field) const;
private:
/**
* the list of parts the template is composed of.
* the string is either the plain string or the name of the field.
* the number is negative for plain strings, the index to @a knownFieldNames for a known field, or the size of
* @a knownFieldNames for an unknown field.
*/
vector<std::pair<string, int>> m_parts;
};
/**
* A set of constants and @a MqttReplacer variables.
*/
class MqttReplacers {
public:
/**
* Get the value of the specified key from the constants only.
* @param key the key for which to get the value.
* @return the value string or empty.
*/
const string& operator[](const string& key) const;
/**
* Check if the specified field is used by one of the replacers.
* @param field the name of the field to check.
* @return true if the specified field is used by one of the replacers.
*/
bool uses(const string& field) const;
/**
* Get the variable value of the specified key.
* @param key the key for which to get the value.
* @return the value @a MqttReplacer.
*/
MqttReplacer& get(const string& key);
/**
* Get the variable or constant value of the specified key.
* @param key the key for which to get the value.
* @param value the value string or empty.
*/
string get(const string& key, bool untilFirstEmpty, bool onlyAlphanum = false) const;
/**
* Set the constant value of the specified key and additionally normalized with uppercase key only (if the key does
* not contain an underscore).
* @param key the key to store.
* @param value the value string.
* @param removeReplacer true to remove a replacer with the same name.
* @return true when an upper case key was stored/updates as well.
*/
bool set(const string& key, const string& value, bool removeReplacer = true);
/**
* Set the constant value of the specified key.
* @param key the key to store.
* @param value the numeric value (converted to a string).
*/
void set(const string& key, int value);
/**
* Reduce as many variables to constants as possible.
*/
void reduce();
private:
/** constant values from the integration file. */
map<string, string> m_constants;
/** variable values from the integration file. */
map<string, MqttReplacer> m_replacers;
};
/**
* The main class supporting MQTT data handling.
*/
@@ -68,6 +228,10 @@ class MqttHandler : public DataSink, public DataSource, public WaitThread {
*/
MqttHandler(UserInfo* userInfo, BusHandler* busHandler, MessageMap* messages);
private:
void parseIntegration(const string& line);
public:
/**
* Destructor.
*/
@@ -150,6 +314,24 @@ class MqttHandler : public DataSink, public DataSource, public WaitThread {
/** whether to publish a separate topic for each message field. */
bool m_publishByField;
/** the @a MqttReplacers from the integration file. */
MqttReplacers m_replacers;
/** whether the @a m_replacers includes the definition_topic. */
bool m_hasDefinitionTopic;
/** whether the @a m_replacers uses the fields_payload variable. */
bool m_hasDefinitionFieldsPayload;
/** the subscribed configuration restart topic, or empty. */
string m_subscribeConfigRestartTopic;
/** the expected payload of the subscribed configuration restart topic, or empty for any. */
string m_subscribeConfigRestartPayload;
/** the last system time when the message definitions were published. */
time_t m_definitionsSince;
/** the mosquitto structure if initialized, or nullptr. */
struct mosquitto* m_mosquitto;