From 11c83c1f371917f316d56e34b9100a6510c870d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20=C3=96hrstr=C3=B6m?= Date: Sat, 2 Mar 2024 15:42:39 +0100 Subject: [PATCH 1/6] Remove unused code. --- src/address.cc | 179 ++----------------------------------------------- src/address.h | 22 ------ wmbusmeters.1 | 2 +- 3 files changed, 6 insertions(+), 197 deletions(-) diff --git a/src/address.cc b/src/address.cc index e6384c0..4115dbc 100644 --- a/src/address.cc +++ b/src/address.cc @@ -23,6 +23,11 @@ using namespace std; vector splitSequenceOfAddressExpressionsAtCommas(const string& mes); +bool isValidMatchExpression(const std::string& s, bool *has_wildcard); +bool doesIdMatchExpression(const std::string& id, std::string match_rule); +bool doesAddressMatchExpressions(Address &address, + std::vector& address_expressions, + bool *used_wildcard); bool isValidMatchExpression(const string& s, bool *has_wildcard) { @@ -180,180 +185,6 @@ bool hasWildCard(const string& mes) return mes.find('*') != string::npos; } -bool doesIdsMatchExpressionss(vector &ids, vector& mes, bool *used_wildcard) -{ - bool match = false; - for (string &id : ids) - { - if (doesIdMatchExpressionss(id, mes, used_wildcard)) - { - match = true; - } - // Go through all ids even though there is an early match. - // This way we can see if theres an exact match later. - } - return match; -} - -bool doesIdMatchExpressionss(const string& id, vector& mes, bool *used_wildcard) -{ - bool found_match = false; - bool found_negative_match = false; - bool exact_match = false; - *used_wildcard = false; - - // Goes through all possible match expressions. - // If no expression matches, neither positive nor negative, - // then the result is false. (ie no match) - - // If more than one positive match is found, and no negative, - // then the result is true. - - // If more than one negative match is found, irrespective - // if there is any positive matches or not, then the result is false. - - // If a positive match is found, using a wildcard not any exact match, - // then *used_wildcard is set to true. - - for (string me : mes) - { - bool has_wildcard = hasWildCard(me); - bool is_negative_rule = (me.length() > 0 && me.front() == '!'); - if (is_negative_rule) - { - me.erase(0, 1); - } - - bool m = doesIdMatchExpression(id, me); - - if (is_negative_rule) - { - if (m) found_negative_match = true; - } - else - { - if (m) - { - found_match = true; - if (!has_wildcard) - { - exact_match = true; - } - } - } - } - - if (found_negative_match) - { - return false; - } - if (found_match) - { - if (exact_match) - { - *used_wildcard = false; - } - else - { - *used_wildcard = true; - } - return true; - } - return false; -} - -bool doesIdMatchAddressExpressions(const string& id, vector& aes, bool *used_wildcard) -{ -/* bool found_match = false; - bool found_negative_match = false; - bool exact_match = false;*/ - *used_wildcard = false; - - // Goes through all possible match expressions. - // If no expression matches, neither positive nor negative, - // then the result is false. (ie no match) - - // If more than one positive match is found, and no negative, - // then the result is true. - - // If more than one negative match is found, irrespective - // if there is any positive matches or not, then the result is false. - - // If a positive match is found, using a wildcard not any exact match, - // then *used_wildcard is set to true. -/* - for (AddressExpression &ae : aes) - { - bool has_wildcard = ae.has_wildcard; - bool is_negative_rule = (me.length() > 0 && me.front() == '!'); - if (is_negative_rule) - { - me.erase(0, 1); - } - - bool m = doesIdMatchExpression(id, me); - - if (is_negative_rule) - { - if (m) found_negative_match = true; - } - else - { - if (m) - { - found_match = true; - if (!has_wildcard) - { - exact_match = true; - } - } - } - } - - if (found_negative_match) - { - return false; - } - if (found_match) - { - if (exact_match) - { - *used_wildcard = false; - } - else - { - *used_wildcard = true; - } - return true; - } -*/ - return false; -} - -string toIdsCommaSeparated(vector &ids) -{ - string cs; - for (string& s: ids) - { - cs += s; - cs += ","; - } - if (cs.length() > 0) cs.pop_back(); - return cs; -} - -string toIdsCommaSeparated(vector &ids) -{ - string cs; - for (AddressExpression& ae: ids) - { - cs += ae.str(); - cs += ","; - } - if (cs.length() > 0) cs.pop_back(); - return cs; -} - bool AddressExpression::match(const std::string &i, uint16_t m, uchar v, uchar t) { if (!(mfct == 0xffff || mfct == m)) return false; diff --git a/src/address.h b/src/address.h index 6aed8f7..a2de586 100644 --- a/src/address.h +++ b/src/address.h @@ -77,33 +77,11 @@ struct AddressExpression !*.V=33 */ bool isValidSequenceOfAddressExpressions(const std::string& s); - -bool isValidMatchExpression(const std::string& s, bool *has_wildcard); - - -bool doesIdMatchExpression(const std::string& id, - std::string match_rule); -bool doesIdMatchExpressionss(const std::string& id, - std::vector& match_rules, - bool *used_wildcard); -bool doesIdsMatchExpressionss(std::vector &ids, - std::vector& match_rules, - bool *used_wildcard); -std::string toIdsCommaSeparated(std::vector &ids); -std::string toIdsCommaSeparated(std::vector &ids); - std::vector splitAddressExpressions(const std::string &aes); - bool flagToManufacturer(const char *s, uint16_t *out_mfct); - std::string manufacturerFlag(int m_field); - bool doesTelegramMatchExpressions(std::vector
&addresses, std::vector& address_expressions, bool *used_wildcard); -bool doesAddressMatchExpressions(Address &address, - std::vector& address_expressions, - bool *used_wildcard); - #endif diff --git a/wmbusmeters.1 b/wmbusmeters.1 index 0a2fab0..bb3c176 100644 --- a/wmbusmeters.1 +++ b/wmbusmeters.1 @@ -176,7 +176,7 @@ and the other set to beta. Alfa has an antenna tuned for 433M, beta has an anten .TP \fBmeter_type\fR for example multical21:t1 (suffix means that we expect this meter to transmit t1 telegrams) the driver auto can be used, but is not recommended for production. .TP -\fBmeter_id\fR one or more 8 digit numbers separated with commas, a single '*' wildcard, or a prefix '76543*' with wildcard. +\fBmeter_id\fR one or more addresses separated with commas, a single '*' wildcard, or a prefix '76543*' with wildcard. You can as a suffix fully or partially specify manufacturer, version and type: 12345678.M=KAM.V=1b.T=16 You can use p0 to p250 to specify an mbus primary address. .TP \fBmeter_key\fR a unique key for the meter, if meter telegrams are not encrypted, you must supply an empty key: "" From 5962e727ff4a41786d1730eae44f00d6d6d0d7f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20=C3=96hrstr=C3=B6m?= Date: Sat, 2 Mar 2024 22:46:50 +0100 Subject: [PATCH 2/6] Handle more address rules. --- src/address.cc | 29 ++++++++++++++++---- src/cmdline.cc | 4 +-- src/config.cc | 10 +++---- src/main.cc | 8 ++++++ src/metermanager.cc | 1 + src/testinternals.cc | 63 ++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 103 insertions(+), 12 deletions(-) diff --git a/src/address.cc b/src/address.cc index 4115dbc..b846d4b 100644 --- a/src/address.cc +++ b/src/address.cc @@ -27,7 +27,8 @@ bool isValidMatchExpression(const std::string& s, bool *has_wildcard); bool doesIdMatchExpression(const std::string& id, std::string match_rule); bool doesAddressMatchExpressions(Address &address, std::vector& address_expressions, - bool *used_wildcard); + bool *used_wildcard, + bool *filtered_out); bool isValidMatchExpression(const string& s, bool *has_wildcard) { @@ -94,7 +95,7 @@ vector splitSequenceOfAddressExpressionsAtCommas(const string& mes) auto i = v.begin(); for (;;) { - auto id = eatTo(v, i, ',', 16, &eof, &err); + auto id = eatTo(v, i, ',', 64, &eof, &err); if (err) break; trimWhitespace(&id); if (id == "ANYID") id = "*"; @@ -277,6 +278,19 @@ bool AddressExpression::parse(const string &in) bool ok = flagToManufacturer(&parts[i][2], &mfct); if (!ok) return false; } + else if (parts[i].size() == 6) // M=abcd explicit hex version + { + if (parts[i][1] != '=') return false; + if (parts[i][0] != 'M') return false; + + vector data; + bool ok = hex2bin(&parts[i][2], &data); + if (!ok) return false; + if (data.size() != 2) return false; + + mfct = data[1] << 8 | data[0]; + if (!ok) return false; + } else { return false; @@ -396,21 +410,25 @@ bool doesTelegramMatchExpressions(std::vector
&addresses, bool *used_wildcard) { bool match = false; + bool filtered_out = false; for (Address &a : addresses) { - if (doesAddressMatchExpressions(a, address_expressions, used_wildcard)) + if (doesAddressMatchExpressions(a, address_expressions, used_wildcard, &filtered_out)) { match = true; } // Go through all ids even though there is an early match. // This way we can see if theres an exact match later. } + // If any expression triggered a filter out, then the whole telegram does not match. + if (filtered_out) match = false; return match; } bool doesAddressMatchExpressions(Address &address, vector& address_expressions, - bool *used_wildcard) + bool *used_wildcard, + bool *filtered_out) { bool found_match = false; bool found_negative_match = false; @@ -433,7 +451,7 @@ bool doesAddressMatchExpressions(Address &address, bool has_wildcard = ae.has_wildcard; bool is_negative_rule = ae.filter_out; - bool m = doesIdMatchExpression(address.id, ae.id); + bool m = ae.match(address.id, address.mfct, address.version, address.type); if (is_negative_rule) { @@ -453,6 +471,7 @@ bool doesAddressMatchExpressions(Address &address, } if (found_negative_match) { + *filtered_out = true; return false; } if (found_match) diff --git a/src/cmdline.cc b/src/cmdline.cc index 7589a15..129d185 100644 --- a/src/cmdline.cc +++ b/src/cmdline.cc @@ -728,11 +728,11 @@ static shared_ptr parseNormalCommandLine(Configuration *c, int ar string bus; string name = argv[m*4+i+0]; string driver = argv[m*4+i+1]; - string id = argv[m*4+i+2]; + string address_expressions = argv[m*4+i+2]; string key = argv[m*4+i+3]; MeterInfo mi; - mi.parse(name, driver, id, key); + mi.parse(name, driver, address_expressions, key); mi.poll_interval = c->pollinterval; if (mi.driver_name.str() == "") diff --git a/src/config.cc b/src/config.cc index d780519..0e0c2da 100644 --- a/src/config.cc +++ b/src/config.cc @@ -53,7 +53,7 @@ void parseMeterConfig(Configuration *c, vector &buf, string file) string bus; string name; string driver = "auto"; - string id; + string address_expressions; string key = ""; string linkmodes; int poll_interval = 0; @@ -108,7 +108,7 @@ void parseMeterConfig(Configuration *c, vector &buf, string file) else if (p.first == "driver") driver = p.second; else - if (p.first == "id") id = p.second; + if (p.first == "id") address_expressions = p.second; else if (p.first == "key") { @@ -176,11 +176,11 @@ void parseMeterConfig(Configuration *c, vector &buf, string file) MeterInfo mi; - mi.parse(name, driver, id, key); // sets driver, extras, name, bus, bps, link_modes, ids, name, key + mi.parse(name, driver, address_expressions, key); // sets driver, extras, name, bus, bps, link_modes, ids, name, key mi.poll_interval = poll_interval; - if (!isValidSequenceOfAddressExpressions(id)) { - warning("Not a valid meter id nor a valid sequence of match expression \"%s\"\n", id.c_str()); + if (!isValidSequenceOfAddressExpressions(address_expressions)) { + warning("Not a valid meter id nor a valid sequence of match expression \"%s\"\n", address_expressions.c_str()); use = false; } if (!isValidKey(key, mi)) { diff --git a/src/main.cc b/src/main.cc index ebd95ef..9233b45 100644 --- a/src/main.cc +++ b/src/main.cc @@ -403,6 +403,14 @@ void log_start_information(Configuration *config) verbose("(config) using device: %s \n", specified_device.str().c_str()); } verbose("(config) number of meters: %d\n", config->meters.size()); + if (isDebugEnabled()) + { + for (MeterInfo &m : config->meters) + { + string aes = AddressExpression::concat(m.address_expressions); + debug("(config) template %s %s %s\n", m.name.c_str(), aes.c_str(), m.str().c_str()); + } + } } void oneshot_check(Configuration *config, Telegram *t, Meter *meter) diff --git a/src/metermanager.cc b/src/metermanager.cc index c5d1579..a23d29a 100644 --- a/src/metermanager.cc +++ b/src/metermanager.cc @@ -187,6 +187,7 @@ public: aes.push_back(AddressExpression(t.addresses.back())); meter_info.address_expressions = aes; + // Overwrite the mfct,version and type. if (meter_info.driverName().str() == "auto") { // Look up the proper meter driver! diff --git a/src/testinternals.cc b/src/testinternals.cc index c2c7d6e..07089cc 100644 --- a/src/testinternals.cc +++ b/src/testinternals.cc @@ -575,6 +575,42 @@ void tst_address_match(string expr, string id, uint16_t m, uchar v, uchar t, boo } } +void tst_telegram_match(string addresses, string expressions, bool match, bool uw) +{ + vector exprs = splitAddressExpressions(expressions); + vector as = splitAddressExpressions(addresses); + vector
addrs; + + for (auto &ad : as) + { + Address a; + a.id = ad.id; + a.mfct = ad.mfct; + a.version = ad.version; + a.type = ad.type; + + addrs.push_back(a); + } + + bool used_wildcard = false; + bool m = doesTelegramMatchExpressions(addrs, exprs, &used_wildcard); + + if (m != match) + { + printf("Expected addresses %s to %smatch expressions %s\n", + addresses.c_str(), + match?"":"NOT ", + expressions.c_str()); + } + if (uw != used_wildcard) + { + printf("Expected addresses %s from match expression %s %susing wildcard\n", + addresses.c_str(), + expressions.c_str(), + uw?"":"NOT "); + } +} + void test_addresses() { tst_address("12345678", @@ -634,6 +670,33 @@ void test_addresses() tst_address_match("!9*.V=06", "89999999", MANUFACTURER_ABB, 0x06, 1, false, true); tst_address_match("!9*.V=06", "99999999", MANUFACTURER_ABB, 0x07, 1, false, true); tst_address_match("!9*.V=06", "89999999", MANUFACTURER_ABB, 0x07, 1, false, true); + + tst_telegram_match("12345678", "12345678", true, false); + tst_telegram_match("11111111,22222222", "12345678,22*", true, true); + tst_telegram_match("11111111,22222222", "12345678,22222222", true, false); + tst_telegram_match("11111111.M=KAM,22222222.M=PII", "11111111.M=KAM", true, false); + tst_telegram_match("11111111.M=KAF", "11111111.M=KAM", false, false); + + tst_telegram_match("11111111.M=KAM.V=1b.T=16", "11111111.M=KAM", true, false); + tst_telegram_match("11111111.M=KAM.V=1b.T=16", "11111111.M=KAF", false, false); + tst_telegram_match("11111111.M=KAM.V=1b.T=16", "11111111", true, false); + tst_telegram_match("11111111.M=KAM.V=1b.T=16", "11111111.M=KAM", true, false); + tst_telegram_match("11111111.M=KAM.V=1b.T=16", "11111111.V=1b", true, false); + tst_telegram_match("11111111.M=KAM.V=1b.T=16", "11111111.T=16", true, false); + tst_telegram_match("11111111.M=KAM.V=1b.T=16", "11111111.M=KAM.T=16", true, false); + tst_telegram_match("11111111.M=KAM.V=1b.T=16", "11111111.M=KAM.V=1b", true, false); + tst_telegram_match("11111111.M=KAM.V=1b.T=16", "11111111.T=16.V=1b", true, false); + + tst_telegram_match("11111111.M=KAM.V=1b.T=16", "11111111.M=KAL", false, false); + tst_telegram_match("11111111.M=KAM.V=1b.T=16", "11111111.V=1c", false, false); + tst_telegram_match("11111111.M=KAM.V=1b.T=16", "11111111.T=17", false, false); + tst_telegram_match("11111111.M=KAM.V=1b.T=16", "11111111.M=KAM.T=17", false, false); + tst_telegram_match("11111111.M=KAM.V=1b.T=16", "11111111.M=KAL.V=1b", false, false); + tst_telegram_match("11111111.M=KAM.V=1b.T=16", "11111111.T=17.V=1b", false, false); + + // Test * matches both 11111111 and 2222222 but the only the 111111 matches the filter out V=1b. + // Verify that the filter out !1*.V=1b will override successfull match (with no filter out) * for 22222222. + tst_telegram_match("11111111.M=KAM.V=1b.T=16,22222222.M=XXX.V=aa.T=99", "*,!1*.V=1b", false, true); } void eq(string a, string b, const char *tn) From 23779cb9f71af2af8b03cf52baac9a4ce4b0caa8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20=C3=96hrstr=C3=B6m?= Date: Sun, 3 Mar 2024 12:57:40 +0100 Subject: [PATCH 3/6] Add option --identitymode=... --- CHANGES | 10 ++ simulations/simulation_identity_mode.txt | 2 + src/address.cc | 111 ++++++++++++++++++++++ src/address.h | 36 +++++++- src/cmdline.cc | 10 ++ src/config.cc | 11 +++ src/config.h | 1 + src/metermanager.cc | 44 ++++++--- src/meters.cc | 113 +++++++---------------- src/meters.h | 2 + src/meters_common_implementation.h | 3 +- test.sh | 6 ++ tests/test_addresses.sh | 84 +++++++++++++++++ tests/test_aes.sh | 6 +- tests/test_identity_mode.sh | 38 ++++++++ tests/test_key_warnings.sh | 4 +- tests/test_wrongkeys.sh | 4 +- wmbusmeters.1 | 2 + 18 files changed, 381 insertions(+), 106 deletions(-) create mode 100644 simulations/simulation_identity_mode.txt create mode 100755 tests/test_addresses.sh create mode 100755 tests/test_identity_mode.sh diff --git a/CHANGES b/CHANGES index 97927a1..127c130 100644 --- a/CHANGES +++ b/CHANGES @@ -10,6 +10,16 @@ filter out some versions, do: 12345678,!12345678.V=77 You can now specify p0 to p250, to read from an mbus using the primary address. E.g. wmbusmeters --pollinterval=5s /dev/ttyUSB1:mbus:2400 TEMP piigth:mbus p0 NOKEY +Added option --identitymode=(id|id-mfct|full|none) to specify how +wmbusmeters groups meter state when receiving telegrams. + +The default (which is the same as before) is to map state based only on id. +This usually works ok, however if you have two meters with the same id, but +from different manufacturers, you must separate their state with --identitymode=id-mfct +Full takes into account version and type as well. None means do not separate state +at all, used with wildcards and meters that do not need to keep state, ie all info +is in every telegram. + Version 1.16.1 2024-02-22 Fix docker file generation. diff --git a/simulations/simulation_identity_mode.txt b/simulations/simulation_identity_mode.txt new file mode 100644 index 0000000..7211b08 --- /dev/null +++ b/simulations/simulation_identity_mode.txt @@ -0,0 +1,2 @@ +telegram=|7B4479169977997730378C208B900F002C25E4EF0A002EA98E7D58B3ADC57299779977991611028B005087102F2F#0DFD090F34302e3030562030303030303030300D790E31323334353637383839595345310DFD100AAAAAAAAAAAAAAAAAAAAA0D780E31323334353637383930594553312F2F2F2F2F2F2F2F2F2F2F| +telegram=|7B4479169977997730378C20F0900F002C2549EE0A0077C19D3D1A08ABCD729977997779161102F0005007102F2F#0702F5C3FA000000000007823C5407000000000000841004E081020084200415000000042938AB000004A9FF01FA0A000004A9FF02050A000004A9FF03389600002F2F2F2F2F2F2F2F2F2F2F2F2F| diff --git a/src/address.cc b/src/address.cc index b846d4b..e666532 100644 --- a/src/address.cc +++ b/src/address.cc @@ -19,6 +19,8 @@ #include"manufacturers.h" #include +#include +#include using namespace std; @@ -196,6 +198,36 @@ bool AddressExpression::match(const std::string &i, uint16_t m, uchar v, uchar t return true; } +void AddressExpression::trimToIdentity(IdentityMode im, Address &a) +{ + switch (im) + { + case IdentityMode::FULL: + id = a.id; + mfct = a.mfct; + version = a.version; + type = a.type; + required = true; + break; + case IdentityMode::ID_MFCT: + id = a.id; + mfct = a.mfct; + version = 0xff; + type = 0xff; + required = true; + break; + case IdentityMode::ID: + id = a.id; + mfct = 0xffff; + version = 0xff; + type = 0xff; + required = true; + break; + default: + break; + } +} + bool AddressExpression::parse(const string &in) { string s = in; @@ -433,6 +465,7 @@ bool doesAddressMatchExpressions(Address &address, bool found_match = false; bool found_negative_match = false; bool exact_match = false; + bool failed_required_match = false; // Goes through all possible match expressions. // If no expression matches, neither positive nor negative, @@ -446,10 +479,13 @@ bool doesAddressMatchExpressions(Address &address, // If a positive match is found, using a wildcard not any exact match, // then *used_wildcard is set to true. + + // If an expression is required and it fails, then the match fails. for (AddressExpression &ae : address_expressions) { bool has_wildcard = ae.has_wildcard; bool is_negative_rule = ae.filter_out; + bool is_required = ae.required; bool m = ae.match(address.id, address.mfct, address.version, address.type); @@ -467,8 +503,21 @@ bool doesAddressMatchExpressions(Address &address, exact_match = true; } } + else + { + // No match + if (is_required) + { + // Oups! A required match! + failed_required_match = true; + } + } } } + if (failed_required_match) + { + return false; + } if (found_negative_match) { *filtered_out = true; @@ -488,3 +537,65 @@ bool doesAddressMatchExpressions(Address &address, } return false; } + +const char *toString(IdentityMode im) +{ + switch (im) + { + case IdentityMode::ID: return "id"; + case IdentityMode::ID_MFCT: return "id-mfct"; + case IdentityMode::FULL: return "full"; + case IdentityMode::NONE: return "none"; + case IdentityMode::INVALID: return "invalid"; + } + return "?"; +} + +IdentityMode toIdentityMode(const char *s) +{ + if (!strcmp(s,"id")) return IdentityMode::ID; + if (!strcmp(s,"id-mfct")) return IdentityMode::ID_MFCT; + if (!strcmp(s, "full")) return IdentityMode::FULL; + if (!strcmp(s, "none")) return IdentityMode::NONE; + return IdentityMode::INVALID; +} + +void AddressExpression::clear() +{ + id = ""; + has_wildcard = false; + mbus_primary = false; + mfct = 0xffff; + version = 0xff; + type = 0xff; +} + +void AddressExpression::appendIdentity(IdentityMode im, + AddressExpression *identity_expression, + std::vector
&as, + std::vector &es) +{ + identity_expression->clear(); + if (im == IdentityMode::NONE) return; + + // Copy id, id-mfct, id-mfct-v-t to identity_expression from the last address. + identity_expression->trimToIdentity(im, as.back()); + + // Is this identity expression already in the list of address expressions? + if (std::find(es.begin(), es.end(), *identity_expression) == es.end()) + { + // No, then add it at the end. + es.push_back(*identity_expression); + } +} + +bool AddressExpression::operator==(const AddressExpression&ae) const +{ + return id == ae.id && + has_wildcard == ae.has_wildcard&& + mbus_primary == ae.mbus_primary && + mfct == ae.mfct && + version == ae.version && + type == ae.type && + filter_out == ae.filter_out; +} diff --git a/src/address.h b/src/address.h index a2de586..23542f0 100644 --- a/src/address.h +++ b/src/address.h @@ -21,6 +21,28 @@ #include "util.h" #include +/** + IdentityMode: + + @ID: The default, only the id groups the meter content. + @ID_MFCT: Used when you have two meters with the same id but different manufacturers. + @FULL: Used when you want to fully separate meter content on id.mft.v.t + @NONE: Do not separate any meters! This might lead to telegrams overwriting each others state. + Use this when no state is to be kept in the wmbusmeters object. + @INVALID: Cannot parse cmdline. +*/ +enum class IdentityMode +{ + ID, + ID_MFCT, + FULL, + NONE, + INVALID +}; + +const char *toString(IdentityMode im); +IdentityMode toIdentityMode(const char *s); + struct Address { std::string id; // p1 or 12345678 or non-compliant hex: 1234abcd @@ -53,18 +75,26 @@ struct AddressExpression bool has_wildcard {}; // The id contains a * bool mbus_primary {}; // Signals that the id is 0-250 - uint16_t mfct {}; // If 0xffff then any mfct matches this address. - uchar version {}; // If 0xff then any version matches this address. - uchar type {}; // If 0xff then any type matches this address. + uint16_t mfct { 0xffff }; // If 0xffff then any mfct matches this address. + uchar version { 0xff }; // If 0xff then any version matches this address. + uchar type { 0xff }; // If 0xff then any type matches this address. bool filter_out {}; // Telegrams matching this rule should be filtered out! + bool required {}; // If true, then this address expression must be matched! AddressExpression() {} AddressExpression(Address &a) : id(a.id), mfct(a.mfct), version(a.version), type(a.type) { } + bool operator==(const AddressExpression&) const; + void clear(); + void trimToIdentity(IdentityMode im, Address &a); bool parse(const std::string &s); bool match(const std::string &id, uint16_t mfct, uchar version, uchar type); std::string str(); static std::string concat(std::vector &address_expressions); + static void appendIdentity(IdentityMode im, + AddressExpression *identity_expression, + std::vector
&as, + std::vector &es); }; /** diff --git a/src/cmdline.cc b/src/cmdline.cc index 129d185..b4003f2 100644 --- a/src/cmdline.cc +++ b/src/cmdline.cc @@ -617,6 +617,15 @@ static shared_ptr parseNormalCommandLine(Configuration *c, int ar i++; continue; } + if (!strncmp(argv[i], "--identitymode=", 15) && strlen(argv[i]) > 15) { + c->identity_mode = toIdentityMode(argv[i]+15); + if (c->identity_mode == IdentityMode::INVALID) + { + error("Not a valid identity mode. \"%s\"\n", argv[i]+15); + } + i++; + continue; + } if (!strncmp(argv[i], "--resetafter=", 13) && strlen(argv[i]) > 13) { c->resetafter = parseTime(argv[i]+13); if (c->resetafter <= 0) { @@ -734,6 +743,7 @@ static shared_ptr parseNormalCommandLine(Configuration *c, int ar MeterInfo mi; mi.parse(name, driver, address_expressions, key); mi.poll_interval = c->pollinterval; + mi.identity_mode = c->identity_mode; if (mi.driver_name.str() == "") { diff --git a/src/config.cc b/src/config.cc index 0e0c2da..c7cddc0 100644 --- a/src/config.cc +++ b/src/config.cc @@ -57,6 +57,7 @@ void parseMeterConfig(Configuration *c, vector &buf, string file) string key = ""; string linkmodes; int poll_interval = 0; + IdentityMode identity_mode {}; vector telegram_shells; vector meter_shells; vector alarm_shells; @@ -129,6 +130,15 @@ void parseMeterConfig(Configuration *c, vector &buf, string file) } } else + if (p.first == "identitymode") { + identity_mode = toIdentityMode(p.second.c_str()); + + if (identity_mode == IdentityMode::INVALID) + { + error("Invalid identity mode: \"%s\"!\n", p.second.c_str()); + } + } + else if (p.first == "shell") { telegram_shells.push_back(p.second); } @@ -178,6 +188,7 @@ void parseMeterConfig(Configuration *c, vector &buf, string file) mi.parse(name, driver, address_expressions, key); // sets driver, extras, name, bus, bps, link_modes, ids, name, key mi.poll_interval = poll_interval; + mi.identity_mode = identity_mode; if (!isValidSequenceOfAddressExpressions(address_expressions)) { warning("Not a valid meter id nor a valid sequence of match expression \"%s\"\n", address_expressions.c_str()); diff --git a/src/config.h b/src/config.h index d940784..6df3be8 100644 --- a/src/config.h +++ b/src/config.h @@ -99,6 +99,7 @@ struct Configuration bool json {}; bool pretty_print_json {}; int pollinterval {}; // Time between polling of mbus meters. + IdentityMode identity_mode {}; // How to group meters identities into state objects. bool fields {}; char separator { ';' }; std::vector telegram_shells; diff --git a/src/metermanager.cc b/src/metermanager.cc index a23d29a..e079e30 100644 --- a/src/metermanager.cc +++ b/src/metermanager.cc @@ -177,17 +177,29 @@ public: { // We found a match, make a copy of the meter info. MeterInfo meter_info = mi; - // Overwrite the wildcard pattern with the highest level id. - // The last id in the t.ids is the highest level id. - // For example: a telegram can have dll_id,tpl_id - // This will pick the tpl_id. - // Or a telegram can have a single dll_id, - // then the dll_id will be picked. - vector aes; - aes.push_back(AddressExpression(t.addresses.back())); - meter_info.address_expressions = aes; + // Append the identity to the address expressions. + // The identity is by default the highest level id found. + // I.e. often the tpl_id. This is the last element in t->addresses. + // + // When instantiating a meter from a template we + // make sure the meter triggers exactly on this identity. + // So we append the identity to the address expressions. + // + // E.g. if the template address expression is 12*.M=PII and the meter + // 12345678 is received then the meters address expressions + // will be: 12*.M=PII,12345678 + // + // The default type of identity can be changed. + // identitymode=id + // identitymode=id-mfct + // identitymode=full + // identitymode=none + AddressExpression identity_expression; + AddressExpression::appendIdentity(mi.identity_mode, + &identity_expression, + t.addresses, + meter_info.address_expressions); - // Overwrite the mfct,version and type. if (meter_info.driverName().str() == "auto") { // Look up the proper meter driver! @@ -222,20 +234,24 @@ public: if (is_daemon_) { string mi_idsc = AddressExpression::concat(mi.address_expressions); - notice("(wmbusmeters) started meter %d (%s %s %s)\n", + notice("(wmbusmeters) started meter %d (%s %s %s) identity mode: %s %s\n", meter->index(), mi.name.c_str(), mi_idsc.c_str(), - mi.driverName().str().c_str()); + mi.driverName().str().c_str(), + toString(mi.identity_mode), + identity_expression.str().c_str()); } else { string mi_idsc = AddressExpression::concat(mi.address_expressions); - verbose("(meter) started meter %d (%s %s %s)\n", + verbose("(meter) started meter %d (%s %s %s) identity mode: %s %s\n", meter->index(), mi.name.c_str(), mi_idsc.c_str(), - mi.driverName().str().c_str()); + mi.driverName().str().c_str(), + toString(mi.identity_mode), + identity_expression.str().c_str()); } bool match = false; diff --git a/src/meters.cc b/src/meters.cc index 40eb3cf..a3f15e1 100644 --- a/src/meters.cc +++ b/src/meters.cc @@ -328,6 +328,7 @@ MeterCommonImplementation::MeterCommonImplementation(MeterInfo &mi, waiting_for_poll_response_sem_("waiting_for_poll_response") { address_expressions_ = mi.address_expressions; + identity_mode_ = mi.identity_mode; link_modes_ = mi.link_modes; poll_interval_= mi.poll_interval; @@ -783,6 +784,11 @@ vector& MeterCommonImplementation::addressExpressions() return address_expressions_; } +IdentityMode MeterCommonImplementation::identityMode() +{ + return identity_mode_; +} + vector &MeterCommonImplementation::fieldInfos() { return field_infos_; @@ -932,11 +938,11 @@ bool MeterCommonImplementation::isTelegramForMeter(Telegram *t, Meter *meter, Me } bool used_wildcard = false; - bool id_match = doesTelegramMatchExpressions(t->addresses, address_expressions, &used_wildcard); + bool match = doesTelegramMatchExpressions(t->addresses, address_expressions, &used_wildcard); - if (!id_match) { + if (!match) { // The id must match. - debug("(meter) %s: not for me: not my id\n", name.c_str()); + debug("(meter) %s: not for me: no match\n", name.c_str()); return false; } @@ -952,16 +958,11 @@ bool MeterCommonImplementation::isTelegramForMeter(Telegram *t, Meter *meter, Me // this particular driver, mfct, media, version combo // is not registered in the METER_DETECTION list in meters.h - /* - if (used_wildcard) - { - // The match for the id was not exact, thus the user is listening using a wildcard - // to many meters and some received matched meter telegrams are not from the right meter type, - // ie their driver does not match. Lets just ignore telegrams that probably cannot be decoded properly. - verbose("(meter) ignoring telegram from %s since it matched a wildcard id rule but driver (%s) does not match.\n", - t->idsc.c_str(), driver_name.c_str()); - return false; - }*/ + // There was an attempt to give up here if there was a wildcard and it was the wrong driver. + // However some users did expect it to work anyway! This might make sense + // in the future when we have even better dynamic drivers. + // It already make sense if you create an amalgamation driver for several different + // types of meters and want to force the use of this driver. // The match was exact, ie the user has actually specified 12345678 and foo as driver even // though they do not match. Lets warn and then proceed. It is common that a user tries a @@ -1041,6 +1042,21 @@ string findField(string key, vector *extra_constant_fields) return ""; } +string build_id(Address &a, IdentityMode im) +{ + string id = a.id; + if (im == IdentityMode::ID_MFCT || + im == IdentityMode::FULL) + { + id += string(".M=")+manufacturerFlag(a.mfct); + } + if (im == IdentityMode::FULL) + { + id += tostrprintf(".V=%02x.T=%02x", a.version, a.type); + } + return id; +} + // Is the desired field one of the fields common to all meters and telegrams? bool checkCommonField(string *buf, string desired_field, Meter *m, Telegram *t, char c, bool human_readable) { @@ -1051,7 +1067,8 @@ bool checkCommonField(string *buf, string desired_field, Meter *m, Telegram *t, } if (desired_field == "id") { - *buf += t->addresses.back().id + c; + string id = build_id(t->addresses.back(), m->identityMode()); + *buf += id + c; return true; } if (desired_field == "timestamp") @@ -1802,7 +1819,7 @@ void MeterCommonImplementation::printMeter(Telegram *t, string id = ""; if (t->addresses.size() > 0) { - id = t->addresses.back().id; + id = build_id(t->addresses.back(), identityMode()); } string indent = ""; @@ -1862,72 +1879,6 @@ void MeterCommonImplementation::printMeter(Telegram *t, } } } - /* - for (FieldInfo& fi : field_infos_) - { - if (fi.printProperties().hasHIDE()) continue; - - // The field should be printed in the json. (Most usually should.) - for (auto& i : t->dv_entries) - { - // Check each telegram dv entry. - DVEntry *dve = &i.second.second; - // Has the entry been matches to this field, then print it as json. - if (dve->hasFieldInfo(&fi)) - { - assert(founds[&fi].count(dve) == 0); - - founds[&fi].insert(dve); - string field_name = fi.generateFieldNameNoUnit(dve); - found_vnames.insert(field_name); - } - } - } - - for (FieldInfo& fi : field_infos_) - { - if (fi.printProperties().hasHIDE()) continue; - - if (founds.count(&fi) != 0) - { - // This field info has matched against some dventries. - for (DVEntry *dve : founds[&fi]) - { - debug("(meters) render field %s(%s %s)[%d] with dventry @%d key %s data %s\n", - fi.vname().c_str(), toString(fi.xuantity()), unitToStringLowerCase(fi.displayUnit()).c_str(), fi.index(), - dve->offset, - dve->dif_vif_key.str().c_str(), - dve->value.c_str()); - string out = fi.renderJson(this, dve); - debug("(meters) %s\n", out.c_str()); - s += indent+out+","+newline; - } - } - else - { - // Ok, no value found in received telegram. - // Print field anyway if it is required, - // or if a value has been received before and this field has not been received using a different rule. - // Why this complicated rule? - // E.g. the minmoess mbus seems to use storage 1 for target_m3 but the wmbus version uses storage 8. - // I.e. we have two rules that store into target_m3, this check will prevent target_m3 from being printed twice. - if (fi.printProperties().hasREQUIRED() || - (hasValue(&fi) && ( - found_vnames.count(fi.vname()) == 0 || - fi.hasFormula()))) // TODO! Fix so a new field total_l does not overwrite total_m3 in mem. - { - // No telegram entries found, but this field should be printed anyway. - // It will be printed with any value received from a previous telegram. - // Or if no value has been received, null. - debug("(meters) render field %s(%s)[%d] without dventry\n", - fi.vname().c_str(), toString(fi.xuantity()), fi.index()); - string out = fi.renderJson(this, NULL); - debug("(meters) %s\n", out.c_str()); - s += indent+out+","+newline; - } - } - } - */ s += indent+"\"timestamp\":\""+datetimeOfUpdateRobot()+"\""; if (t->about.device != "") diff --git a/src/meters.h b/src/meters.h index 933282a..691f7af 100644 --- a/src/meters.h +++ b/src/meters.h @@ -91,6 +91,7 @@ struct MeterInfo DriverName driver_name; // Will replace MeterDriver. string extras; // Extra driver specific settings. vector address_expressions; // Match expressions for ids. + IdentityMode identity_mode {}; // How to group telegram content into objects with state. Default is by id. string key; // Decryption key. LinkModeSet link_modes; int bps {}; // For mbus communication you need to know the baud rate. @@ -373,6 +374,7 @@ struct Meter virtual string bus() = 0; // This meter listens to these address expressions. virtual std::vector& addressExpressions() = 0; + virtual IdentityMode identityMode() = 0; // This meter can report these fields, like total_m3, temp_c. virtual vector &fieldInfos() = 0; virtual vector &extraConstantFields() = 0; diff --git a/src/meters_common_implementation.h b/src/meters_common_implementation.h index 71071ba..85ee8fd 100644 --- a/src/meters_common_implementation.h +++ b/src/meters_common_implementation.h @@ -60,6 +60,7 @@ struct MeterCommonImplementation : public virtual Meter void setIndex(int i); string bus(); vector& addressExpressions(); + IdentityMode identityMode(); vector &fieldInfos(); vector &extraConstantFields(); string name(); @@ -84,7 +85,6 @@ struct MeterCommonImplementation : public virtual Meter static bool isTelegramForMeter(Telegram *t, Meter *meter, MeterInfo *mi); MeterKeys *meterKeys(); -// MeterCommonImplementation(MeterInfo &mi, string driver); MeterCommonImplementation(MeterInfo &mi, DriverInfo &di); ~MeterCommonImplementation() = default; @@ -222,6 +222,7 @@ private: TPLSecurityMode expected_tpl_sec_mode_ {}; string name_; vector address_expressions_; + IdentityMode identity_mode_; vector> on_update_; int num_updates_ {}; time_t datetime_of_update_ {}; diff --git a/test.sh b/test.sh index 17e2940..22f5e7c 100755 --- a/test.sh +++ b/test.sh @@ -123,6 +123,12 @@ if [ "$?" != "0" ]; then RC="1"; fi tests/test_additional_json.sh $PROG if [ "$?" != "0" ]; then RC="1"; fi +tests/test_addresses.sh $PROG +if [ "$?" != "0" ]; then RC="1"; fi + +tests/test_identity_mode.sh $PROG +if [ "$?" != "0" ]; then RC="1"; fi + tests/test_rtlwmbus.sh $PROG if [ "$?" != "0" ]; then RC="1"; fi diff --git a/tests/test_addresses.sh b/tests/test_addresses.sh new file mode 100755 index 0000000..d74abc2 --- /dev/null +++ b/tests/test_addresses.sh @@ -0,0 +1,84 @@ +#!/bin/sh + +PROG="$1" + +mkdir -p testoutput +TEST=testoutput + +TESTNAME="Test addresses" +TESTRESULT="OK" + +# dll-mfct (ESY) dll-id (77887788) dll-version (30) dll-type (37 Radio converter (meter side)) +# tpl-id (77997799) tpl-mfct (ESY) tpl-version (11) tpl-type (02 Electricity meter) +TELEGRAM=7B4479168877887730378C20F0900F002C2549EE0A0077C19D3D1A08ABCD729977997779161102F0005007102F2F_0702F5C3FA000000000007823C5407000000000000841004E081020084200415000000042938AB000004A9FF01FA0A000004A9FF02050A000004A9FF03389600002F2F2F2F2F2F2F2F2F2F2F2F2F +ARGS="--format=fields --selectfields=total_energy_consumption_kwh $TELEGRAM EL esyswm" + +checkResult() { + F=$($PROG $ARGS "$E" NOKEY) + if [ "$F" != "1643.4165" ] + then + echo "EXPECTED 1643.4165 *********************************************" + echo "E=$E" + echo $PROG $ARGS "$E" NOKEY + $PROG $ARGS "$E" NOKEY + echo "*********************************************" + TESTRESULT=ERROR + fi +} + +expectEmpty() { + F=$($PROG $ARGS "$E" NOKEY) + if [ "$F" != "" ] + then + echo "EXPECTED EMPTY OUTPUT *********************************************" + echo "E=$E" + echo $PROG $ARGS "$E" NOKEY + $PROG $ARGS "$E" NOKEY + echo "*********************************************" + TESTRESULT=ERROR + fi +} + +E=77997799 +checkResult + +E=77997799.M=ESY +checkResult + +E=77997799.M=PII +expectEmpty + +E=77* +checkResult + +E=* +checkResult + +E=ANYID +checkResult + +E=77997799.T=02 +checkResult + +E=77887788.T=02 +expectEmpty + +E=7788*.T=37.V=30.M=ESY +checkResult + +E=77997799,!*.V=88 +checkResult + +E=*.T=02 +checkResult + +E=*.T=02,!77* +expectEmpty + +E=*.T=02,!77*.V=11 +expectEmpty + +E=7788*.T=37,!7799*.T=02 +expectEmpty + +echo "$TESTRESULT: $TESTNAME" diff --git a/tests/test_aes.sh b/tests/test_aes.sh index 309e68b..06f2950 100755 --- a/tests/test_aes.sh +++ b/tests/test_aes.sh @@ -36,11 +36,11 @@ TESTRESULT="ERROR" cat > $TEST/test_expected.txt < $TEST/test_output.txt 2>&1 +$PROG --identitymode=id-mfct --format=fields simulations/simulation_identity_mode.txt EL esyswm 77997799 NOKEY >> $TEST/test_output.txt 2>&1 +$PROG --identitymode=full --format=fields simulations/simulation_identity_mode.txt EL esyswm 77997799 NOKEY >> $TEST/test_output.txt 2>&1 + +cat $TEST/test_output.txt | sed 's/[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9] [0-9][0-9]:[0-9][0-9].[0-9][0-9]$/1111-11-11 11:11.11/' > $TEST/test_responses.txt + +cat > $TEST/test_expected.txt < $TEST/expected_err.txt < $TEST/expected_err.txt < $TEST/test_expected.txt Started config rtlwmbus on stdin listening on any (wmbus) WARNING!! decrypted content failed check, did you use the correct decryption key? Permanently ignoring telegrams from id: 88888888 mfct: (APA) Apator, Poland (0x601) type: Water meter (0x07) ver: 0x05 -(meter) newly created meter (ApWater 88888888.M=APA.V=05.T=07 apator162) did not handle telegram! +(meter) newly created meter (ApWater 88888888 apator162) did not handle telegram! (wmbus) WARNING! decrypted payload crc failed check, did you use the correct decryption key? 979f payload crc (calculated 3431) Permanently ignoring telegrams from id: 76348799 mfct: (KAM) Kamstrup Energi (0x2c2d) type: Cold water meter (0x16) ver: 0x1b -(meter) newly created meter (Vatten 76348799.M=KAM.V=1b.T=16 multical21) did not handle telegram! +(meter) newly created meter (Vatten 76348799 multical21) did not handle telegram! EOF diff $TEST/test_expected.txt $TEST/test_stderr.txt diff --git a/wmbusmeters.1 b/wmbusmeters.1 index bb3c176..e83406e 100644 --- a/wmbusmeters.1 +++ b/wmbusmeters.1 @@ -53,6 +53,8 @@ Add :verbose to any analyze to get more verbose analyze output. \fB\--help\fR list all options +\fB\--identitymode\fR=(id,id-mfct,full,none) group meter state based on the identity mode. Default is id. + \fB\--ignoreduplicates\fR= ignore duplicate telegrams, remember the last 10 telegrams. Default is true. \fB\--field_xxx=yyy\fR always add "xxx"="yyy" to the json output and add shell env METER_xxx=yyy The field xxx can also be selected or added using selectfields=. Equivalent older command is --json_xxx=yyy. From 3247a4a576f5ae1c15e43f1711e18572823e9941 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20=C3=96hrstr=C3=B6m?= Date: Sun, 3 Mar 2024 15:44:59 +0100 Subject: [PATCH 4/6] Update README. --- README.md | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 3a58fd6..982bf03 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,25 @@ wireless wm-bus meters. The readings can then be published using MQTT, curled to a REST api, inserted into a database or stored in a log file. +# What does it do? + +Wmbusmeters converts incoming telegrams from (w)mbus/OMS compatible meters like: +`2A442D2C998734761B168D2091D37CAC21576C78_02FF207100041308190000441308190000615B7F616713` + +into human readable tab separated fields: +`MyTapWater 12345678 6.388 m3 6.377 m3 0.000 m3/h 8°C 23°C DRY(dry 22-31 days) 2018-03-05 12:02.50` + +or into computer readable fields: +`MyTapWater;12345678;6.388;6.377;0.000;8;23;DRY(dry 22-31 days);2018-03-05 12:02.50` + +or into json: +```json +{"media":"cold water","meter":"multical21","name":"MyTapWater","id":"12345678","total_m3":6.388,"target_m3":6.377,"max_flow_m3h":0.000,"flow_temperature":8,"external_temperature":23,"current_status":"DRY","time_dry":"22-31 days","time_reversed":"","time_leaking":"","time_bursting":"","timestamp":"2018-02-08T09:07:22Z","device":"im871a[1234567]","rssi_dbm":-40} +``` + +Wmbusmeters can collect telegrams from radio using hardware dongles or rtl-sdr software radio dongles, +or from m-bus meters using serial ports, or from files/pipes. + [FAQ/WIKI/MANUAL pages](https://wmbusmeters.github.io/wmbusmeters-wiki/) The program runs on GNU/Linux, MacOSX, FreeBSD, and Raspberry Pi. @@ -175,7 +194,7 @@ And an mbus meter file in /etc/wmbusmeters.d/MyTempHygro ```ini name=MyTempHygro id=11223344 -driver=piigth:mbus +driver=piigth:MAIN:mbus pollinterval=60s ``` @@ -425,6 +444,7 @@ As {options} you can use: --exitafter=