/* Copyright (C) 2017-2018 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; #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 WaterMeter, public virtual MeterCommonImplementation { MeterMultical21(WMBus *bus, const char *name, const char *id, const char *key); // Total water counted through the meter float totalWaterConsumption(); 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. float targetWaterConsumption(); bool hasTargetWaterConsumption(); // Max flow during last month or last 24 hours depending on meter configuration. float maxFlow(); bool hasMaxFlow(); // 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(); void printMeterHumanReadable(FILE *output); void printMeterFields(FILE *output, char separator); void printMeterJSON(FILE *output); private: void handleTelegram(Telegram *t); void processContent(Telegram *t); string decodeTime(int time); uint16_t info_codes_ {}; float total_water_consumption_ {}; bool has_total_water_consumption_ {}; float target_volume_ {}; bool has_target_volume_ {}; float max_flow_ {}; bool has_max_flow_ {}; }; MeterMultical21::MeterMultical21(WMBus *bus, const char *name, const char *id, const char *key) : MeterCommonImplementation(bus, name, id, key, MULTICAL21_METER, MANUFACTURER_KAM, 0x16) { MeterCommonImplementation::bus()->onTelegram(calll(this,handleTelegram,Telegram*)); } float MeterMultical21::totalWaterConsumption() { return total_water_consumption_; } bool MeterMultical21::hasTotalWaterConsumption() { return has_total_water_consumption_; } float MeterMultical21::targetWaterConsumption() { return target_volume_; } bool MeterMultical21::hasTargetWaterConsumption() { return has_target_volume_; } float MeterMultical21::maxFlow() { return max_flow_; } bool MeterMultical21::hasMaxFlow() { return has_max_flow_; } WaterMeter *createMultical21(WMBus *bus, const char *name, const char *id, const char *key) { return new MeterMultical21(bus,name,id,key); } void MeterMultical21::handleTelegram(Telegram *t) { if (!isTelegramForMe(t)) { // This telegram is not intended for this meter. return; } verbose("(multical21) telegram for %s %02x%02x%02x%02x\n", 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_device_type != 0x16) { warning("(multical21) expected telegram for water media, but got \"%s\"!\n", mediaType(t->m_field, t->a_field_device_type).c_str()); } if (t->m_field != manufacturer() || t->a_field_version != 0x1b) { warning("(multical21) expected telegram from KAM meter with version 0x1b, but got \"%s\" version 0x2x !\n", manufacturerFlag(t->m_field).c_str(), t->a_field_version); } if (useAes()) { vector aeskey = key(); decryptKamstrupC1(t, aeskey); } else { t->content = t->payload; } logTelegram("(multical21) log", t->parsed, t->content); int content_start = t->parsed.size(); processContent(t); if (isDebugEnabled()) { t->explainParse("(multical21)", content_start); } triggerUpdate(t); } void MeterMultical21::processContent(Telegram *t) { vector::iterator bytes = t->content.begin(); int crc0 = t->content[0]; int crc1 = t->content[1]; t->addExplanation(bytes, 2, "%02x%02x payload crc", crc0, crc1); int frame_type = t->content[2]; t->addExplanation(bytes, 1, "%02x frame type (%s)", frame_type, frameTypeKamstrupC1(frame_type).c_str()); if (frame_type == 0x79) { if (t->content.size() != 15) { warning("(multical21) warning: Unexpected length of short frame %zu. Expected 15 bytes! ", t->content.size()); padWithZeroesTo(&t->content, 15, &t->content); warning("\n"); } // 0,1 = crc for format signature = hash over DRH (Data Record Header) // The DRH is the dif(dife)vif(vife) bytes for all the records... // This hash should be used to pick up a suitable format string. // Below, DRH is hardcoded to 02FF2004134413 int ecrc0 = t->content[3]; int ecrc1 = t->content[4]; // 2,3 = crc for payload = hash over both DRH and data bytes. int ecrc2 = t->content[5]; int ecrc3 = t->content[6]; t->addExplanation(bytes, 4, "%02x%02x%02x%02x ecrc", ecrc0, ecrc1, ecrc2, ecrc3); vector::iterator data = t->content.begin()+7; size_t data_len = t->content.size()-7; map> values; uint16_t format_hash; vector format_bytes; hex2bin("02FF2004134413", &format_bytes); vector::iterator format = format_bytes.begin(); parseDV(t, data, data_len, &values, &format, format_bytes.size(), &format_hash); int offset; extractDVuint16(&values, "02FF20", &offset, &info_codes_); t->addMoreExplanation(offset, " info codes (%s)", statusHumanReadable().c_str()); extractDVfloat(&values, "0413", &offset, &total_water_consumption_); has_total_water_consumption_ = true; t->addMoreExplanation(offset, " total consumption (%f m3)", total_water_consumption_); extractDVfloatCombined(&values, "0413", "4413", &offset, &target_volume_); has_target_volume_ = true; t->addMoreExplanation(offset, " target consumption (%f m3)", target_volume_); } else if (frame_type == 0x78) { if (t->content.size() != 22) { warning("(multical21) warning: Unexpected length of long frame %zu. Expected 22 bytes! ", t->content.size()); padWithZeroesTo(&t->content, 22, &t->content); warning("\n"); } vector::iterator data = t->content.begin()+3; size_t data_len = t->content.size()-3-4; // Why this number? map> values; uint16_t format_hash; parseDV(t, data, data_len, &values, NULL, 0, &format_hash); // There are two more bytes in the data. Unknown purpose. int val0 = t->content[20]; int val1 = t->content[21]; int offset; extractDVuint16(&values, "02FF20", &offset, &info_codes_); t->addMoreExplanation(offset, " info codes (%s)", statusHumanReadable().c_str()); extractDVfloat(&values, "0413", &offset, &total_water_consumption_); has_total_water_consumption_ = true; t->addMoreExplanation(offset, " total consumption (%f m3)", total_water_consumption_); extractDVfloat(&values, "4413", &offset, &target_volume_); has_target_volume_ = true; t->addMoreExplanation(offset, " target consumption (%f m3)", target_volume_); // To unknown bytes, seems to be very constant. t->addExplanation(data, 2, "%02x%02x unknown", val0, val1); } else { warning("(multical21) warning: unknown frame %02x (did you use the correct encryption key?)\n", frame_type); } } 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) { warning("(multical21) warning: Cannot decode time %d should be 0-7.\n", 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 "?"; } } void MeterMultical21::printMeterHumanReadable(FILE *output) { fprintf(output, "%s\t%s\t% 3.3f m3\t% 3.3f m3\t%s\t%s\n", name().c_str(), id().c_str(), totalWaterConsumption(), targetWaterConsumption(), statusHumanReadable().c_str(), datetimeOfUpdateHumanReadable().c_str()); } void MeterMultical21::printMeterFields(FILE *output, char separator) { fprintf(output, "%s%c%s%c%f%c%f%c%s%c%s\n", name().c_str(), separator, id().c_str(), separator, totalWaterConsumption(), separator, targetWaterConsumption(), separator, statusHumanReadable().c_str(), separator, datetimeOfUpdateRobot().c_str()); } #define Q(x,y) "\""#x"\":"#y"," #define QS(x,y) "\""#x"\":\""#y"\"," #define QSE(x,y) "\""#x"\":\""#y"\"" void MeterMultical21::printMeterJSON(FILE *output) { fprintf(output, "{media:\"%s\",meter:\"multical21\"," QS(name,%s) QS(id,%s) Q(total_m3,%f) Q(target_m3,%f) QS(current_status,%s) QS(time_dry,%s) QS(time_reversed,%s) QS(time_leaking,%s) QS(time_bursting,%s) QSE(timestamp,%s) "}\n", mediaType(manufacturer(), media()).c_str(), name().c_str(), id().c_str(), totalWaterConsumption(), targetWaterConsumption(), status().c_str(), // DRY REVERSED LEAK BURST timeDry().c_str(), timeReversed().c_str(), timeLeaking().c_str(), timeBursting().c_str(), datetimeOfUpdateRobot().c_str()); }