From 302b08a478ebca1b6d83dd162b25c41c3601cf2d Mon Sep 17 00:00:00 2001 From: Jacek Tomasiak Date: Tue, 29 Oct 2019 21:26:01 +0100 Subject: [PATCH] Add Izar water meter support --- Makefile | 1 + README.md | 1 + simulations/simulation_t1.txt | 4 + src/cmdline.cc | 2 +- src/config.cc | 2 +- src/meter_izar.cc | 163 ++++++++++++++++++++++++++++++++++ src/meters.h | 2 + tests/test_t1_meters.sh | 1 + 8 files changed, 174 insertions(+), 2 deletions(-) create mode 100644 src/meter_izar.cc diff --git a/Makefile b/Makefile index b3305fd..6cd20f3 100644 --- a/Makefile +++ b/Makefile @@ -104,6 +104,7 @@ METER_OBJS:=\ $(BUILD)/meter_vario451.o \ $(BUILD)/meter_lansenth.o \ $(BUILD)/meter_rfmamb.o \ + $(BUILD)/meter_izar.o \ $(BUILD)/printer.o \ $(BUILD)/serial.o \ $(BUILD)/shell.o \ diff --git a/README.md b/README.md index 8b041f9..379289a 100644 --- a/README.md +++ b/README.md @@ -174,6 +174,7 @@ Tauron Amiplus (amiplus) (includes vendor apator and echelon) Work in progress: Heat meter Kamstrup Multical 302 (multical302) Electricity meter Kamstrup Omnipower (omnipower) +Water meter Diehl/Sappel IZAR RC 868 I R4 PL (izar) ``` The wmbus dongles imst871a can listen to one type of wmbus telegrams diff --git a/simulations/simulation_t1.txt b/simulations/simulation_t1.txt index af92e5c..72813ca 100644 --- a/simulations/simulation_t1.txt +++ b/simulations/simulation_t1.txt @@ -58,3 +58,7 @@ telegram=|2e44333003020100071b7a634820252f2f0265840842658308820165950802fb1aae01 telegram=|5744b40988227711101b7ab20800000265a00842658f088201659f08226589081265a0086265510852652b0902fb1aba0142fb1ab0018201fb1abd0122fb1aa90112fb1aba0162fb1aa60152fb1af501066d3b3bb36b2a00| {"media":"room sensor","meter":"rfmamb","name":"Rummet","id":"11772288","current_temperature_c":22.08,"average_temperature_1h_c":21.91,"average_temperature_24h_c":22.07,"maximum_temperature_1h_c":22.08,"minimum_temperature_1h_c":21.85,"maximum_temperature_24h_c":23.47,"minimum_temperature_24h_c":21.29,"current_relative_humidity_rh":44.2,"average_relative_humidity_1h_rh":43.2,"average_relative_humidity_24h_rh":44.5,"minimum_relative_humidity_1h_rh":42.2,"maximum_relative_humidity_1h_rh":50.1,"maximum_relative_humidity_24h_rh":0,"minimum_relative_humidity_24h_rh":0,"device_date_time":"2019-10-11 19:59","timestamp":"1111-11-11T11:11:11Z"} + +# Test IZAR RC 868 I R4 PL water meter telegram +telegram=|1944304C72242421D401A2|013D4013DD8B46A4999C1293E582CC| +{"media":"oil","meter":"izar","name":"IzarWater","id":"21242472","total_m3":3.488,"timestamp":"1111-11-11T11:11:11Z"} diff --git a/src/cmdline.cc b/src/cmdline.cc index 71e208a..29c48b1 100644 --- a/src/cmdline.cc +++ b/src/cmdline.cc @@ -359,7 +359,7 @@ unique_ptr parseCommandLine(int argc, char **argv) { mt = toMeterType(type); if (mt == MeterType::UNKNOWN) error("Not a valid meter type \"%s\"\n", type.c_str()); - if (!isValidMatchExpressions(id, false)) error("Not a valid id nor a valid meter match expression \"%s\"\n", id.c_str()); + if (!isValidMatchExpressions(id, true)) error("Not a valid id nor a valid meter match expression \"%s\"\n", id.c_str()); if (!isValidKey(key)) error("Not a valid meter key \"%s\"\n", key.c_str()); vector no_meter_shells, no_meter_jsons; c->meters.push_back(MeterInfo(name, type, id, key, modes, no_meter_shells, no_meter_jsons)); diff --git a/src/config.cc b/src/config.cc index 0727ea7..f7cb8f6 100644 --- a/src/config.cc +++ b/src/config.cc @@ -124,7 +124,7 @@ void parseMeterConfig(Configuration *c, vector &buf, string file) warning("Not a valid meter type \"%s\"\n", type.c_str()); use = false; } - if (!isValidMatchExpressions(id, false)) { + if (!isValidMatchExpressions(id, true)) { warning("Not a valid meter id nor a valid meter match expression \"%s\"\n", id.c_str()); use = false; } diff --git a/src/meter_izar.cc b/src/meter_izar.cc new file mode 100644 index 0000000..67e98c7 --- /dev/null +++ b/src/meter_izar.cc @@ -0,0 +1,163 @@ +/* + Copyright (C) 2019 Jacek Tomasiak + + 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.h" +#include"meters_common_implementation.h" +#include"wmbus.h" +#include"wmbus_utils.h" + +#include + +using namespace std; + +#define PRIOS_DEFAULT_KEY1 "39BC8A10E66D83F8" +#define PRIOS_DEFAULT_KEY2 "51728910E66D83F8" + +struct MeterIzar : public virtual WaterMeter, public virtual MeterCommonImplementation { + MeterIzar(WMBus *bus, MeterInfo &mi); + + // Total water counted through the meter + double totalWaterConsumption(Unit u); + bool hasTotalWaterConsumption(); + +private: + + void processContent(Telegram *t); + uint32_t convertKey(const char *hex); + uint32_t uint32FromBytes(const vector &data, int offset, bool reverse = false); + vector decodePrios(const vector &payload, uint32_t key); + + double total_water_consumption_l_ {}; +}; + +unique_ptr createIzar(WMBus *bus, MeterInfo &mi) +{ + return unique_ptr(new MeterIzar(bus, mi)); +} + +MeterIzar::MeterIzar(WMBus *bus, MeterInfo &mi) : + MeterCommonImplementation(bus, mi, MeterType::IZAR, MANUFACTURER_SAP) +{ + addMedia(0x01); // Oil meter? why? + + addLinkMode(LinkMode::T1); + + // meters with different versions exist, don't set any to avoid warnings + // setExpectedVersion(0xd4); // or 0xcc + + addPrint("total", Quantity::Volume, + [&](Unit u){ return totalWaterConsumption(u); }, + "The total water consumption recorded by this meter.", + true, true); + + MeterCommonImplementation::bus()->onTelegram(calll(this,handleTelegram,Telegram*)); +} + +double MeterIzar::totalWaterConsumption(Unit u) +{ + assertQuantity(u, Quantity::Volume); + return convert(total_water_consumption_l_, Unit::L, u); +} + +bool MeterIzar::hasTotalWaterConsumption() +{ + return true; +} + +uint32_t MeterIzar::uint32FromBytes(const vector &data, int offset, bool reverse) +{ + if (reverse) + return ((uint32_t)data[offset + 3] << 24) | + ((uint32_t)data[offset + 2] << 16) | + ((uint32_t)data[offset + 1] << 8) | + (uint32_t)data[offset]; + else + return ((uint32_t)data[offset] << 24) | + ((uint32_t)data[offset + 1] << 16) | + ((uint32_t)data[offset + 2] << 8) | + (uint32_t)data[offset + 3]; +} + +uint32_t MeterIzar::convertKey(const char *hex) +{ + vector bytes; + hex2bin(hex, &bytes); + uint32_t key1 = uint32FromBytes(bytes, 0); + uint32_t key2 = uint32FromBytes(bytes, 4); + uint32_t key = key1 ^ key2; + return key; +} + +void MeterIzar::processContent(Telegram *t) +{ + // recover full frame content + vector frame; + frame.insert(frame.end(), t->parsed.begin(), t->parsed.end()); + frame.insert(frame.end(), t->payload.begin(), t->payload.end()); + + vector keys; + keys.push_back(convertKey(PRIOS_DEFAULT_KEY1)); + keys.push_back(convertKey(PRIOS_DEFAULT_KEY2)); + + vector decoded_content; + for (auto& key : keys) { + decoded_content = decodePrios(frame, key); + if (!decoded_content.empty()) + break; + } + + debug("(izar) Decoded PRIOS data: %s\n", bin2hex(decoded_content).c_str()); + + if (decoded_content.empty()) + { + warning("(izar) Decoding PRIOS data failed. Ignoring telegram.\n"); + return; + } + + total_water_consumption_l_ = uint32FromBytes(decoded_content, 1, true); +} + +vector MeterIzar::decodePrios(const vector &frame, uint32_t key) +{ + // modify seed key with header values + key ^= uint32FromBytes(frame, 2); // manufacturer + address[0-1] + key ^= uint32FromBytes(frame, 6); // address[2-3] + version + type + key ^= uint32FromBytes(frame, 10); // ci + some more bytes from the telegram... + + int size = frame.size() - 15; + vector decoded(size); + + for (int i = 0; i < size; ++i) { + // calculate new key (LFSR) + // https://en.wikipedia.org/wiki/Linear-feedback_shift_register + for (int j = 0; j < 8; ++j) { + // calculate new bit value (xor of selected bits from previous key) + uchar bit = ((key & 0x2) != 0) ^ ((key & 0x4) != 0) ^ ((key & 0x800) != 0) ^ ((key & 0x80000000) != 0); + // shift key bits and add new one at the end + key = (key << 1) | bit; + } + // decode i-th content byte with fresh/last 8-bits of key + decoded[i] = frame[i + 15] ^ (key & 0xFF); + // check-byte doesn't match? + if (decoded[0] != 0x4B) { + decoded.clear(); + return decoded; + } + } + + return decoded; +} diff --git a/src/meters.h b/src/meters.h index 26e0648..a15ecae 100644 --- a/src/meters.h +++ b/src/meters.h @@ -31,6 +31,7 @@ X(eurisii, T1_bit, HeatCostAllocation, EURISII, EurisII) \ X(flowiq3100, C1_bit, Water, FLOWIQ3100, Multical21) \ X(iperl, T1_bit, Water, IPERL, Iperl) \ + X(izar, T1_bit, Water, IZAR, Izar) \ X(lansenth, T1_bit, TempHygro, LANSENTH, LansenTH) \ X(mkradio3, T1_bit, Water, MKRADIO3, MKRadio3) \ X(multical21, C1_bit, Water, MULTICAL21, Multical21) \ @@ -187,6 +188,7 @@ unique_ptr createSupercom587(WMBus *bus, MeterInfo &m); unique_ptr createMKRadio3(WMBus *bus, MeterInfo &m); unique_ptr createApator162(WMBus *bus, MeterInfo &m); unique_ptr createIperl(WMBus *bus, MeterInfo &m); +unique_ptr createIzar(WMBus *bus, MeterInfo &m); unique_ptr createQCaloric(WMBus *bus, MeterInfo &m); unique_ptr createEurisII(WMBus *bus, MeterInfo &m); unique_ptr createLansenTH(WMBus *bus, MeterInfo &m); diff --git a/tests/test_t1_meters.sh b/tests/test_t1_meters.sh index e6927ea..4e58821 100755 --- a/tests/test_t1_meters.sh +++ b/tests/test_t1_meters.sh @@ -21,6 +21,7 @@ $PROG --format=json simulations/simulation_t1.txt \ HeatMeter eurisii 88018801 "" \ Tempoo lansenth 00010203 "" \ Rummet rfmamb 11772288 "" \ + IzarWater izar 21242472 "" \ > $TEST/test_output.txt if [ "$?" == "0" ] then