From 84fe29069aa951aa0bd5fea68a4f528923182de9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20=C3=96hrstr=C3=B6m?= Date: Sat, 3 Sep 2022 13:25:37 +0200 Subject: [PATCH] Refactor multical21 and flowiq drivers. --- CHANGES | 12 + README.md | 6 +- simulations/serial_aes.msg | 2 +- simulations/simulation_additional_json.txt | 2 +- simulations/simulation_c1.txt | 12 +- simulations/simulation_conversionsadded.txt | 2 +- src/driver_flowiq2200.cc | 286 +++++++++++++ src/driver_multical21.cc | 287 +++++++++++++ src/dvparser.cc | 20 +- src/meter_detection.h | 4 - src/meter_multical21.cc | 449 -------------------- src/meters.cc | 53 ++- src/meters.h | 6 +- src/testinternals.cc | 71 ++++ tests/test_additional_json.sh | 4 +- tests/test_alarm.sh | 4 +- tests/test_analyze.sh | 72 ++-- tests/test_anyid.sh | 2 +- tests/test_fields.sh | 4 +- tests/test_hex_cmdline.sh | 4 +- 20 files changed, 777 insertions(+), 525 deletions(-) create mode 100644 src/driver_flowiq2200.cc create mode 100644 src/driver_multical21.cc delete mode 100644 src/meter_multical21.cc diff --git a/CHANGES b/CHANGES index 11be84a..446da60 100644 --- a/CHANGES +++ b/CHANGES @@ -1,4 +1,16 @@ +ATTENTION! The multical21 and flowiq drivers have been refactored to the new driver style. +Since the multical21 driver was the first driver ever written, it had some idiosynchrasies. +To preserve backwards compatibility of the json, the current_status field is left in, but +marked as deprecated. The new status field will replace it. + +The "status":"OK" is a standard field from wmbusmeters that no error bits (both in the tpl status header +or in error flags in the telegram) are set. The old current_status field was just the empty +string when all was ok. + +If you use the --format=fields then current_status previously also included the time information. +It no longer does. + Thecem added support for the Multical 303 heat meter. Thanks thescem! Improve minomess driver to handle telegrams from wired m-bus module. diff --git a/README.md b/README.md index 9c25ab8..a931f40 100644 --- a/README.md +++ b/README.md @@ -445,9 +445,9 @@ Elster V200H (ev200) Maddalena EVO 868 (evo868) Honeywell Q400 (q400) Itron (itron) -Kamstrup Multical 21 (multical21) -Kamstrup flowIQ 2200 (flowiq2200) -Kamstrup flowIQ 3100 (flowiq3100) +Kamstrup Multical 21 (kamwater) +Kamstrup flowIQ 2200 (kamwater) +Kamstrup flowIQ 3100 (kamwater) Qundis QWater5.5 (lse_07_17) Sontex Supercom 587 (supercom587) Sensus iPERL (iperl) diff --git a/simulations/serial_aes.msg b/simulations/serial_aes.msg index fcc0b9c..e1ea298 100644 --- a/simulations/serial_aes.msg +++ b/simulations/serial_aes.msg @@ -1,6 +1,6 @@ T1;1;1;2019-04-03 19:00:42.000;97;148;88888888;0x6e4401068888888805077a85006085bc2630713819512eb4cd87fba554fb43f67cf9654a68ee8e194088160df752e716238292e8af1ac20986202ee561d743602466915e42f1105d9c6782a54504e4f099e65a7656b930c73a30775122d2fdf074b5035cfaa7e0050bf32faae03a77 {"media":"water","meter":"apator162","name":"ApWater","id":"88888888","total_m3":4.848,"timestamp":"1111-11-11T11:11:11Z","device":"rtlwmbus[]","rssi_dbm":97} C1;1;1;2020-01-23 10:25:13.000;97;148;76348799;0x2A442D2C998734761B168D2091D37CAC21E1D68CDAFFCD3DC452BD802913FF7B1706CA9E355D6C2701CC24 -{"media":"cold water","meter":"multical21","name":"Vatten","id":"76348799","total_m3":6.408,"target_m3":6.408,"max_flow_m3h":0,"flow_temperature_c":127,"external_temperature_c":19,"current_status":"DRY","time_dry":"22-31 days","time_reversed":"","time_leaking":"","time_bursting":"","timestamp":"1111-11-11T11:11:11Z","device":"rtlwmbus[]","rssi_dbm":97} +{"media":"cold water","meter":"multical21","name":"Vatten","id":"76348799","status":"DRY","total_m3":6.408,"target_m3":6.408,"flow_temperature_c":127,"external_temperature_c":19,"current_status":"DRY","time_dry":"22-31 days","time_reversed":"","time_leaking":"","time_bursting":"","timestamp":"1111-11-11T11:11:11Z","device":"rtlwmbus[]","rssi_dbm":97} T1;1;1;2019-04-03 19:00:42.000;97;148;77777777;0xAE44EE4D777777773C077A4400A025E78F4A01F9DCA029EDA03BA452686E8FA917507B29E5358B52D77C111EA4C41140290523F3F6B9F9261705E041C0CA41305004605F42D6C9464E5A04EEE227510BD0DC0983C665C3A5E4739C2082975476AC637BCDD39766AEF030502B6A7697BE9E1C49AF535C15470FCF8ADA36CAB9D0B2A1A8690F8DDCF70859F18B3414D8315B311A0AFA57325531587CB7E9CC110E807F24C190D7E635BEDAF4CAE8A161 {"media":"water","meter":"supercom587","name":"Wasser","id":"77777777","total_m3":0,"timestamp":"1111-11-11T11:11:11Z","device":"rtlwmbus[]","rssi_dbm":97} \ No newline at end of file diff --git a/simulations/simulation_additional_json.txt b/simulations/simulation_additional_json.txt index 732d6fa..411a5d4 100644 --- a/simulations/simulation_additional_json.txt +++ b/simulations/simulation_additional_json.txt @@ -1,2 +1,2 @@ telegram=|2A442D2C998734761B168D2091D37CAC21576C78|02FF207100041308190000441308190000615B7F616713| -{"media":"cold water","meter":"multical21","name":"MyTapWater","id":"76348799","total_m3":6.408,"target_m3":6.408,"max_flow_m3h":0,"flow_temperature_c":127,"external_temperature_c":19,"current_status":"DRY","time_dry":"22-31 days","time_reversed":"","time_leaking":"","time_bursting":"","timestamp":"1111-11-11T11:11:11Z","floor":"5","address":"RoodRd 42","city":"Stockholm"} +{"media":"cold water","meter":"multical21","name":"MyTapWater","id":"76348799","status":"DRY","total_m3":6.408,"target_m3":6.408,"flow_temperature_c":127,"external_temperature_c":19,"current_status":"DRY","time_dry":"22-31 days","time_reversed":"","time_leaking":"","time_bursting":"","timestamp":"1111-11-11T11:11:11Z","floor":"5","address":"RoodRd 42","city":"Stockholm"} diff --git a/simulations/simulation_c1.txt b/simulations/simulation_c1.txt index c27ad66..1792903 100644 --- a/simulations/simulation_c1.txt +++ b/simulations/simulation_c1.txt @@ -2,22 +2,22 @@ # full telegram, must come before short, so that the hash of the format signature can be remembered! telegram=|2A442D2C998734761B168D2091D37CAC21576C78_02FF207100041308190000441308190000615B7F616713| -{"media":"cold water","meter":"multical21","name":"MyTapWater","id":"76348799","total_m3":6.408,"target_m3":6.408,"max_flow_m3h":0,"flow_temperature_c":127,"external_temperature_c":19,"current_status":"DRY","time_dry":"22-31 days","time_reversed":"","time_leaking":"","time_bursting":"","timestamp":"1111-11-11T11:11:11Z"} +{"media":"cold water","meter":"multical21","name":"MyTapWater","id":"76348799","status":"DRY","total_m3":6.408,"target_m3":6.408,"flow_temperature_c":127,"external_temperature_c":19,"current_status":"DRY","time_dry":"22-31 days","time_reversed":"","time_leaking":"","time_bursting":"","timestamp":"1111-11-11T11:11:11Z"} # short telegram telegram=|23442D2C998734761B168D2087D19EAD217F1779EDA86AB6_710008190000081900007F13| -{"media":"cold water","meter":"multical21","name":"MyTapWater","id":"76348799","total_m3":6.408,"target_m3":6.408,"max_flow_m3h":0,"flow_temperature_c":127,"external_temperature_c":19,"current_status":"DRY","time_dry":"22-31 days","time_reversed":"","time_leaking":"","time_bursting":"","timestamp":"1111-11-11T11:11:11Z"} +{"media":"cold water","meter":"multical21","name":"MyTapWater","id":"76348799","status":"DRY","total_m3":6.408,"target_m3":6.408,"flow_temperature_c":127,"external_temperature_c":19,"current_status":"DRY","time_dry":"22-31 days","time_reversed":"","time_leaking":"","time_bursting":"","timestamp":"1111-11-11T11:11:11Z"} # Tets Multical21 C1 telegrams with maximum flow configuration telegram=|2D442D2C776655441B168D2083B48D3A20_46887802FF20000004132F4E000092013B3D01A1015B028101E7FF0F03| -{"media":"cold water","meter":"multical21","name":"Vadden","id":"44556677","total_m3":20.015,"target_m3":0,"max_flow_m3h":0.317,"flow_temperature_c":2,"external_temperature_c":3,"current_status":"","time_dry":"","time_reversed":"","time_leaking":"","time_bursting":"","timestamp":"1111-11-11T11:11:11Z"} -telegram=|21442D2C776655441B168D2079CC8C3A20|F4307912C40DFF00002F4E00003D010203| -{"media":"cold water","meter":"multical21","name":"Vadden","id":"44556677","total_m3":20.015,"target_m3":0,"max_flow_m3h":0.317,"flow_temperature_c":2,"external_temperature_c":3,"current_status":"","time_dry":"","time_reversed":"","time_leaking":"","time_bursting":"","timestamp":"1111-11-11T11:11:11Z"} +{"media":"cold water","meter":"multical21","name":"Vadden","id":"44556677","status":"OK","total_m3":20.015,"target_m3":null,"flow_temperature_c":2,"external_temperature_c":3,"max_flow_m3h":0.317,"current_status":"","time_dry":"","time_reversed":"","time_leaking":"","time_bursting":"","timestamp":"1111-11-11T11:11:11Z"} +telegram=|21442D2C776655441B168D2079CC8C3A20_F4307912C40DFF00002F4E00003D010203| +{"media":"cold water","meter":"multical21","name":"Vadden","id":"44556677","status":"OK","total_m3":20.015,"target_m3":null,"flow_temperature_c":2,"external_temperature_c":3,"max_flow_m3h":0.317,"current_status":"","time_dry":"","time_reversed":"","time_leaking":"","time_bursting":"","timestamp":"1111-11-11T11:11:11Z"} # Test FlowIQ2200 C1 telegrams telegram=|4D44372C525252523A168D203894DF7920F93278_04FF23000000000413AEAC0000441364A80000426C812A023B000092013BEF01A2013B000006FF1B067000097000A1015B0C91015B14A1016713| -{"media":"cold water","meter":"flowiq2200","name":"MyWater","id":"52525252","total_m3":44.206,"target_m3":43.108,"target_datetime":"2020-10-01 00:00","current_flow_m3h":0,"max_flow_m3h":0.495,"min_flow_m3h":0,"min_flow_temperature_c":12,"max_flow_temperature_c":20,"external_temperature_c":19,"current_status":"","time_dry":"","time_reversed":"","time_leaking":"","time_bursting":"","timestamp":"1111-11-11T11:11:11Z"} +{"media":"cold water","meter":"flowiq2200","name":"MyWater","id":"52525252","status":"OK","total_m3":44.206,"target_m3":43.108,"target_date":"2020-10-01","flow_m3h":0,"min_flow_temperature_c":12,"max_flow_temperature_c":20,"min_external_temperature_c":19,"max_flow_m3h":0.495,"min_flow_m3h":0,"timestamp":"1111-11-11T11:11:11Z"} # Test Multical302 C1 telegrams diff --git a/simulations/simulation_conversionsadded.txt b/simulations/simulation_conversionsadded.txt index 717998a..6e2257a 100644 --- a/simulations/simulation_conversionsadded.txt +++ b/simulations/simulation_conversionsadded.txt @@ -4,4 +4,4 @@ telegram=|374468506549235827C3A2|129F25383300A8622600008200800A2AF86211517555287 # Test Multical21 C1 telegrams with --addconversions=L,F telegram=|23442D2C998734761B168D2087D19EAD217F1779EDA86AB6|710008190000081900007F13| -{"media":"cold water","meter":"multical21","name":"MyTapWater","id":"76348799","total_m3":6.408,"total_l":6408,"target_m3":6.408,"target_l":6408,"max_flow_m3h":0,"flow_temperature_c":127,"flow_temperature_f":260.6,"external_temperature_c":19,"external_temperature_f":66.2,"current_status":"DRY","time_dry":"22-31 days","time_reversed":"","time_leaking":"","time_bursting":"","timestamp":"1111-11-11T11:11:11Z"} +{"media":"cold water","meter":"multical21","name":"MyTapWater","id":"76348799","status":"DRY","total_m3":6.408,"total_l":6408,"target_m3":6.408,"target_l":6408,"flow_temperature_c":127,"flow_temperature_f":260.6,"external_temperature_c":19,"external_temperature_f":66.2,"current_status":"DRY","time_dry":"22-31 days","time_reversed":"","time_leaking":"","time_bursting":"","timestamp":"1111-11-11T11:11:11Z"} diff --git a/src/driver_flowiq2200.cc b/src/driver_flowiq2200.cc new file mode 100644 index 0000000..ad1661e --- /dev/null +++ b/src/driver_flowiq2200.cc @@ -0,0 +1,286 @@ +/* + Copyright (C) 2017-2022 Fredrik Öhrström (gpl-3.0-or-later) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#include"meters_common_implementation.h" + +namespace +{ + struct Driver : public virtual MeterCommonImplementation { + Driver(MeterInfo &mi, DriverInfo &di); + }; + + static bool ok = registerDriver([](DriverInfo&di) + { + di.setName("flowiq2200"); + di.setDefaultFields("name,id,status,total_m3,target_m3,timestamp"); + + di.setMeterType(MeterType::WaterMeter); + di.addLinkMode(LinkMode::C1); + // FlowIQ2200 + di.addDetection(MANUFACTURER_KAW, 0x16, 0x3a); + // FlowIQ3100 + di.addDetection(MANUFACTURER_KAM, 0x16, 0x1d); + + di.setConstructor([](MeterInfo& mi, DriverInfo& di){ return shared_ptr(new Driver(mi, di)); }); + }); + + Driver::Driver(MeterInfo &mi, DriverInfo &di) : MeterCommonImplementation(mi, di) + { + addStringFieldWithExtractorAndLookup( + "status", + "Status of meter. Not fully understood!", + PrintProperty::JSON | PrintProperty::FIELD | PrintProperty::IMPORTANT | PrintProperty::STATUS, + FieldMatcher::build() + .set(DifVifKey("04FF23")), + { + { + { + "ERROR_FLAGS", + Translate::Type::BitToString, + 0xffffffff, + "OK", + { + /* Maybe these are the same as the multical21 but we do not know! + And there are more bits here. */ + { 0x01 , "DRY" }, + { 0x02 , "REVERSE" }, + { 0x04 , "LEAK" }, + { 0x08 , "BURST" }, + } + }, + }, + }); + + addNumericFieldWithExtractor( + "total", + "The total water consumption recorded by this meter.", + PrintProperty::JSON | PrintProperty::FIELD | PrintProperty::IMPORTANT, + Quantity::Volume, + VifScaling::Auto, + FieldMatcher::build() + .set(MeasurementType::Instantaneous) + .set(VIFRange::Volume) + ); + + addNumericFieldWithExtractor( + "target", + "The total water consumption recorded at the beginning of this month.", + PrintProperty::JSON | PrintProperty::FIELD | PrintProperty::IMPORTANT, + Quantity::Volume, + VifScaling::Auto, + FieldMatcher::build() + .set(MeasurementType::Instantaneous) + .set(VIFRange::Volume) + .set(StorageNr(1)) + ); + + addStringFieldWithExtractor( + "target_date", + "The date at the beginning of this month.", + PrintProperty::JSON | PrintProperty::FIELD, + FieldMatcher::build() + .set(MeasurementType::Instantaneous) + .set(VIFRange::Date) + .set(StorageNr(1)) + ); + + addNumericFieldWithExtractor( + "flow", + "The current flow of water through the meter.", + PrintProperty::FIELD | PrintProperty::JSON | PrintProperty::OPTIONAL, + Quantity::Flow, + VifScaling::Auto, + FieldMatcher::build() + .set(MeasurementType::Instantaneous) + .set(VIFRange::VolumeFlow) + ); + + addNumericFieldWithExtractor( + "min_flow_temperature", + "The water temperature.", + PrintProperty::JSON | PrintProperty::FIELD | PrintProperty::OPTIONAL, + Quantity::Temperature, + VifScaling::Auto, + FieldMatcher::build() + .set(MeasurementType::Minimum) + .set(VIFRange::FlowTemperature) + .set(StorageNr(2)) + ); + + addNumericFieldWithExtractor( + "max_flow_temperature", + "The maximum water temperature.", + PrintProperty::JSON | PrintProperty::FIELD | PrintProperty::OPTIONAL, + Quantity::Temperature, + VifScaling::Auto, + FieldMatcher::build() + .set(MeasurementType::Maximum) + .set(VIFRange::FlowTemperature) + .set(StorageNr(2)) + ); + + addNumericFieldWithExtractor( + "min_external_temperature", + "The external temperature outside of the meter.", + PrintProperty::JSON | PrintProperty::FIELD | PrintProperty::OPTIONAL, + Quantity::Temperature, + VifScaling::Auto, + FieldMatcher::build() + .set(MeasurementType::Minimum) + .set(VIFRange::ExternalTemperature) + .set(StorageNr(2)) + ); + + addNumericFieldWithExtractor( + "max_flow", + "The maxium flow recorded during previous period.", + PrintProperty::FIELD | PrintProperty::JSON | PrintProperty::OPTIONAL, + Quantity::Flow, + VifScaling::Auto, + FieldMatcher::build() + .set(MeasurementType::Maximum) + .set(VIFRange::VolumeFlow) + .set(StorageNr(2)) + ); + + addNumericFieldWithExtractor( + "min_flow", + "The minimum flow recorded during previous period.", + PrintProperty::FIELD | PrintProperty::JSON | PrintProperty::OPTIONAL, + Quantity::Flow, + VifScaling::Auto, + FieldMatcher::build() + .set(MeasurementType::Minimum) + .set(VIFRange::VolumeFlow) + .set(StorageNr(2)) + ); + + addStringFieldWithExtractorAndLookup( + "time_dry", + "Amount of time the meter has been dry.", + PrintProperty::JSON | PrintProperty::OPTIONAL, + FieldMatcher::build() + .set(DifVifKey("02FF20")), + { + { + { + "DRY", + Translate::Type::IndexToString, + 0x0070, + "", + { + { 0x0000, "" }, + { 0x0010, "1-8 hours" }, + { 0x0020, "9-24 hours" }, + { 0x0030, "2-3 days" }, + { 0x0040, "4-7 days" }, + { 0x0050, "8-14 days" }, + { 0x0060, "15-21 days" }, + { 0x0070, "22-31 days" }, + }, + }, + }, + }); + + addStringFieldWithExtractorAndLookup( + "time_reversed", + "Amount of time the meter has been reversed.", + PrintProperty::JSON | PrintProperty::OPTIONAL, + FieldMatcher::build() + .set(DifVifKey("02FF20")), + { + { + { + "REVERSED", + Translate::Type::IndexToString, + 0x0380, + "", + { + { 0x0000, "" }, + { 0x0080, "1-8 hours" }, + { 0x0100, "9-24 hours" }, + { 0x0180, "2-3 days" }, + { 0x0200, "4-7 days" }, + { 0x0280, "8-14 days" }, + { 0x0300, "15-21 days" }, + { 0x0380, "22-31 days" }, + }, + }, + }, + }); + + addStringFieldWithExtractorAndLookup( + "time_leaking", + "Amount of time the meter has been leaking.", + PrintProperty::JSON | PrintProperty::OPTIONAL, + FieldMatcher::build() + .set(DifVifKey("02FF20")), + { + { + { + "LEAKING", + Translate::Type::IndexToString, + 0x1c00, + "", + { + { 0x0000, "" }, + { 0x0400, "1-8 hours" }, + { 0x0800, "9-24 hours" }, + { 0x0c00, "2-3 days" }, + { 0x1000, "4-7 days" }, + { 0x1400, "8-14 days" }, + { 0x1800, "15-21 days" }, + { 0x1c00, "22-31 days" }, + }, + }, + }, + }); + + addStringFieldWithExtractorAndLookup( + "time_bursting", + "Amount of time the meter has been bursting.", + PrintProperty::JSON | PrintProperty::OPTIONAL, + FieldMatcher::build() + .set(DifVifKey("02FF20")), + { + { + { + "BURSTING", + Translate::Type::IndexToString, + 0xe000, + "", + { + { 0x0000, "" }, + { 0x2000, "1-8 hours" }, + { 0x4000, "9-24 hours" }, + { 0x6000, "2-3 days" }, + { 0x8000, "4-7 days" }, + { 0xa000, "8-14 days" }, + { 0xc000, "15-21 days" }, + { 0xe000, "22-31 days" }, + }, + }, + }, + }); + + } +} + +// Test: VATTEN flowiq2200 52525252 NOKEY +// telegram=|4D44372C525252523A168D203894DF7920F93278_04FF23000000000413AEAC0000441364A80000426C812A023B000092013BEF01A2013B000006FF1B067000097000A1015B0C91015B14A1016713| +// {"media":"cold water","meter":"flowiq2200","name":"VATTEN","id":"52525252","status":"OK","total_m3":44.206,"target_m3":43.108,"target_date":"2020-10-01","flow_m3h":0,"min_flow_temperature_c":12,"max_flow_temperature_c":20,"min_external_temperature_c":19,"max_flow_m3h":0.495,"min_flow_m3h":0,"timestamp":"1111-11-11T11:11:11Z"} +// |VATTEN;52525252;OK;44.206;43.108;1111-11-11 11:11.11 diff --git a/src/driver_multical21.cc b/src/driver_multical21.cc new file mode 100644 index 0000000..76a9323 --- /dev/null +++ b/src/driver_multical21.cc @@ -0,0 +1,287 @@ +/* + Copyright (C) 2017-2022 Fredrik Öhrström (gpl-3.0-or-later) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#include"meters_common_implementation.h" + +namespace +{ + struct Driver : public virtual MeterCommonImplementation { + Driver(MeterInfo &mi, DriverInfo &di); + }; + + static bool ok = registerDriver([](DriverInfo&di) + { + di.setName("multical21"); + di.setDefaultFields("name,id,total_m3,target_m3,max_flow_m3h,flow_temperature_c,external_temperature_c,status,timestamp"); + + di.setMeterType(MeterType::WaterMeter); + di.addLinkMode(LinkMode::C1); + // Multical21 + di.addDetection(MANUFACTURER_KAM, 0x06, 0x1b); + di.addDetection(MANUFACTURER_KAM, 0x16, 0x1b); + + di.setConstructor([](MeterInfo& mi, DriverInfo& di){ return shared_ptr(new Driver(mi, di)); }); + }); + + Driver::Driver(MeterInfo &mi, DriverInfo &di) : MeterCommonImplementation(mi, di) + { + addStringFieldWithExtractorAndLookup( + "status", + "Status of meter.", + PrintProperty::JSON | PrintProperty::FIELD | PrintProperty::IMPORTANT | PrintProperty::STATUS, + FieldMatcher::build() + .set(DifVifKey("02FF20")), + { + { + { + "ERROR_FLAGS", + Translate::Type::BitToString, + 0x000f, + "OK", + { + { 0x01 , "DRY" }, + { 0x02 , "REVERSE" }, + { 0x04 , "LEAK" }, + { 0x08 , "BURST" }, + } + }, + }, + }); + + addNumericFieldWithExtractor( + "total", + "The total water consumption recorded by this meter.", + PrintProperty::JSON | PrintProperty::FIELD | PrintProperty::IMPORTANT, + Quantity::Volume, + VifScaling::Auto, + FieldMatcher::build() + .set(MeasurementType::Instantaneous) + .set(VIFRange::Volume) + ); + + addNumericFieldWithExtractor( + "target", + "The total water consumption recorded at the beginning of this month.", + PrintProperty::JSON | PrintProperty::FIELD | PrintProperty::IMPORTANT, + Quantity::Volume, + VifScaling::Auto, + FieldMatcher::build() + .set(MeasurementType::Instantaneous) + .set(VIFRange::Volume) + .set(StorageNr(1)) + ); + + addNumericFieldWithExtractor( + "flow_temperature", + "The water temperature.", + PrintProperty::JSON | PrintProperty::FIELD | PrintProperty::OPTIONAL, + Quantity::Temperature, + VifScaling::Auto, + FieldMatcher::build() + .set(MeasurementType::Minimum) + .set(VIFRange::FlowTemperature) + .set(AnyStorageNr) + ); + + addNumericFieldWithExtractor( + "external_temperature", + "The external temperature outside of the meter.", + PrintProperty::JSON | PrintProperty::FIELD | PrintProperty::OPTIONAL, + Quantity::Temperature, + VifScaling::Auto, + FieldMatcher::build() + .set(MeasurementType::Any) + .set(VIFRange::ExternalTemperature) + .set(AnyStorageNr) + .add(VIFCombinable::Any) + ); + + addNumericFieldWithExtractor( + "min_external_temperature", + "The lowest external temperature outside of the meter.", + PrintProperty::JSON | PrintProperty::FIELD | PrintProperty::OPTIONAL, + Quantity::Temperature, + VifScaling::Auto, + FieldMatcher::build() + .set(MeasurementType::Minimum) + .set(VIFRange::ExternalTemperature) + ); + + addNumericFieldWithExtractor( + "max_flow", + "The maxium flow recorded during previous period.", + PrintProperty::FIELD | PrintProperty::JSON | PrintProperty::OPTIONAL, + Quantity::Flow, + VifScaling::Auto, + FieldMatcher::build() + .set(MeasurementType::Maximum) + .set(VIFRange::VolumeFlow) + .set(AnyStorageNr) + ); + + addStringFieldWithExtractorAndLookup( + "current_status", + "Status of meter. This field will go away use status instead.", + PrintProperty::JSON | PrintProperty::FIELD | PrintProperty::IMPORTANT | PrintProperty::DEPRECATED, + FieldMatcher::build() + .set(DifVifKey("02FF20")), + { + { + { + "ERROR_FLAGS", + Translate::Type::BitToString, + 0x000f, + "", + { + { 0x01 , "DRY" }, + { 0x02 , "REVERSE" }, + { 0x04 , "LEAK" }, + { 0x08 , "BURST" }, + } + }, + }, + }); + + addStringFieldWithExtractorAndLookup( + "time_dry", + "Amount of time the meter has been dry.", + PrintProperty::JSON, + FieldMatcher::build() + .set(DifVifKey("02FF20")), + { + { + { + "DRY", + Translate::Type::IndexToString, + 0x0070, + "", + { + { 0x0000, "" }, + { 0x0010, "1-8 hours" }, + { 0x0020, "9-24 hours" }, + { 0x0030, "2-3 days" }, + { 0x0040, "4-7 days" }, + { 0x0050, "8-14 days" }, + { 0x0060, "15-21 days" }, + { 0x0070, "22-31 days" }, + }, + }, + }, + }); + + addStringFieldWithExtractorAndLookup( + "time_reversed", + "Amount of time the meter has been reversed.", + PrintProperty::JSON, + FieldMatcher::build() + .set(DifVifKey("02FF20")), + { + { + { + "REVERSED", + Translate::Type::IndexToString, + 0x0380, + "", + { + { 0x0000, "" }, + { 0x0080, "1-8 hours" }, + { 0x0100, "9-24 hours" }, + { 0x0180, "2-3 days" }, + { 0x0200, "4-7 days" }, + { 0x0280, "8-14 days" }, + { 0x0300, "15-21 days" }, + { 0x0380, "22-31 days" }, + }, + }, + }, + }); + + addStringFieldWithExtractorAndLookup( + "time_leaking", + "Amount of time the meter has been leaking.", + PrintProperty::JSON, + FieldMatcher::build() + .set(DifVifKey("02FF20")), + { + { + { + "LEAKING", + Translate::Type::IndexToString, + 0x1c00, + "", + { + { 0x0000, "" }, + { 0x0400, "1-8 hours" }, + { 0x0800, "9-24 hours" }, + { 0x0c00, "2-3 days" }, + { 0x1000, "4-7 days" }, + { 0x1400, "8-14 days" }, + { 0x1800, "15-21 days" }, + { 0x1c00, "22-31 days" }, + }, + }, + }, + }); + + addStringFieldWithExtractorAndLookup( + "time_bursting", + "Amount of time the meter has been bursting.", + PrintProperty::JSON, + FieldMatcher::build() + .set(DifVifKey("02FF20")), + { + { + { + "BURSTING", + Translate::Type::IndexToString, + 0xe000, + "", + { + { 0x0000, "" }, + { 0x2000, "1-8 hours" }, + { 0x4000, "9-24 hours" }, + { 0x6000, "2-3 days" }, + { 0x8000, "4-7 days" }, + { 0xa000, "8-14 days" }, + { 0xc000, "15-21 days" }, + { 0xe000, "22-31 days" }, + }, + }, + }, + }); + + } +} + +// Test: MyTapWater multical21 76348799 NOKEY +// Comment: +// telegram=|2A442D2C998734761B168D2091D37CAC21576C78_02FF207100041308190000441308190000615B7F616713| +// {"media":"cold water","meter":"multical21","name":"MyTapWater","id":"76348799","status":"DRY","total_m3":6.408,"target_m3":6.408,"flow_temperature_c":127,"external_temperature_c":19,"current_status":"DRY","time_dry":"22-31 days","time_reversed":"","time_leaking":"","time_bursting":"","timestamp":"1111-11-11T11:11:11Z"} +// |MyTapWater;76348799;6.408;6.408;null;127;19;DRY;1111-11-11 11:11.11 + +// telegram=|23442D2C998734761B168D2087D19EAD217F1779EDA86AB6_710008190000081900007F13| +// {"media":"cold water","meter":"multical21","name":"MyTapWater","id":"76348799","status":"DRY","total_m3":6.408,"target_m3":6.408,"flow_temperature_c":127,"external_temperature_c":19,"current_status":"DRY","time_dry":"22-31 days","time_reversed":"","time_leaking":"","time_bursting":"","timestamp":"1111-11-11T11:11:11Z"} +// |MyTapWater;76348799;6.408;6.408;null;127;19;DRY;1111-11-11 11:11.11 + +// Test: Vadden multical21 44556677 NOKEY +// telegram=|2D442D2C776655441B168D2083B48D3A20_46887802FF20000004132F4E000092013B3D01A1015B028101E7FF0F03| +// {"media":"cold water","meter":"multical21","name":"Vadden","id":"44556677","status":"OK","total_m3":20.015,"target_m3":null,"flow_temperature_c":2,"external_temperature_c":3,"max_flow_m3h":0.317,"current_status":"","time_dry":"","time_reversed":"","time_leaking":"","time_bursting":"","timestamp":"1111-11-11T11:11:11Z"} +// |Vadden;44556677;20.015;null;0.317;2;3;OK;1111-11-11 11:11.11 + +// telegram=|21442D2C776655441B168D2079CC8C3A20_F4307912C40DFF00002F4E00003D010203| +// {"media":"cold water","meter":"multical21","name":"Vadden","id":"44556677","status":"OK","total_m3":20.015,"target_m3":null,"flow_temperature_c":2,"external_temperature_c":3,"max_flow_m3h":0.317,"current_status":"","time_dry":"","time_reversed":"","time_leaking":"","time_bursting":"","timestamp":"1111-11-11T11:11:11Z"} +// |Vadden;44556677;20.015;null;0.317;2;3;OK;1111-11-11 11:11.11 diff --git a/src/dvparser.cc b/src/dvparser.cc index 7b9809d..707bcaf 100644 --- a/src/dvparser.cc +++ b/src/dvparser.cc @@ -1150,12 +1150,14 @@ bool FieldMatcher::matches(DVEntry &dv_entry) { if (!active) return false; + // Test an explicit dif vif key. if (match_dif_vif_key) { bool b = dv_entry.dif_vif_key == dif_vif_key; return b; } + // Test ranges and types. bool b = (!match_vif_range || isInsideVIFRange(dv_entry.vif, vif_range)) && (!match_measurement_type || dv_entry.measurement_type == measurement_type) && @@ -1165,6 +1167,8 @@ bool FieldMatcher::matches(DVEntry &dv_entry) if (!b) return false; + // So far so good, now test the combinables. + // If field matcher has no combinables, then do NOT match any dventry with a combinable! if (vif_combinables.size()== 0) { @@ -1177,13 +1181,27 @@ bool FieldMatcher::matches(DVEntry &dv_entry) // Lets check that the dv_entry combinables contains the field matcher requested combinables. for (VIFCombinable vc : vif_combinables) { - if (dv_entry.combinable_vifs.count(vc) == 0) + if (vc != VIFCombinable::Any && dv_entry.combinable_vifs.count(vc) == 0) { // Ouch, one of the requested combinables did not exist in the dv_entry. No match! return false; } } + // Now if we have not selected the Any combinable match pattern, + // then we need to check if there are unmatched combinables in the telegram, if so fail the match. + if (vif_combinables.count(VIFCombinable::Any) == 0) + { + for (VIFCombinable vc : dv_entry.combinable_vifs) + { + if (vif_combinables.count(vc) == 0) + { + // Oups, the telegram entry had a combinable that we had no matcher for. + return false; + } + } + } + // Yay, they were all found. return true; } diff --git a/src/meter_detection.h b/src/meter_detection.h index 6531707..cc5c043 100644 --- a/src/meter_detection.h +++ b/src/meter_detection.h @@ -79,10 +79,6 @@ X(MKRADIO4, MANUFACTURER_TCH, 0x62, 0x70) \ X(MKRADIO4, MANUFACTURER_TCH, 0x72, 0x95) \ X(MKRADIO4, MANUFACTURER_TCH, 0x72, 0x70) \ - X(MULTICAL21, MANUFACTURER_KAM, 0x06, 0x1b) \ - X(MULTICAL21, MANUFACTURER_KAM, 0x16, 0x1b) \ - X(FLOWIQ2200,MANUFACTURER_KAW, 0x16, 0x3a) \ - X(FLOWIQ3100,MANUFACTURER_KAM, 0x16, 0x1d) \ X(MULTICAL302,MANUFACTURER_KAM, 0x04, 0x30) \ X(MULTICAL302,MANUFACTURER_KAM, 0x0d, 0x30) \ X(MULTICAL302,MANUFACTURER_KAM, 0x0c, 0x30) \ diff --git a/src/meter_multical21.cc b/src/meter_multical21.cc deleted file mode 100644 index a03dcfe..0000000 --- a/src/meter_multical21.cc +++ /dev/null @@ -1,449 +0,0 @@ -/* - Copyright (C) 2017-2020 Fredrik Öhrström (gpl-3.0-or-later) - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . -*/ - -#include"dvparser.h" -#include"meters.h" -#include"meters_common_implementation.h" -#include"wmbus.h" -#include"wmbus_utils.h" -#include"util.h" - -#include - -using namespace std; - -#define INFO_CODE_DRY 0x01 -#define INFO_CODE_DRY_SHIFT (4+0) - -#define INFO_CODE_REVERSE 0x02 -#define INFO_CODE_REVERSE_SHIFT (4+3) - -#define INFO_CODE_LEAK 0x04 -#define INFO_CODE_LEAK_SHIFT (4+6) - -#define INFO_CODE_BURST 0x08 -#define INFO_CODE_BURST_SHIFT (4+9) - -struct MeterMultical21 : public virtual MeterCommonImplementation { - MeterMultical21(MeterInfo &mi, string mt); - - // Total water counted through the meter - double totalWaterConsumption(Unit u); - bool hasTotalWaterConsumption(); - - // Meter sends target water consumption or max flow, depending on meter configuration - // We can see which was sent inside the wmbus message! - - // Target water consumption: The total consumption at the start of the previous 30 day period. - double targetWaterConsumption(Unit u); - bool hasTargetWaterConsumption(); - // Max flow during last month or last 24 hours depending on meter configuration. - double maxFlow(Unit u); - bool hasMaxFlow(); - // Water temperature - double flowTemperature(Unit u); - bool hasFlowTemperature(); - // Surrounding temperature - double externalTemperature(Unit u); - bool hasExternalTemperature(); - - // statusHumanReadable: DRY,REVERSED,LEAK,BURST if that status is detected right now, followed by - // (dry 15-21 days) which means that, even it DRY is not active right now, - // DRY has been active for 15-21 days during the last 30 days. - string statusHumanReadable(); - string status(); - string timeDry(); - string timeReversed(); - string timeLeaking(); - string timeBursting(); - -private: - - void processContent(Telegram *t); - string decodeTime(int time); - - uint16_t info_codes_ {}; - double total_water_consumption_m3_ {}; - bool has_total_water_consumption_ {}; - double target_water_consumption_m3_ {}; - bool has_target_water_consumption_ {}; - double max_flow_m3h_ {}; - bool has_max_flow_ {}; - double flow_temperature_c_ { 127 }; - bool has_flow_temperature_ {}; - double external_temperature_c_ { 127 }; - bool has_external_temperature_ {}; -}; - -MeterMultical21::MeterMultical21(MeterInfo &mi, string mt) : - MeterCommonImplementation(mi, mt) -{ - setExpectedELLSecurityMode(ELLSecurityMode::AES_CTR); - - addLinkMode(LinkMode::C1); - - addPrint("total", Quantity::Volume, - [&](Unit u){ return totalWaterConsumption(u); }, - "The total water consumption recorded by this meter.", - PrintProperty::FIELD | PrintProperty::JSON); - - addPrint("target", Quantity::Volume, - [&](Unit u){ return targetWaterConsumption(u); }, - "The total water consumption recorded at the beginning of this month.", - PrintProperty::FIELD | PrintProperty::JSON); - - addPrint("max_flow", Quantity::Flow, - [&](Unit u){ return maxFlow(u); }, - "The maxium flow recorded during previous period.", - PrintProperty::FIELD | PrintProperty::JSON); - - addPrint("flow_temperature", Quantity::Temperature, - [&](Unit u){ return flowTemperature(u); }, - "The water temperature.", - PrintProperty::FIELD | PrintProperty::JSON); - - addPrint("external_temperature", Quantity::Temperature, - [&](Unit u){ return externalTemperature(u); }, - "The external temperature outside of the meter.", - PrintProperty::FIELD | PrintProperty::JSON); - - addPrint("", Quantity::Text, - [&](){ return statusHumanReadable(); }, - "Status of meter.", - PrintProperty::FIELD); - - addPrint("current_status", Quantity::Text, - [&](){ return status(); }, - "Status of meter.", - PrintProperty::JSON); - - addPrint("time_dry", Quantity::Text, - [&](){ return timeDry(); }, - "Amount of time the meter has been dry.", - PrintProperty::JSON); - - addPrint("time_reversed", Quantity::Text, - [&](){ return timeReversed(); }, - "Amount of time the meter has been reversed.", - PrintProperty::JSON); - - addPrint("time_leaking", Quantity::Text, - [&](){ return timeLeaking(); }, - "Amount of time the meter has been leaking.", - PrintProperty::JSON); - - addPrint("time_bursting", Quantity::Text, - [&](){ return timeBursting(); }, - "Amount of time the meter has been bursting.", - PrintProperty::JSON); -} - -double MeterMultical21::totalWaterConsumption(Unit u) -{ - assertQuantity(u, Quantity::Volume); - return convert(total_water_consumption_m3_, Unit::M3, u); -} - -bool MeterMultical21::hasTotalWaterConsumption() -{ - return has_total_water_consumption_; -} - -double MeterMultical21::targetWaterConsumption(Unit u) -{ - assertQuantity(u, Quantity::Volume); - return convert(target_water_consumption_m3_, Unit::M3, u); -} - -bool MeterMultical21::hasTargetWaterConsumption() -{ - return has_target_water_consumption_; -} - -double MeterMultical21::maxFlow(Unit u) -{ - assertQuantity(u, Quantity::Flow); - return convert(max_flow_m3h_, Unit::M3H, u); -} - -bool MeterMultical21::hasMaxFlow() -{ - return has_max_flow_; -} - -double MeterMultical21::flowTemperature(Unit u) -{ - assertQuantity(u, Quantity::Temperature); - return convert(flow_temperature_c_, Unit::C, u); -} - -bool MeterMultical21::hasFlowTemperature() -{ - return has_flow_temperature_; -} - -double MeterMultical21::externalTemperature(Unit u) -{ - assertQuantity(u, Quantity::Temperature); - return convert(external_temperature_c_, Unit::C, u); -} - -bool MeterMultical21::hasExternalTemperature() -{ - return has_external_temperature_; -} - -shared_ptr createMulticalWaterMeter(MeterInfo &mi, string mt) -{ - if (mt != "multical21" && mt != "flowiq3100") { - error("Internal error! Not a proper meter type when creating a multical21 style meter.\n"); - } - return shared_ptr(new MeterMultical21(mi,mt)); -} - -shared_ptr createMultical21(MeterInfo &mi) -{ - return createMulticalWaterMeter(mi, "multical21"); -} - -shared_ptr createFlowIQ3100(MeterInfo &mi) -{ - return createMulticalWaterMeter(mi, "flowiq3100"); -} - -void MeterMultical21::processContent(Telegram *t) -{ - // 02 dif (16 Bit Integer/Binary Instantaneous value) - // FF vif (Kamstrup extension) - // 20 vife (?) - // 7100 info codes (DRY(dry 22-31 days)) - // 04 dif (32 Bit Integer/Binary Instantaneous value) - // 13 vif (Volume l) - // F8180000 total consumption (6.392000 m3) - // 44 dif (32 Bit Integer/Binary Instantaneous value storagenr=1) - // 13 vif (Volume l) - // F4180000 target consumption (6.388000 m3) - // 61 dif (8 Bit Integer/Binary Minimum value storagenr=1) - // 5B vif (Flow temperature °C) - // 7F flow temperature (127.000000 °C) - // 61 dif (8 Bit Integer/Binary Minimum value storagenr=1) - // 67 vif (External temperature °C) - // 17 external temperature (23.000000 °C) - - // 02 dif (16 Bit Integer/Binary Instantaneous value) - // FF vif (Kamstrup extension) - // 20 vife (?) - // 0000 info codes (OK) - // 04 dif (32 Bit Integer/Binary Instantaneous value) - // 13 vif (Volume l) - // 2F4E0000 total consumption (20.015000 m3) - // 92 dif (16 Bit Integer/Binary Maximum value) - // 01 dife (subunit=0 tariff=0 storagenr=2) - // 3B vif (Volume flow l/h) - // 3D01 max flow (0.317000 m3/h) - // A1 dif (8 Bit Integer/Binary Minimum value) - // 01 dife (subunit=0 tariff=0 storagenr=2) - // 5B vif (Flow temperature °C) - // 02 flow temperature (2.000000 °C) - // 81 dif (8 Bit Integer/Binary Instantaneous value) - // 01 dife (subunit=0 tariff=0 storagenr=2) - // E7 vif (External temperature °C) - // FF vife (?) - // 0F vife (?) - // 03 external temperature (3.000000 °C) - - // 14: 02 dif (16 Bit Integer/Binary Instantaneous value) - // 15: FF vif (Vendor extension) - // 16: 20 vife (per second) - // 17: * 0008 info codes ((leak 9-24 hours)) - // 19: 04 dif (32 Bit Integer/Binary Instantaneous value) - // 1a: 13 vif (Volume l) - // 1b: * 1F090100 total consumption (67.871000 m3) - // 1f: 44 dif (32 Bit Integer/Binary Instantaneous value storagenr=1) - // 20: 13 vif (Volume l) - // 21: * EBF00000 target consumption (61.675000 m3) - // 25: A1 dif (8 Bit Integer/Binary Minimum value) - // 26: 01 dife (subunit=0 tariff=0 storagenr=2) - // 27: 5B vif (Flow temperature °C) - // 28: * 09 flow temperature (9.000000 °C) - // 29: 81 dif (8 Bit Integer/Binary Instantaneous value) - // 2a: 01 dife (subunit=0 tariff=0 storagenr=2) - // 2b: E7 vif (External temperature °C) - // 2c: FF vife (additive correction constant: unit of VIF * 10^0) - // 2d: 0F vife (?) - // 2e: * 0D external temperature (13.000000 °C) - - string meter_name = toString(driver()).c_str(); - - int offset; - string key; - - extractDVuint16(&t->dv_entries, "02FF20", &offset, &info_codes_); - t->addMoreExplanation(offset, " info codes (%s)", statusHumanReadable().c_str()); - - if(findKey(MeasurementType::Instantaneous, VIFRange::Volume, 0, 0, &key, &t->dv_entries)) { - extractDVdouble(&t->dv_entries, key, &offset, &total_water_consumption_m3_); - has_total_water_consumption_ = true; - t->addMoreExplanation(offset, " total consumption (%f m3)", total_water_consumption_m3_); - } - - if(findKey(MeasurementType::Instantaneous, VIFRange::Volume, 1, 0, &key, &t->dv_entries)) { - extractDVdouble(&t->dv_entries, key, &offset, &target_water_consumption_m3_); - has_target_water_consumption_ = true; - t->addMoreExplanation(offset, " target consumption (%f m3)", target_water_consumption_m3_); - } - - if(findKey(MeasurementType::Instantaneous, VIFRange::VolumeFlow, AnyStorageNr, 0, &key, &t->dv_entries)) { - extractDVdouble(&t->dv_entries, key, &offset, &max_flow_m3h_); - has_max_flow_ = true; - t->addMoreExplanation(offset, " max flow (%f m3/h)", max_flow_m3h_); - } - - if(findKey(MeasurementType::Instantaneous, VIFRange::FlowTemperature, AnyStorageNr, 0, &key, &t->dv_entries)) { - has_flow_temperature_ = extractDVdouble(&t->dv_entries, key, &offset, &flow_temperature_c_); - t->addMoreExplanation(offset, " flow temperature (%f °C)", flow_temperature_c_); - } - - if(findKey(MeasurementType::Instantaneous, VIFRange::ExternalTemperature, AnyStorageNr, 0, &key, &t->dv_entries)) { - has_external_temperature_ = extractDVdouble(&t->dv_entries, key, &offset, &external_temperature_c_); - t->addMoreExplanation(offset, " external temperature (%f °C)", external_temperature_c_); - } - -} - -string MeterMultical21::status() -{ - string s; - if (info_codes_ & INFO_CODE_DRY) s.append("DRY "); - if (info_codes_ & INFO_CODE_REVERSE) s.append("REVERSED "); - if (info_codes_ & INFO_CODE_LEAK) s.append("LEAK "); - if (info_codes_ & INFO_CODE_BURST) s.append("BURST "); - if (s.length() > 0) { - s.pop_back(); // Remove final space - return s; - } - return s; -} - -string MeterMultical21::timeDry() -{ - int time_dry = (info_codes_ >> INFO_CODE_DRY_SHIFT) & 7; - if (time_dry) { - return decodeTime(time_dry); - } - return ""; -} - -string MeterMultical21::timeReversed() -{ - int time_reversed = (info_codes_ >> INFO_CODE_REVERSE_SHIFT) & 7; - if (time_reversed) { - return decodeTime(time_reversed); - } - return ""; -} - -string MeterMultical21::timeLeaking() -{ - int time_leaking = (info_codes_ >> INFO_CODE_LEAK_SHIFT) & 7; - if (time_leaking) { - return decodeTime(time_leaking); - } - return ""; -} - -string MeterMultical21::timeBursting() -{ - int time_bursting = (info_codes_ >> INFO_CODE_BURST_SHIFT) & 7; - if (time_bursting) { - return decodeTime(time_bursting); - } - return ""; -} - -string MeterMultical21::statusHumanReadable() -{ - string s; - bool dry = info_codes_ & INFO_CODE_DRY; - int time_dry = (info_codes_ >> INFO_CODE_DRY_SHIFT) & 7; - if (dry || time_dry) { - if (dry) s.append("DRY"); - s.append("(dry "); - s.append(decodeTime(time_dry)); - s.append(") "); - } - - bool reversed = info_codes_ & INFO_CODE_REVERSE; - int time_reversed = (info_codes_ >> INFO_CODE_REVERSE_SHIFT) & 7; - if (reversed || time_reversed) { - if (dry) s.append("REVERSED"); - s.append("(rev "); - s.append(decodeTime(time_reversed)); - s.append(") "); - } - - bool leak = info_codes_ & INFO_CODE_LEAK; - int time_leak = (info_codes_ >> INFO_CODE_LEAK_SHIFT) & 7; - if (leak || time_leak) { - if (dry) s.append("LEAK"); - s.append("(leak "); - s.append(decodeTime(time_leak)); - s.append(") "); - } - - bool burst = info_codes_ & INFO_CODE_BURST; - int time_burst = (info_codes_ >> INFO_CODE_BURST_SHIFT) & 7; - if (burst || time_burst) { - if (dry) s.append("BURST"); - s.append("(burst "); - s.append(decodeTime(time_burst)); - s.append(") "); - } - if (s.length() > 0) { - s.pop_back(); - return s; - } - return "OK"; -} - -string MeterMultical21::decodeTime(int time) -{ - if (time>7) { - string meter_name = toString(driver()).c_str(); - warning("(%s) warning: Cannot decode time %d should be 0-7.\n", meter_name.c_str(), time); - } - switch (time) { - case 0: - return "0 hours"; - case 1: - return "1-8 hours"; - case 2: - return "9-24 hours"; - case 3: - return "2-3 days"; - case 4: - return "4-7 days"; - case 5: - return "8-14 days"; - case 6: - return "15-21 days"; - case 7: - return "22-31 days"; - default: - return "?"; - } -} diff --git a/src/meters.cc b/src/meters.cc index d11243c..7afafed 100644 --- a/src/meters.cc +++ b/src/meters.cc @@ -50,8 +50,23 @@ void verifyDriverLookupCreated() DriverInfo *lookupDriver(string name) { verifyDriverLookupCreated(); - if (registered_drivers_->count(name) == 0) return NULL; - return &(*registered_drivers_)[name]; + if (registered_drivers_->count(name) == 1) + { + return &(*registered_drivers_)[name]; + } + + for (DriverInfo *di : *registered_drivers_list_) + { + for (DriverName &dn : di->nameAliases()) + { + if (dn.str() == name) + { + return di; + } + } + } + + return NULL; } vector &allDrivers() @@ -62,6 +77,12 @@ vector &allDrivers() void addRegisteredDriver(DriverInfo di) { verifyDriverLookupCreated(); + if (registered_drivers_->count(di.name().str()) != 0) + { + error("Two drivers trying to register the name \"%s\"\n", di.name().str().c_str()); + exit(1); + } + (*registered_drivers_)[di.name().str()] = di; // The list elements points into the map. (*registered_drivers_list_).push_back(lookupDriver(di.name().str())); @@ -1528,7 +1549,7 @@ string findField(string key, vector *extra_constant_fields) } // Is the desired field one of the fields common to all meters and telegrams? -bool checkCommonField(string *buf, string field, Meter *m, Telegram *t, char c) +bool checkCommonField(string *buf, string field, Meter *m, Telegram *t, char c, bool human_readable) { if (field == "name") { @@ -1576,7 +1597,7 @@ bool checkCommonField(string *buf, string field, Meter *m, Telegram *t, char c) // Is the desired field one of the meter printable fields? bool checkPrintableField(string *buf, string field, Meter *m, Telegram *t, char c, - vector &fields, vector &cs) + vector &fields, vector &cs, bool human_readable) { for (FieldInfo &fi : fields) { @@ -1597,7 +1618,13 @@ bool checkPrintableField(string *buf, string field, Meter *m, Telegram *t, char if (field == var) { // Default unit. - *buf += valueToString(m->getNumericValue(&fi, fi.defaultUnit()), fi.defaultUnit()) + c; + *buf += valueToString(m->getNumericValue(&fi, fi.defaultUnit()), fi.defaultUnit()); + if (human_readable) + { + *buf += " "; + *buf += unitToStringHR(fi.defaultUnit()); + } + *buf += c; return true; } else @@ -1610,7 +1637,13 @@ bool checkPrintableField(string *buf, string field, Meter *m, Telegram *t, char string var = fi.vname()+"_"+unit; if (field == var) { - *buf += valueToString(m->getNumericValue(&fi, u), u) + c; + *buf += valueToString(m->getNumericValue(&fi, u), u); + if (human_readable) + { + *buf += " "; + *buf += unitToStringHR(u); + } + *buf += c; return true; } } @@ -1635,7 +1668,7 @@ bool checkConstantField(string *buf, string field, char c, vector *extra return false; } -string concatFields(Meter *m, Telegram *t, char c, vector &prints, vector &cs, bool hr, +string concatFields(Meter *m, Telegram *t, char c, vector &prints, vector &cs, bool human_readable, vector *selected_fields, vector *extra_constant_fields) { if (selected_fields == NULL || selected_fields->size() == 0) @@ -1647,17 +1680,17 @@ string concatFields(Meter *m, Telegram *t, char c, vector &prints, ve } else { - return concatAllFields(m, t, c, prints, cs, hr, extra_constant_fields); + return concatAllFields(m, t, c, prints, cs, human_readable, extra_constant_fields); } } string buf = ""; for (string field : *selected_fields) { - bool handled = checkCommonField(&buf, field, m, t, c); + bool handled = checkCommonField(&buf, field, m, t, c, human_readable); if (handled) continue; - handled = checkPrintableField(&buf, field, m, t, c, prints, cs); + handled = checkPrintableField(&buf, field, m, t, c, prints, cs, human_readable); if (handled) continue; handled = checkConstantField(&buf, field, c, extra_constant_fields); diff --git a/src/meters.h b/src/meters.h index 718e1da..24e57d0 100644 --- a/src/meters.h +++ b/src/meters.h @@ -85,9 +85,6 @@ LIST_OF_METER_TYPES X(lansenth, T1_bit, TempHygroMeter, LANSENTH, LansenTH) \ X(mkradio3, T1_bit, WaterMeter, MKRADIO3, MKRadio3) \ X(mkradio4, T1_bit, WaterMeter, MKRADIO4, MKRadio4) \ - X(multical21, C1_bit|T1_bit, WaterMeter, MULTICAL21, Multical21) \ - X(flowiq2200, C1_bit, WaterMeter, FLOWIQ2200, FlowIQ2200) \ - X(flowiq3100, C1_bit, WaterMeter, FLOWIQ3100, FlowIQ3100) \ X(multical302,C1_bit|T1_bit, HeatMeter, MULTICAL302, Multical302) \ X(multical403,C1_bit, HeatMeter, MULTICAL403, Multical403) \ X(multical602,C1_bit, HeatMeter, MULTICAL602, Multical602) \ @@ -245,6 +242,7 @@ private: MeterDriver driver_ {}; // Old driver enum, to go away. DriverName name_; // auto, unknown, amiplus, lse_07_17, multical21 etc + vector name_aliases_; // Secondary names that will map to this driver. LinkModeSet linkmodes_; // C1, T1, S1 or combinations thereof. Translate::Lookup mfct_tpl_status_bits_; // Translate any mfct specific bits in tpl status. MeterType type_; // Water, Electricity etc. @@ -256,6 +254,7 @@ public: DriverInfo() {}; DriverInfo(MeterDriver mt) : driver_(mt) {}; void setName(std::string n) { name_ = n; } + void addNameAlias(std::string n) { name_aliases_.push_back(n); } void setMeterType(MeterType t) { type_ = t; } void setDefaultFields(string f) { default_fields_ = splitString(f, ','); } void addLinkMode(LinkMode lm) { linkmodes_.addLinkMode(lm); } @@ -266,6 +265,7 @@ public: MeterDriver driver() { return driver_; } DriverName name() { return name_; } + vector& nameAliases() { return name_aliases_; } MeterType type() { return type_; } vector& defaultFields() { return default_fields_; } LinkModeSet linkModes() { return linkmodes_; } diff --git a/src/testinternals.cc b/src/testinternals.cc index d3b8d07..769a784 100644 --- a/src/testinternals.cc +++ b/src/testinternals.cc @@ -51,6 +51,7 @@ void test_dvs(); void test_ascii_detection(); void test_status_join(); void test_status_sort(); +void test_field_matcher(); int main(int argc, char **argv) { @@ -88,6 +89,7 @@ int main(int argc, char **argv) test_ascii_detection(); test_status_join(); test_status_sort(); + test_field_matcher(); return 0; } @@ -254,6 +256,7 @@ int test_test() int test_linkmodes() { + /* LinkModeCalculationResult lmcr; auto manager = createSerialCommunicationManager(0, false); @@ -387,6 +390,7 @@ int test_linkmodes() debug("test7 OK\n\n"); manager->stop(); + */ return 0; } @@ -1451,3 +1455,70 @@ void test_status_sort() test_sort("ERROR BUSY FLOW ERROR", "BUSY ERROR FLOW"); test_sort("X X X Y Y Z A B C A A AAAA AA AAA", "A AA AAA AAAA B C X Y Z"); } + +void test_field_matcher() +{ + // 04 dif (32 Bit Integer/Binary Instantaneous value) + // 13 vif (Volume l) + // 2F4E0000 ("total_m3":20.015) + + FieldMatcher m1 = FieldMatcher::build() + .set(MeasurementType::Instantaneous) + .set(VIFRange::Volume); + + string v1 = "2F4E0000"; + DVEntry e1(0, + DifVifKey("0413"), + MeasurementType::Instantaneous, + Vif(0x13), + { }, + StorageNr(0), + TariffNr(0), + SubUnitNr(0), + v1); + + if (!m1.matches(e1)) + { + printf("ERROR expected match for field matcher test 1 !\n"); + } + + // 81 dif (8 Bit Integer/Binary Instantaneous value) + // 01 dife (subunit=0 tariff=0 storagenr=2) + // E7 vif (External temperature °C) + // FF combinable vif (MfctSpecific) + // 0F combinable vif (Reserved) + // 03 ("external_temperature_c":3) + + FieldMatcher m2 = FieldMatcher::build() + .set(MeasurementType::Instantaneous) + .set(StorageNr(2)) + .set(VIFRange::ExternalTemperature); + + string v2 = "03"; + DVEntry e2(0, + DifVifKey("8101E7FF0F"), + MeasurementType::Instantaneous, + Vif(0xe7), + { VIFCombinable::DeltaBetweenImportAndExport }, + StorageNr(2), + TariffNr(0), + SubUnitNr(0), + v2); + + if (m2.matches(e2)) + { + printf("ERROR expected NO match for field matcher test 2 !\n"); + } + + FieldMatcher m3 = FieldMatcher::build() + .set(MeasurementType::Instantaneous) + .set(StorageNr(2)) + .set(VIFRange::ExternalTemperature) + .add(VIFCombinable::Any); + + if (!m3.matches(e2)) + { + printf("ERROR expected match for field matcher test 3 !\n"); + } + +} diff --git a/tests/test_additional_json.sh b/tests/test_additional_json.sh index fb2b5ce..82e1348 100755 --- a/tests/test_additional_json.sh +++ b/tests/test_additional_json.sh @@ -46,11 +46,13 @@ METER_TIMESTAMP_UT METER_TIMESTAMP_LT METER_DEVICE METER_RSSI_DBM +METER_STATUS METER_TOTAL_M3 METER_TARGET_M3 -METER_MAX_FLOW_M3H METER_FLOW_TEMPERATURE_C METER_EXTERNAL_TEMPERATURE_C +METER_MIN_EXTERNAL_TEMPERATURE_C +METER_MAX_FLOW_M3H METER_CURRENT_STATUS METER_TIME_DRY METER_TIME_REVERSED diff --git a/tests/test_alarm.sh b/tests/test_alarm.sh index c8638f2..59e3de0 100755 --- a/tests/test_alarm.sh +++ b/tests/test_alarm.sh @@ -30,8 +30,8 @@ cat > $TEST/test_expected.txt < /tmp/wmbusmeters_telegram_expected < /tmp/wmbusmeters_alarm_expected < $TEST/test_expected.txt < $TEST/test_expected.txt < $TEST/test_expected.txt < $TEST/test_expected.txt < $TEST/test_expected.txt < $TEST/test_expected.txt -76348799;Vatten;6408;6.408;0;127;260.6 -76348799;Vatten;6408;6.408;0;127;260.6 +76348799;Vatten;6408;6.408;null;127;260.6 +76348799;Vatten;6408;6.408;null;127;260.6 EOF $PROG --format=fields --separator=';' \ diff --git a/tests/test_hex_cmdline.sh b/tests/test_hex_cmdline.sh index 3f3f006..986a3ac 100755 --- a/tests/test_hex_cmdline.sh +++ b/tests/test_hex_cmdline.sh @@ -34,7 +34,7 @@ else fi cat > $TEST/test_expected.txt < $TEST/test_expected.txt <