Merge remote-tracking branch 'origin/master' into fb_add_itron_ultramaxx

pull/1091/head
Andreas Horrer 2024-03-14 21:01:05 +01:00
commit c5ab867acd
24 zmienionych plików z 620 dodań i 317 usunięć

10
CHANGES
Wyświetl plik

@ -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.

Wyświetl plik

@ -6,6 +6,33 @@ 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:
`1844AE4C4455223368077A55000000_041389E20100023B0000`
into human readable:
`MyTapWater 33225544 123.529 m³ 0 m³/h 2024-03-03 19:36:22`
or into csv:
`MyTapWater;33225544;123.529;0;2024-03-03 19:36:45`
or into json:
```json
{
"media":"water",
"meter":"iperl",
"name":"MyTapWater",
"id":"33225544",
"max_flow_m3h":0,
"total_m3":123.529,
"timestamp":"2024-03-03T18:37:00Z"
}
```
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 +202,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 +452,7 @@ As {options} you can use:
--exitafter=<time> exit program after time, eg 20h, 10m 5s
--format=<hr/json/fields> for human readable, json or semicolon separated fields
--help list all options
--identitymode=(id|id-mfct|full|none) group meter state based on the identity mode. Default is id.
--ignoreduplicates=<bool> ignore duplicate telegrams, remember the last 10 telegrams
--field_xxx=yyy always add "xxx"="yyy" to the json output and add shell env METER_xxx=yyy (--json_xxx=yyy also works)
--license print GPLv3+ license

Wyświetl plik

@ -0,0 +1,2 @@
telegram=|7B4479169977997730378C208B900F002C25E4EF0A002EA98E7D58B3ADC57299779977991611028B005087102F2F#0DFD090F34302e3030562030303030303030300D790E31323334353637383839595345310DFD100AAAAAAAAAAAAAAAAAAAAA0D780E31323334353637383930594553312F2F2F2F2F2F2F2F2F2F2F|
telegram=|7B4479169977997730378C20F0900F002C2549EE0A0077C19D3D1A08ABCD729977997779161102F0005007102F2F#0702F5C3FA000000000007823C5407000000000000841004E081020084200415000000042938AB000004A9FF01FA0A000004A9FF02050A000004A9FF03389600002F2F2F2F2F2F2F2F2F2F2F2F2F|

Wyświetl plik

@ -0,0 +1,4 @@
telegram=|414493447514916746377275149167934446044D000020_0C06490000004C0600000000426CFF2CCC080611000000C2086C1F3102FD170000326CFFFF046D330F1432|
telegram=|5b44934475149167463778077975149167934446040dff5f3500823d0000810007c006ffff49000000ff2c000000001f3111000000008000800080008000800080008000800080000000000B002f02fd170000046d390d1432488408|
telegram=|414493447514916746377275149167934446044D000020_0C06490000004C0600000000426CFF2CCC080611000000C2086C1F3102FD170000326CFFFF046D330F1432|
telegram=|5b44934475149167463778077975149167934446040dff5f3500823d0000810007c006ffff49000000ff2c000000001f3111000000008000800080008000800080008000800080000000000B002f02fd170000046d390d1432488408|

Wyświetl plik

@ -19,10 +19,20 @@
#include"manufacturers.h"
#include<assert.h>
#include<algorithm>
#include<string.h>
using namespace std;
vector<string> 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<AddressExpression>& address_expressions,
bool *used_wildcard,
bool *filtered_out,
bool *required_found,
bool *required_failed);
bool isValidMatchExpression(const string& s, bool *has_wildcard)
{
@ -89,7 +99,7 @@ vector<string> 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 = "*";
@ -180,180 +190,6 @@ bool hasWildCard(const string& mes)
return mes.find('*') != string::npos;
}
bool doesIdsMatchExpressionss(vector<string> &ids, vector<string>& 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<string>& 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<AddressExpression>& 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<string> &ids)
{
string cs;
for (string& s: ids)
{
cs += s;
cs += ",";
}
if (cs.length() > 0) cs.pop_back();
return cs;
}
string toIdsCommaSeparated(vector<AddressExpression> &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;
@ -364,6 +200,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;
@ -446,6 +312,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<uchar> 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;
@ -471,6 +350,7 @@ string AddressExpression::str()
string s;
if (filter_out) s = "!";
if (required) s = "R";
s.append(id);
if (mfct != 0xffff)
@ -565,21 +445,37 @@ bool doesTelegramMatchExpressions(std::vector<Address> &addresses,
bool *used_wildcard)
{
bool match = false;
bool filtered_out = false;
bool required_found = false; // An R12345678 field was found.
bool required_failed = true; // Init to fail, set to true if R is satistifed anywhere.
for (Address &a : addresses)
{
if (doesAddressMatchExpressions(a, address_expressions, used_wildcard))
if (doesAddressMatchExpressions(a,
address_expressions,
used_wildcard,
&filtered_out,
&required_found,
&required_failed))
{
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;
// If a required field was found and it failed....
if (required_found && required_failed) match = false;
return match;
}
bool doesAddressMatchExpressions(Address &address,
vector<AddressExpression>& address_expressions,
bool *used_wildcard)
bool *used_wildcard,
bool *filtered_out,
bool *required_found,
bool *required_failed)
{
bool found_match = false;
bool found_negative_match = false;
@ -597,12 +493,18 @@ 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;
// We currently assume that only a single expression is required, the last one!
bool is_required = ae.required;
bool m = doesIdMatchExpression(address.id, ae.id);
if (is_required) *required_found = true;
bool m = ae.match(address.id, address.mfct, address.version, address.type);
if (is_negative_rule)
{
@ -612,16 +514,25 @@ bool doesAddressMatchExpressions(Address &address,
{
if (m)
{
found_match = true;
if (!has_wildcard)
// A match, but the required does not count.
if (!is_required)
{
exact_match = true;
found_match = true;
if (!has_wildcard)
{
exact_match = true;
}
}
else
{
*required_failed = false;
}
}
}
}
if (found_negative_match)
{
*filtered_out = true;
return false;
}
if (found_match)
@ -638,3 +549,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<Address> &as,
std::vector<AddressExpression> &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;
}

Wyświetl plik

@ -21,6 +21,28 @@
#include "util.h"
#include <string>
/**
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<AddressExpression> &address_expressions);
static void appendIdentity(IdentityMode im,
AddressExpression *identity_expression,
std::vector<Address> &as,
std::vector<AddressExpression> &es);
};
/**
@ -77,33 +107,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<std::string>& match_rules,
bool *used_wildcard);
bool doesIdsMatchExpressionss(std::vector<std::string> &ids,
std::vector<std::string>& match_rules,
bool *used_wildcard);
std::string toIdsCommaSeparated(std::vector<std::string> &ids);
std::string toIdsCommaSeparated(std::vector<AddressExpression> &ids);
std::vector<AddressExpression> splitAddressExpressions(const std::string &aes);
bool flagToManufacturer(const char *s, uint16_t *out_mfct);
std::string manufacturerFlag(int m_field);
bool doesTelegramMatchExpressions(std::vector<Address> &addresses,
std::vector<AddressExpression>& address_expressions,
bool *used_wildcard);
bool doesAddressMatchExpressions(Address &address,
std::vector<AddressExpression>& address_expressions,
bool *used_wildcard);
#endif

Wyświetl plik

@ -617,6 +617,15 @@ static shared_ptr<Configuration> 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) {
@ -728,12 +737,13 @@ static shared_ptr<Configuration> 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;
mi.identity_mode = c->identity_mode;
if (mi.driver_name.str() == "")
{

Wyświetl plik

@ -53,10 +53,11 @@ void parseMeterConfig(Configuration *c, vector<char> &buf, string file)
string bus;
string name;
string driver = "auto";
string id;
string address_expressions;
string key = "";
string linkmodes;
int poll_interval = 0;
IdentityMode identity_mode {};
vector<string> telegram_shells;
vector<string> meter_shells;
vector<string> alarm_shells;
@ -108,7 +109,7 @@ void parseMeterConfig(Configuration *c, vector<char> &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")
{
@ -129,6 +130,15 @@ void parseMeterConfig(Configuration *c, vector<char> &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);
}
@ -176,11 +186,12 @@ void parseMeterConfig(Configuration *c, vector<char> &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;
mi.identity_mode = identity_mode;
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)) {

Wyświetl plik

@ -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<std::string> telegram_shells;

Wyświetl plik

@ -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)

Wyświetl plik

@ -177,15 +177,28 @@ 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<AddressExpression> 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);
if (meter_info.driverName().str() == "auto")
{
@ -221,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;

Wyświetl plik

@ -330,6 +330,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;
@ -785,6 +786,11 @@ vector<AddressExpression>& MeterCommonImplementation::addressExpressions()
return address_expressions_;
}
IdentityMode MeterCommonImplementation::identityMode()
{
return identity_mode_;
}
vector<FieldInfo> &MeterCommonImplementation::fieldInfos()
{
return field_infos_;
@ -934,11 +940,14 @@ 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;
}
@ -954,16 +963,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
@ -1043,6 +1047,21 @@ string findField(string key, vector<string> *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)
{
@ -1053,7 +1072,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")
@ -1804,7 +1824,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 = "";
@ -1864,72 +1884,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 != "")

Wyświetl plik

@ -91,6 +91,7 @@ struct MeterInfo
DriverName driver_name; // Will replace MeterDriver.
string extras; // Extra driver specific settings.
vector<AddressExpression> 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<AddressExpression>& addressExpressions() = 0;
virtual IdentityMode identityMode() = 0;
// This meter can report these fields, like total_m3, temp_c.
virtual vector<FieldInfo> &fieldInfos() = 0;
virtual vector<string> &extraConstantFields() = 0;

Wyświetl plik

@ -60,6 +60,7 @@ struct MeterCommonImplementation : public virtual Meter
void setIndex(int i);
string bus();
vector<AddressExpression>& addressExpressions();
IdentityMode identityMode();
vector<FieldInfo> &fieldInfos();
vector<string> &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<AddressExpression> address_expressions_;
IdentityMode identity_mode_;
vector<function<void(Telegram*,Meter*)>> on_update_;
int num_updates_ {};
time_t datetime_of_update_ {};

Wyświetl plik

@ -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<AddressExpression> exprs = splitAddressExpressions(expressions);
vector<AddressExpression> as = splitAddressExpressions(addresses);
vector<Address> 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)

12
test.sh
Wyświetl plik

@ -123,6 +123,18 @@ 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_address_filtering.sh $PROG
if [ "$?" != "0" ]; then RC="1"; fi
tests/test_address_dll.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

Wyświetl plik

@ -0,0 +1,47 @@
#!/bin/sh
PROG="$1"
mkdir -p testoutput
TEST=testoutput
TESTNAME="Test addresses dll"
TESTRESULT="OK"
# Test that selecting the meter using the dll id instead of the tpl id works.
OUT=$($PROG --format=fields --selectfields=total_m3 6644496A1064035514377251345015496A0007EE0050052F2F_0C1359000000026CBE2B82046CA12B8C0413FFFFFFFF8D0493132CFBFEFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF02FD1700002F2F W minomess 55036410 NOKEY)
if [ "$OUT" != "0.059" ]
then
echo "ERROR: Test addresses dll failed"
else
echo "OK: Test addresses dll"
fi
# Test that selecting the meter using the tpl id instead of the dll id works.
OUT=$($PROG --format=fields --selectfields=total_m3 6644496A1064035514377251345015496A0007EE0050052F2F_0C1359000000026CBE2B82046CA12B8C0413FFFFFFFF8D0493132CFBFEFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF02FD1700002F2F W minomess 15503451 NOKEY)
if [ "$OUT" != "0.059" ]
then
echo "ERROR: Test addresses tpl failed"
else
echo "OK: Test addresses tpl"
fi
OUT=$($PROG --format=fields --selectfields=total_m3 6644496A1064035514377251345015496A0007EE0050052F2F_0C1359000000026CBE2B82046CA12B8C0413FFFFFFFF8D0493132CFBFEFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF02FD1700002F2F W minomess '*.T=07' NOKEY)
if [ "$OUT" != "0.059" ]
then
echo "ERROR: Test addresses *.T=07 failed"
else
echo "OK: Test addresses *.T=07"
fi
OUT=$($PROG --format=fields --selectfields=total_m3 6644496A1064035514377251345015496A0007EE0050052F2F_0C1359000000026CBE2B82046CA12B8C0413FFFFFFFF8D0493132CFBFEFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF02FD1700002F2F W minomess '*.T=37' NOKEY)
if [ "$OUT" != "0.059" ]
then
echo "ERROR: Test addresses *.T=37 failed"
else
echo "OK: Test addresses *.T=37"
fi

Wyświetl plik

@ -0,0 +1,28 @@
#!/bin/sh
PROG="$1"
mkdir -p testoutput
TEST=testoutput
TESTNAME="Test address type filtering"
TESTRESULT="OK"
$PROG --format=fields simulations/simulation_qheat_bad.txt HEAT qheat '67911475.T=04' 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 <<EOF
HEAT;67911475;49;2024-01-31;11;1111-11-11 11:11.11
EOF
if ! diff $TEST/test_expected.txt $TEST/test_responses.txt
then
if [ "$USE_MELD" = "true" ]
then
meld $TEST/test_expected.txt $TEST/test_responses.txt
fi
echo "ERROR: $TESTNAME"
else
echo OK: $TESTNAME
fi

Wyświetl plik

@ -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"

Wyświetl plik

@ -36,11 +36,11 @@ TESTRESULT="ERROR"
cat > $TEST/test_expected.txt <<EOF
(wmbus) WARNING!! telegram should have been fully encrypted, but was not! 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? e1d6 payload crc (calculated a528) 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!
(wmbus) WARNING!! telegram should have been fully encrypted, but was not! id: 77777777 mfct: (SON) Sontex, Switzerland (0x4dee) type: Water meter (0x07) ver: 0x3c
(meter) newly created meter (Wasser 77777777.M=SON.V=3c.T=07 supercom587) did not handle telegram!
(meter) newly created meter (Wasser 77777777 supercom587) did not handle telegram!
(wmbus) WARNING!!! telegram should have been encrypted, but was not! id: 77777777 mfct: (SON) Sontex, Switzerland (0x4dee) type: Water meter (0x07) ver: 0x3c
EOF

Wyświetl plik

@ -0,0 +1,38 @@
#!/bin/sh
PROG="$1"
mkdir -p testoutput
TEST=testoutput
TESTNAME="Test identity mode"
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)
$PROG --identitymode=id --format=fields simulations/simulation_identity_mode.txt EL esyswm 77997799 NOKEY > $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 <<EOF
EL;77997799;null;null;null;null;null;null;null;null;1ESY9887654321;1111-11-11 11:11.11
EL;77997799;1643.4165;0.43832;0.1876;1643.2;0.21;0.0281;0.02565;0.38456;1ESY9887654321;1111-11-11 11:11.11
EL;77997799.M=ETY;null;null;null;null;null;null;null;null;1ESY9887654321;1111-11-11 11:11.11
EL;77997799.M=ESY;1643.4165;0.43832;0.1876;1643.2;0.21;0.0281;0.02565;0.38456;null;1111-11-11 11:11.11
EL;77997799.M=ETY.V=11.T=02;null;null;null;null;null;null;null;null;1ESY9887654321;1111-11-11 11:11.11
EL;77997799.M=ESY.V=11.T=02;1643.4165;0.43832;0.1876;1643.2;0.21;0.0281;0.02565;0.38456;null;1111-11-11 11:11.11
EOF
if ! diff $TEST/test_expected.txt $TEST/test_responses.txt
then
if [ "$USE_MELD" = "true" ]
then
meld $TEST/test_expected.txt $TEST/test_responses.txt
fi
echo "ERROR: $TESTNAME"
else
echo OK: $TESTNAME
fi

Wyświetl plik

@ -13,7 +13,7 @@ $PROG --format=json simulations/simulation_bad_keys.txt room fhkvdataiv 03065716
cat > $TEST/expected_err.txt <<EOF
(wmbus) WARNING! no key to decrypt payload! Permanently ignoring telegrams from id: 03065716 mfct: (TCH) Techem Service (0x5068) type: Heat Cost Allocator (0x08) ver: 0x94
(meter) newly created meter (room 03065716.M=TCH.V=94.T=08 fhkvdataiv) did not handle telegram!
(meter) newly created meter (room 03065716 fhkvdataiv) did not handle telegram!
EOF
diff $TEST/test_stderr.txt $TEST/expected_err.txt
@ -28,7 +28,7 @@ $PROG --format=json simulations/simulation_bad_keys.txt room fhkvdataiv 03065716
cat > $TEST/expected_err.txt <<EOF
(wmbus) WARNING!! decrypted content failed check, did you use the correct decryption key? Permanently ignoring telegrams from id: 03065716 mfct: (TCH) Techem Service (0x5068) type: Heat Cost Allocator (0x08) ver: 0x94
(meter) newly created meter (room 03065716.M=TCH.V=94.T=08 fhkvdataiv) did not handle telegram!
(meter) newly created meter (room 03065716 fhkvdataiv) did not handle telegram!
EOF
diff $TEST/test_stderr.txt $TEST/expected_err.txt

Wyświetl plik

@ -28,9 +28,9 @@ fi
cat <<EOF > $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

Wyświetl plik

@ -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=<bool> 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.
@ -176,7 +178,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. You can filter out telegrams: 76543*,!76543210
.TP
\fBmeter_key\fR a unique key for the meter, if meter telegrams are not encrypted, you must supply an empty key: ""