From 14ad435d027d4f421fd50c4a437009a7427d9400 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20=C3=96hrstr=C3=B6m?= Date: Sat, 5 Dec 2020 12:01:33 +0100 Subject: [PATCH] Added support for multical803. --- CHANGES | 2 + Makefile | 1 + README.md | 1 + simulations/simulation_c1.txt | 6 +- src/meter_multical603.cc | 13 +- src/meter_multical803.cc | 255 +++++++++++++++++++++++++ src/meters.h | 2 + tests/config1/etc/wmbusmeters.d/Heater | 5 + tests/test_c1_meters.sh | 1 + 9 files changed, 278 insertions(+), 8 deletions(-) create mode 100644 src/meter_multical803.cc create mode 100644 tests/config1/etc/wmbusmeters.d/Heater diff --git a/CHANGES b/CHANGES index 8a86d9a..76ee61f 100644 --- a/CHANGES +++ b/CHANGES @@ -1,4 +1,6 @@ +Nikodem added support for Multical803! Thanks Nikodem! + Added a warning to be printed if an rtl_sdr dongle is found when using auto and either rtl_sdr or rtl_wmbus cannot be found in the path at startup. diff --git a/Makefile b/Makefile index 8ae7334..43a8f15 100644 --- a/Makefile +++ b/Makefile @@ -147,6 +147,7 @@ METER_OBJS:=\ $(BUILD)/meter_multical302.o \ $(BUILD)/meter_multical403.o \ $(BUILD)/meter_multical603.o \ + $(BUILD)/meter_multical803.o \ $(BUILD)/meter_omnipower.o \ $(BUILD)/meter_q400.o \ $(BUILD)/meter_qcaloric.o \ diff --git a/README.md b/README.md index b6fb148..87aa354 100644 --- a/README.md +++ b/README.md @@ -294,6 +294,7 @@ Heat meter Techem Vario 4 (vario451) (non-standard protocol) Heat meter Kamstrup Multical 302 (multical302) (in C1 mode, please open issue for T1 mode) Heat and Cooling meter Kamstrup Multical 403 (multical403) (in C1 mode) Heat and Cooling meter Kamstrup Multical 603 (multical603) (in C1 mode) +Heat and Cooling meter Kamstrup Multical 803 (multical803) (in C1 mode) Supported room sensors: Bmeters RFM-AMB Thermometer/Hygrometer (rfmamb) diff --git a/simulations/simulation_c1.txt b/simulations/simulation_c1.txt index 345b6cc..2c29cf8 100644 --- a/simulations/simulation_c1.txt +++ b/simulations/simulation_c1.txt @@ -51,4 +51,8 @@ telegram=|5B442D2C02017878340A8D2096809C1320EF2B7934147ED7|2D0A0000FAFF000043180 # Test Multical603 C1 telegram telegram=|42442D2C3636363635048D20E18025B62087D078|0406A500000004FF072B01000004FF089C000000041421020000043B120000000259D014025D000904FF2200000000| -{"media":"heat","meter":"multical603","name":"Heat","id":"36363636","total_energy_consumption_kwh":165,"total_volume_m3":5.45,"volume_flow_m3h":0.018,"t1_temperature_c":53.28,"t2_temperature_c":23.04,"at_date":"","current_status":"","energy_forward_kwh":"299","energy_returned_kwh":"156","timestamp":"1111-11-11T11:11:11Z"} +{"media":"heat","meter":"multical603","name":"Heat","id":"36363636","total_energy_consumption_kwh":165,"total_volume_m3":5.45,"volume_flow_m3h":0.018,"t1_temperature_c":53.28,"t2_temperature_c":23.04,"at_date":"","current_status":"","energy_forward_kwh":299,"energy_returned_kwh":156,"timestamp":"1111-11-11T11:11:11Z"} + +# Test Multical803 C1 telegram +telegram=|88442D2C8180808039048D20864051322084C178|040F0000000004FF070000000004FF0800000000041400000000844014000000008480401400000000043B0000000002590000025D0000142D0000000084100F0000000084200F0000000004FF2260000100026C892B440F00000000441400000000C4401400000000C480401400000000426C812B| +{"media":"heat","meter":"multical803","name":"Heater","id":"80808081","total_energy_consumption_kwh":0,"total_volume_m3":0,"volume_flow_m3h":0,"t1_temperature_c":0,"t2_temperature_c":0,"at_date":"2020-11-09 00:00","current_status":"SENSOR_T1_BELOW_MEASURING_RANGE SENSOR_T2_BELOW_MEASURING_RANGE","energy_forward_kwh":0,"energy_returned_kwh":0,"timestamp":"1111-11-11T11:11:11Z"} diff --git a/src/meter_multical603.cc b/src/meter_multical603.cc index 5bbab5f..3eda18d 100644 --- a/src/meter_multical603.cc +++ b/src/meter_multical603.cc @@ -105,16 +105,15 @@ MeterMultical603::MeterMultical603(MeterInfo &mi) : "Status of meter.", true, true); - addPrint("energy_forward_kwh", Quantity::Text, - [&](){ return to_string(energy_forward_kwh_); }, + addPrint("energy_forward", Quantity::Energy, + [&](Unit u){ assertQuantity(u, Quantity::Energy); return convert(energy_forward_kwh_, Unit::KWH, u); }, "Energy forward.", false, true); - addPrint("energy_returned_kwh", Quantity::Text, - [&](){ return to_string(energy_returned_kwh_); }, + addPrint("energy_returned", Quantity::Energy, + [&](Unit u){ assertQuantity(u, Quantity::Energy); return convert(energy_returned_kwh_, Unit::KWH, u); }, "Energy returned.", false, true); - } shared_ptr createMultical603(MeterInfo &mi) { @@ -200,10 +199,10 @@ void MeterMultical603::processContent(Telegram *t) t->addMoreExplanation(offset, " info codes (%s)", status().c_str()); extractDVuint32(&t->values, "04FF07", &offset, &energy_forward_kwh_); - t->addMoreExplanation(offset, " something A (%zu)", energy_forward_kwh_); + t->addMoreExplanation(offset, " energy forward kwh (%zu)", energy_forward_kwh_); extractDVuint32(&t->values, "04FF08", &offset, &energy_returned_kwh_); - t->addMoreExplanation(offset, " something B (%zu)", energy_returned_kwh_); + t->addMoreExplanation(offset, " energy returned kwh (%zu)", energy_returned_kwh_); if(findKey(MeasurementType::Instantaneous, ValueInformation::EnergyWh, 0, 0, &key, &t->values)) { extractDVdouble(&t->values, key, &offset, &total_energy_kwh_); diff --git a/src/meter_multical803.cc b/src/meter_multical803.cc new file mode 100644 index 0000000..d82fb9a --- /dev/null +++ b/src/meter_multical803.cc @@ -0,0 +1,255 @@ +/* + Copyright (C) 2018-2020 Fredrik Öhrström + 2020 Eric Bus + 2020 Nikodem Jędrzejczak + + 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"dvparser.h" +#include"wmbus.h" +#include"wmbus_utils.h" +#include"util.h" + +#define INFO_CODE_VOLTAGE_INTERRUPTED 1 +#define INFO_CODE_LOW_BATTERY_LEVEL 2 +#define INFO_CODE_EXTERNAL_ALARM 4 +#define INFO_CODE_SENSOR_T1_ABOVE_MEASURING_RANGE 8 +#define INFO_CODE_SENSOR_T2_ABOVE_MEASURING_RANGE 16 +#define INFO_CODE_SENSOR_T1_BELOW_MEASURING_RANGE 32 +#define INFO_CODE_SENSOR_T2_BELOW_MEASURING_RANGE 64 +#define INFO_CODE_TEMP_DIFF_WRONG_POLARITY 128 + +struct MeterMultical803 : public virtual HeatMeter, public virtual MeterCommonImplementation { + MeterMultical803(MeterInfo &mi); + + double totalEnergyConsumption(Unit u); + string status(); + double totalVolume(Unit u); + double volumeFlow(Unit u); + + // Water temperatures + double t1Temperature(Unit u); + bool hasT1Temperature(); + double t2Temperature(Unit u); + bool hasT2Temperature(); + +private: + void processContent(Telegram *t); + + uchar info_codes_ {}; + double total_energy_gj_ {}; + double total_volume_m3_ {}; + double volume_flow_m3h_ {}; + double t1_temperature_c_ { 127 }; + bool has_t1_temperature_ {}; + double t2_temperature_c_ { 127 }; + bool has_t2_temperature_ {}; + string target_date_ {}; + + uint32_t energy_forward_gj_ {}; + uint32_t energy_returned_gj_ {}; +}; + +MeterMultical803::MeterMultical803(MeterInfo &mi) : + MeterCommonImplementation(mi, MeterType::MULTICAL803) +{ + setExpectedELLSecurityMode(ELLSecurityMode::AES_CTR); + + addLinkMode(LinkMode::C1); + + addPrint("total_energy_consumption", Quantity::Energy, + [&](Unit u){ return totalEnergyConsumption(u); }, + "The total energy consumption recorded by this meter.", + true, true); + + addPrint("total_volume", Quantity::Volume, + [&](Unit u){ return totalVolume(u); }, + "Total volume of media.", + true, true); + + addPrint("volume_flow", Quantity::Flow, + [&](Unit u){ return volumeFlow(u); }, + "The current flow.", + true, true); + + addPrint("t1_temperature", Quantity::Temperature, + [&](Unit u){ return t1Temperature(u); }, + "The T1 temperature.", + true, true); + + addPrint("t2_temperature", Quantity::Temperature, + [&](Unit u){ return t2Temperature(u); }, + "The T2 temperature.", + true, true); + + addPrint("at_date", Quantity::Text, + [&](){ return target_date_; }, + "Date when total energy consumption was recorded.", + false, true); + + addPrint("current_status", Quantity::Text, + [&](){ return status(); }, + "Status of meter.", + true, true); + + addPrint("energy_forward", Quantity::Energy, + [&](Unit u){ assertQuantity(u, Quantity::Energy); return convert(energy_forward_gj_, Unit::GJ, u); }, + "Energy forward.", + false, true); + + addPrint("energy_returned", Quantity::Energy, + [&](Unit u){ assertQuantity(u, Quantity::Energy); return convert(energy_returned_gj_, Unit::GJ, u); }, + "Energy returned.", + false, true); +} + +shared_ptr createMultical803(MeterInfo &mi) { + return shared_ptr(new MeterMultical803(mi)); +} + +double MeterMultical803::totalEnergyConsumption(Unit u) +{ + assertQuantity(u, Quantity::Energy); + return convert(total_energy_gj_, Unit::KWH, u); +} + +double MeterMultical803::totalVolume(Unit u) +{ + assertQuantity(u, Quantity::Volume); + return convert(total_volume_m3_, Unit::M3, u); +} + +double MeterMultical803::t1Temperature(Unit u) +{ + assertQuantity(u, Quantity::Temperature); + return convert(t1_temperature_c_, Unit::C, u); +} + +bool MeterMultical803::hasT1Temperature() +{ + return has_t1_temperature_; +} + +double MeterMultical803::t2Temperature(Unit u) +{ + assertQuantity(u, Quantity::Temperature); + return convert(t2_temperature_c_, Unit::C, u); +} + +bool MeterMultical803::hasT2Temperature() +{ + return has_t2_temperature_; +} + +double MeterMultical803::volumeFlow(Unit u) +{ + assertQuantity(u, Quantity::Flow); + return convert(volume_flow_m3h_, Unit::M3H, u); +} + +void MeterMultical803::processContent(Telegram *t) +{ + /* + (multical803) 13: 78 tpl-ci-field (EN 13757-3 Application Layer (no tplh)) + (multical803) 14: 04 dif (32 Bit Integer/Binary Instantaneous value) + (multical803) 15: 06 vif (Energy kWh) + (multical803) 16: * A5000000 total energy consumption (165.000000 kWh) + (multical803) 1a: 04 dif (32 Bit Integer/Binary Instantaneous value) + (multical803) 1b: FF vif (Vendor extension) + (multical803) 1c: 07 vife (?) + (multical803) 1d: 2B010000 + (multical803) 21: 04 dif (32 Bit Integer/Binary Instantaneous value) + (multical803) 22: FF vif (Vendor extension) + (multical803) 23: 08 vife (?) + (multical803) 24: 9C000000 + (multical803) 28: 04 dif (32 Bit Integer/Binary Instantaneous value) + (multical803) 29: 14 vif (Volume 10⁻² m³) + (multical803) 2a: * 21020000 total volume (5.450000 m3) + (multical803) 2e: 04 dif (32 Bit Integer/Binary Instantaneous value) + (multical803) 2f: 3B vif (Volume flow l/h) + (multical803) 30: * 12000000 volume flow (0.018000 m3/h) + (multical803) 34: 02 dif (16 Bit Integer/Binary Instantaneous value) + (multical803) 35: 59 vif (Flow temperature 10⁻² °C) + (multical803) 36: * D014 T1 flow temperature (53.280000 °C) + (multical803) 38: 02 dif (16 Bit Integer/Binary Instantaneous value) + (multical803) 39: 5D vif (Return temperature 10⁻² °C) + (multical803) 3a: * 0009 T2 flow temperature (23.040000 °C) + (multical803) 3c: 04 dif (32 Bit Integer/Binary Instantaneous value) + (multical803) 3d: FF vif (Vendor extension) + (multical803) 3e: 22 vife (per hour) + (multical803) 3f: * 00000000 info codes () +*/ + int offset; + string key; + + extractDVuint8(&t->values, "04FF22", &offset, &info_codes_); + t->addMoreExplanation(offset, " info codes (%s)", status().c_str()); + + extractDVuint32(&t->values, "04FF07", &offset, &energy_forward_gj_); + t->addMoreExplanation(offset, " energy forward gj (%zu)", energy_forward_gj_); + + extractDVuint32(&t->values, "04FF08", &offset, &energy_returned_gj_); + t->addMoreExplanation(offset, " energy returned gj (%zu)", energy_returned_gj_); + + if(findKey(MeasurementType::Instantaneous, ValueInformation::EnergyWh, 0, 0, &key, &t->values)) { + extractDVdouble(&t->values, key, &offset, &total_energy_gj_); + t->addMoreExplanation(offset, " total energy consumption (%f kWh)", total_energy_gj_); + } + + if(findKey(MeasurementType::Instantaneous, ValueInformation::Volume, 0, 0, &key, &t->values)) { + extractDVdouble(&t->values, key, &offset, &total_volume_m3_); + t->addMoreExplanation(offset, " total volume (%f m3)", total_volume_m3_); + } + + if(findKey(MeasurementType::Unknown, ValueInformation::VolumeFlow, 0, 0, &key, &t->values)) { + extractDVdouble(&t->values, key, &offset, &volume_flow_m3h_); + t->addMoreExplanation(offset, " volume flow (%f m3/h)", volume_flow_m3h_); + } + + if(findKey(MeasurementType::Instantaneous, ValueInformation::FlowTemperature, 0, 0, &key, &t->values)) { + has_t1_temperature_ = extractDVdouble(&t->values, key, &offset, &t1_temperature_c_); + t->addMoreExplanation(offset, " T1 flow temperature (%f °C)", t1_temperature_c_); + } + + if(findKey(MeasurementType::Instantaneous, ValueInformation::ReturnTemperature, 0, 0, &key, &t->values)) { + has_t2_temperature_ = extractDVdouble(&t->values, key, &offset, &t2_temperature_c_); + t->addMoreExplanation(offset, " T2 flow temperature (%f °C)", t2_temperature_c_); + } + + if (findKey(MeasurementType::Unknown, ValueInformation::Date, 0, 0, &key, &t->values)) { + struct tm datetime; + extractDVdate(&t->values, key, &offset, &datetime); + target_date_ = strdatetime(&datetime); + t->addMoreExplanation(offset, " target date (%s)", target_date_.c_str()); + } +} + +string MeterMultical803::status() +{ + string s; + if (info_codes_ & INFO_CODE_VOLTAGE_INTERRUPTED) s.append("VOLTAGE_INTERRUPTED "); + if (info_codes_ & INFO_CODE_LOW_BATTERY_LEVEL) s.append("LOW_BATTERY_LEVEL "); + if (info_codes_ & INFO_CODE_EXTERNAL_ALARM) s.append("EXTERNAL_ALARM "); + if (info_codes_ & INFO_CODE_SENSOR_T1_ABOVE_MEASURING_RANGE) s.append("SENSOR_T1_ABOVE_MEASURING_RANGE "); + if (info_codes_ & INFO_CODE_SENSOR_T2_ABOVE_MEASURING_RANGE) s.append("SENSOR_T2_ABOVE_MEASURING_RANGE "); + if (info_codes_ & INFO_CODE_SENSOR_T1_BELOW_MEASURING_RANGE) s.append("SENSOR_T1_BELOW_MEASURING_RANGE "); + if (info_codes_ & INFO_CODE_SENSOR_T2_BELOW_MEASURING_RANGE) s.append("SENSOR_T2_BELOW_MEASURING_RANGE "); + if (info_codes_ & INFO_CODE_TEMP_DIFF_WRONG_POLARITY) s.append("TEMP_DIFF_WRONG_POLARITY "); + if (s.length() > 0) { + s.pop_back(); // Remove final space + return s; + } + return s; +} diff --git a/src/meters.h b/src/meters.h index 8fd95cb..71792aa 100644 --- a/src/meters.h +++ b/src/meters.h @@ -57,6 +57,7 @@ X(multical302,C1_bit, HeatMeter, MULTICAL302, Multical302) \ X(multical403,C1_bit, HeatMeter, MULTICAL403, Multical403) \ X(multical603,C1_bit, HeatMeter, MULTICAL603, Multical603) \ + X(multical803,C1_bit, HeatMeter, MULTICAL803, Multical803) \ X(omnipower, C1_bit, ElectricityMeter, OMNIPOWER, Omnipower) \ X(rfmamb, T1_bit, TempHygroMeter, RFMAMB, RfmAmb) \ X(rfmtx1, T1_bit, WaterMeter, RFMTX1, RfmTX1) \ @@ -137,6 +138,7 @@ X(MULTICAL403,MANUFACTURER_KAM, 0x0c, 0x34) \ X(MULTICAL403,MANUFACTURER_KAM, 0x0d, 0x34) \ X(MULTICAL603,MANUFACTURER_KAM, 0x04, 0x35) \ + X(MULTICAL803,MANUFACTURER_KAM, 0x04, 0x39) \ X(OMNIPOWER, MANUFACTURER_KAM, 0x02, 0x01) \ X(RFMAMB, MANUFACTURER_BMT, 0x1b, 0x10) \ X(RFMTX1, MANUFACTURER_BMT, 0x07, 0x05) \ diff --git a/tests/config1/etc/wmbusmeters.d/Heater b/tests/config1/etc/wmbusmeters.d/Heater new file mode 100644 index 0000000..7269b80 --- /dev/null +++ b/tests/config1/etc/wmbusmeters.d/Heater @@ -0,0 +1,5 @@ +name=Heater +type=multical803 +id=80808081 +#key=testing comment +key= \ No newline at end of file diff --git a/tests/test_c1_meters.sh b/tests/test_c1_meters.sh index 8266015..eeeb075 100755 --- a/tests/test_c1_meters.sh +++ b/tests/test_c1_meters.sh @@ -19,6 +19,7 @@ $PROG --format=json simulations/simulation_c1.txt \ Rum cma12w 66666666 "" \ My403Cooling multical403 78780102 "" \ Heat multical603 36363636 "" \ + Heater multical803 80808081 "" \ > $TEST/test_output.txt 2> $TEST/test_stderr.txt if [ "$?" = "0" ]