diff --git a/Makefile b/Makefile index 83e84ee..e534745 100644 --- a/Makefile +++ b/Makefile @@ -66,6 +66,7 @@ METERS_OBJS:=\ $(BUILD)/meter_supercom587.o \ $(BUILD)/meter_iperl.o \ $(BUILD)/meter_qcaloric.o \ + $(BUILD)/meter_apator162.o \ $(BUILD)/printer.o \ $(BUILD)/serial.o \ $(BUILD)/shell.o \ @@ -148,6 +149,7 @@ update_manufacturers: -e 's/<\/tr>/)\\\n/g' | \ grep -v '' | tr -s ' ' | tr -s '\t' | tr '\t' '|' > tmpfile echo 'X(|QDS|QUNDIS GmbH)\' >> tmpfile + echo 'X(|APA|Apator Powogaz S.A)\' >> tmpfile cat tmpfile | sed -e "s/X(|\(.\)\(.\)\(.\)/X(\1\2\3|MANFCODE('\1','\2','\3')|/g" | \ tr -s '|' ',' >> m.h echo >> m.h @@ -156,4 +158,4 @@ update_manufacturers: echo >> m.h echo '#endif' >> m.h rm tmpfile - mv m.h manufacturers.h + mv m.h src/manufacturers.h diff --git a/README.md b/README.md index 0568df8..2bb9623 100644 --- a/README.md +++ b/README.md @@ -122,6 +122,7 @@ Supported heat cost allocator: Qundis Q caloric (qcaloric) Work in progress: +Water meter Apator wt-wmbus-16-2 (apator162) Heat meter Kamstrup Multical 302 (multical302) Electricity meter Kamstrup Omnipower (omnipower) ``` diff --git a/src/main.cc b/src/main.cc index 0adb4f6..dafc4c3 100644 --- a/src/main.cc +++ b/src/main.cc @@ -100,6 +100,7 @@ Supported heat cost allocator: Qundis Q caloric (qcaloric) Work in progress: +Water meter Apator at-wmbus-16-2 Heat meter Kamstrup Multical 302 (multical302) Electricity meter Kamstrup Omnipower (omnipower) )MANUAL"; @@ -270,6 +271,10 @@ void startUsingCommandline(Configuration *config) meters.push_back(createQCaloric(wmbus.get(), m.name, m.id, m.key)); verbose("(qcaloric) configured \"%s\" \"qcaloric\" \"%s\" %s\n", m.name.c_str(), m.id.c_str(), keymsg); break; + case APATOR162_METER: + meters.push_back(createApator162(wmbus.get(), m.name, m.id, m.key)); + verbose("(apator162) configured \"%s\" \"apator162\" \"%s\" %s\n", m.name.c_str(), m.id.c_str(), keymsg); + break; case UNKNOWN_METER: error("No such meter type \"%s\"\n", m.type.c_str()); break; diff --git a/src/manufacturers.h b/src/manufacturers.h index fbbf9ce..3d91d8d 100644 --- a/src/manufacturers.h +++ b/src/manufacturers.h @@ -1,5 +1,5 @@ // Data downloaded from http://www.m-bus.de/man.html -// 2019-02-17 +// 2019-03-15 #ifndef MANUFACTURERS_H #define MANUFACTURERS_H @@ -116,6 +116,7 @@ X(ZAG,MANFCODE('Z','A','G'),Zellwerg Uster AG)\ X(ZAP,MANFCODE('Z','A','P'),Zaptronix)\ X(ZIV,MANFCODE('Z','I','V'),ZIV Aplicaciones y Tecnologia S.A.)\ X(QDS,MANFCODE('Q','D','S'),QUNDIS GmbH)\ +X(APA,MANFCODE('A','P','A'),Apator Powogaz S.A)\ #define MANUFACTURER_ABB MANFCODE('A','B','B') #define MANUFACTURER_ACE MANFCODE('A','C','E') @@ -228,5 +229,6 @@ X(QDS,MANFCODE('Q','D','S'),QUNDIS GmbH)\ #define MANUFACTURER_ZAP MANFCODE('Z','A','P') #define MANUFACTURER_ZIV MANFCODE('Z','I','V') #define MANUFACTURER_QDS MANFCODE('Q','D','S') +#define MANUFACTURER_APA MANFCODE('A','P','A') #endif diff --git a/src/meter_apator162.cc b/src/meter_apator162.cc new file mode 100644 index 0000000..2150422 --- /dev/null +++ b/src/meter_apator162.cc @@ -0,0 +1,276 @@ +/* + Copyright (C) 2017-2019 Fredrik Öhrström + + 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 +#include +#include +#include +#include +#include +#include + +using namespace std; + +struct MeterApator162 : public virtual WaterMeter, public virtual MeterCommonImplementation { + MeterApator162(WMBus *bus, string& name, string& id, string& key); + + // Total water counted through the meter + double totalWaterConsumption(); + bool hasTotalWaterConsumption(); + double targetWaterConsumption(); + bool hasTargetWaterConsumption(); + double maxFlow(); + bool hasMaxFlow(); + double flowTemperature(); + bool hasFlowTemperature(); + double externalTemperature(); + bool hasExternalTemperature(); + + string statusHumanReadable(); + string status(); + string timeDry(); + string timeReversed(); + string timeLeaking(); + string timeBursting(); + + void printMeter(Telegram *t, + string *human_readable, + string *fields, char separator, + string *json, + vector *envs); + +private: + void handleTelegram(Telegram *t); + void processContent(Telegram *t); + string decodeTime(int time); + + double total_water_consumption_ {}; +}; + +MeterApator162::MeterApator162(WMBus *bus, string& name, string& id, string& key) : + MeterCommonImplementation(bus, name, id, key, APATOR162_METER, MANUFACTURER_APA, LinkMode::T1) +{ + addMedia(0x06); + addMedia(0x07); + MeterCommonImplementation::bus()->onTelegram(calll(this,handleTelegram,Telegram*)); +} + + +double MeterApator162::totalWaterConsumption() +{ + return total_water_consumption_; +} + +unique_ptr createApator162(WMBus *bus, string& name, string& id, string& key) +{ + return unique_ptr(new MeterApator162(bus,name,id,key)); +} + +void MeterApator162::handleTelegram(Telegram *t) +{ + if (!isTelegramForMe(t)) { + // This telegram is not intended for this meter. + return; + } + + verbose("(%s) telegram for %s %02x%02x%02x%02x\n", "apator162", + name().c_str(), + t->a_field_address[0], t->a_field_address[1], t->a_field_address[2], + t->a_field_address[3]); + + if (t->a_field_version != 0x05) { + warning("(%s) expected telegram from APA meter with version 0x%02x, " + "but got \"%s\" meter with version 0x%02x !\n", "apator162", + 0x05, + manufacturerFlag(t->m_field).c_str(), + t->a_field_version); + } + + if (useAes()) { + vector aeskey = key(); + decryptMode5_AES_CBC(t, aeskey); + } else { + t->content = t->payload; + } + char log_prefix[256]; + snprintf(log_prefix, 255, "(%s) log", "apator162"); + logTelegram(log_prefix, t->parsed, t->content); + int content_start = t->parsed.size(); + processContent(t); + if (isDebugEnabled()) { + snprintf(log_prefix, 255, "(%s)", "apator162"); + t->explainParse(log_prefix, content_start); + } + triggerUpdate(t); +} + +void MeterApator162::processContent(Telegram *t) +{ + // Meter record: + + map> values; + parseDV(t, t->content, t->content.begin(), t->content.size(), &values); + + int offset; + string key; + if(findKey(ValueInformation::Volume, 0, &key, &values)) { + extractDVdouble(&values, key, &offset, &total_water_consumption_); + t->addMoreExplanation(offset, " total consumption (%f m3)", total_water_consumption_); + } +} + +void MeterApator162::printMeter(Telegram *t, + string *human_readable, + string *fields, char separator, + string *json, + vector *envs) +{ + char buf[65536]; + buf[65535] = 0; + + snprintf(buf, sizeof(buf)-1, + "%s\t" + "%s\t" + "% 3.3f m3\t" + "%s", + name().c_str(), + t->id.c_str(), + totalWaterConsumption(), + datetimeOfUpdateHumanReadable().c_str()); + + *human_readable = buf; + + snprintf(buf, sizeof(buf)-1, + "%s%c" + "%s%c" + "%f%c" + "%s", + name().c_str(), separator, + t->id.c_str(), separator, + totalWaterConsumption(), separator, + datetimeOfUpdateRobot().c_str()); + + *fields = buf; + +#define Q(x,y) "\""#x"\":"#y"," +#define QS(x,y) "\""#x"\":\""#y"\"," +#define QSE(x,y) "\""#x"\":\""#y"\"" + + snprintf(buf, sizeof(buf)-1, "{" + QS(media,%s) + QS(meter,apator162) + QS(name,%s) + QS(id,%s) + Q(total_m3,%f) + QSE(timestamp,%s) + "}", + mediaType(manufacturer(), t->a_field_device_type).c_str(), + name().c_str(), + t->id.c_str(), + totalWaterConsumption(), + datetimeOfUpdateRobot().c_str()); + + *json = buf; + + envs->push_back(string("METER_JSON=")+*json); + envs->push_back(string("METER_TYPE=apator162")); + envs->push_back(string("METER_ID=")+t->id); + envs->push_back(string("METER_TOTAL_M3=")+to_string(totalWaterConsumption())); + envs->push_back(string("METER_TIMESTAMP=")+datetimeOfUpdateRobot()); +} + +bool MeterApator162::hasTotalWaterConsumption() +{ + return true; +} + +double MeterApator162::targetWaterConsumption() +{ + return 0.0; +} + +bool MeterApator162::hasTargetWaterConsumption() +{ + return false; +} + +double MeterApator162::maxFlow() +{ + return 0.0; +} + +bool MeterApator162::hasMaxFlow() +{ + return false; +} + +double MeterApator162::flowTemperature() +{ + return 127; +} + +bool MeterApator162::hasFlowTemperature() +{ + return false; +} + +double MeterApator162::externalTemperature() +{ + return 127; +} + +bool MeterApator162::hasExternalTemperature() +{ + return false; +} + +string MeterApator162::statusHumanReadable() +{ + return ""; +} + +string MeterApator162::status() +{ + return ""; +} + +string MeterApator162::timeDry() +{ + return ""; +} + +string MeterApator162::timeReversed() +{ + return ""; +} + +string MeterApator162::timeLeaking() +{ + return ""; +} + +string MeterApator162::timeBursting() +{ + return ""; +} diff --git a/src/meters.cc b/src/meters.cc index abc09d4..f96a07c 100644 --- a/src/meters.cc +++ b/src/meters.cc @@ -111,6 +111,7 @@ MeterType toMeterType(string& type) if (type == "supercom587") return SUPERCOM587_METER; if (type == "iperl") return IPERL_METER; if (type == "qcaloric") return QCALORIC_METER; + if (type == "apator162") return APATOR162_METER; return UNKNOWN_METER; } @@ -123,6 +124,7 @@ LinkMode toMeterLinkMode(string& type) if (type == "supercom587") return LinkMode::T1; if (type == "iperl") return LinkMode::T1; if (type == "qcaloric") return LinkMode::C1; + if (type == "apator162") return LinkMode::T1; return LinkMode::UNKNOWN; } diff --git a/src/meters.h b/src/meters.h index 6bc897c..a359e7a 100644 --- a/src/meters.h +++ b/src/meters.h @@ -24,7 +24,7 @@ #include #include -#define LIST_OF_METERS X(MULTICAL21_METER)X(FLOWIQ3100_METER)X(MULTICAL302_METER)X(OMNIPOWER_METER)X(SUPERCOM587_METER)X(IPERL_METER)X(QCALORIC_METER)X(UNKNOWN_METER) +#define LIST_OF_METERS X(MULTICAL21_METER)X(FLOWIQ3100_METER)X(MULTICAL302_METER)X(OMNIPOWER_METER)X(SUPERCOM587_METER)X(IPERL_METER)X(QCALORIC_METER)X(APATOR162_METER)X(UNKNOWN_METER) enum MeterType { #define X(name) name, @@ -116,6 +116,7 @@ unique_ptr createFlowIQ3100(WMBus *bus, string& name, string& id, st unique_ptr createMultical302(WMBus *bus, string& name, string& id, string& key); unique_ptr createOmnipower(WMBus *bus, string& name, string& id, string& key); unique_ptr createSupercom587(WMBus *bus, string& name, string& id, string& key); +unique_ptr createApator162(WMBus *bus, string& name, string& id, string& key); unique_ptr createIperl(WMBus *bus, string& name, string& id, string& key); unique_ptr createQCaloric(WMBus *bus, string& name, string& id, string& key); GenericMeter *createGeneric(WMBus *bus, string& name, string& id, string& key); diff --git a/wmbusmeters.1 b/wmbusmeters.1 index aee73d5..85bd696 100644 --- a/wmbusmeters.1 +++ b/wmbusmeters.1 @@ -68,7 +68,7 @@ mqtt_publish) sent to a REST API (eg curl) or store it in a database .TP \fBmeter_name\fR a mnemonic for your utility meter .TP -\fBmeter_type\fR multical21/flowiq3100/supercom587/iperl/multical302/omnipower/qcaloric +\fBmeter_type\fR multical21/flowiq3100/supercom587/iperl/multical302/omnipower/qcaloric/apator162 .TP \fBmeter_id\fR one or more 8 digit numbers separated with commas, or a single '*' wildcard. .TP