diff --git a/CHANGES b/CHANGES index 1fc3c5e..aa41a32 100644 --- a/CHANGES +++ b/CHANGES @@ -1,4 +1,12 @@ +Version 0.4: + +Added initial support for power meter Multical302. +Restructured to source to more easily support multiple meters. + +ATTENTION! There is a difference in the command line interface. +You must now proved the meter type. Thus for each meter you +supply quadruplets instead of triplets. Version 0.3: diff --git a/Makefile b/Makefile index 3541aae..485a827 100644 --- a/Makefile +++ b/Makefile @@ -27,7 +27,7 @@ endif $(shell mkdir -p $(BUILD)) -CXXFLAGS := $(DEBUG_FLAGS) -Wall -fmessage-length=0 -std=c++11 -Wno-unused-function "-DWMBUSMETERS_VERSION=\"0.3\"" +CXXFLAGS := $(DEBUG_FLAGS) -Wall -fmessage-length=0 -std=c++11 -Wno-unused-function "-DWMBUSMETERS_VERSION=\"0.4\"" $(BUILD)/%.o: %.cc $(wildcard %.h) $(CXX) $(CXXFLAGS) $< -c -o $@ @@ -38,12 +38,15 @@ METERS_OBJS:=\ $(BUILD)/main.o \ $(BUILD)/meters.o \ $(BUILD)/meter_multical21.o \ + $(BUILD)/meter_multical302.o \ $(BUILD)/printer.o \ $(BUILD)/serial.o \ $(BUILD)/util.o \ $(BUILD)/wmbus.o \ $(BUILD)/wmbus_amb8465.o \ $(BUILD)/wmbus_im871a.o \ + $(BUILD)/wmbus_simulator.o \ + $(BUILD)/wmbus_utils.o all: $(BUILD)/wmbusmeters $(STRIP_BINARY) @@ -55,7 +58,7 @@ clean: rm -f build/* build_arm/* build_debug/* build_arm_debug/* *~ test: - @echo No tests yet. + ./test.sh build/wmbusmeters update_manufacturers: wget http://www.m-bus.de/man.html diff --git a/README.md b/README.md index fd3e83c..2d3ef98 100644 --- a/README.md +++ b/README.md @@ -8,10 +8,10 @@ utility meter readings. |Linux G++| [![Build Status](https://travis-ci.org/weetmuts/wmbusmeters.svg?branch=master)](https://travis-ci.org/weetmuts/wmbusmeters) | ``` -wmbusmeters version: 0.3 -Usage: wmbusmeters {options} (auto | /dev/ttyUSBx)] { [meter_name] [meter_id] [meter_key] }* +wmbusmeters version: 0.4 +Usage: wmbusmeters {options} (auto | /dev/ttyUSBx)] { [meter_name] [meter_type] [meter_id] [meter_key] }* -Add more meter triplets to listen to more meters. +Add more meter quadruplets to listen to more meters. Add --verbose for detailed debug information. --robot for json output. --meterfiles to create status files below tmp, @@ -19,16 +19,23 @@ Add --verbose for detailed debug information. --oneshot wait for an update from each meter, then quit. Specifying auto as the device will automatically look for usb -wmbus dongles on /dev/im871a and /dev/amb8465 +wmbus dongles on /dev/im871a and /dev/amb8465. + +Two meter types are supported: multical21 and multical302 (multical302 is still work in progress). ``` -No meter triplets means listen for telegram traffic and print any id heard. +Currently the meters are hardcoded for the European default setting that specifies what extra data +is sent in the telegrams. If someone has a non-default meter that sends other extra data, then this +will show up as a warning when a long telegram is received (but not in the short telegrams!). +If this should happen, then we need to implement a way to pass the meter configuration as a parameter. + +No meter quadruplets means listen for telegram traffic and print any id heard. # Builds and runs on GNU/Linux: ``` make -./build/wmbusmeters /dev/ttyUSB0 MyTapWater 12345678 00112233445566778899AABBCCDDEEFF +./build/wmbusmeters /dev/ttyUSB0 MyTapWater multical21 12345678 00112233445566778899AABBCCDDEEFF ``` wmbusmeters will detect which kind of dongle is connected to /dev/ttyUSB0. It can be either an IMST 871a dongle or an Amber Wireless AMB8465. If you have setup the udev rules below, then you can use auto instead of /dev/ttyUSB0. @@ -36,9 +43,9 @@ wmbusmeters will detect which kind of dongle is connected to /dev/ttyUSB0. It ca Example output: `MyTapWater 12345678 6.375 m3 2017-08-31 09:09.08 3.040 m3 DRY(dry 22-31 days)` -`./build/wmbusmeters --verbose /dev/ttyUSB0 MyTapWater 12345678 00112233445566778899AABBCCDDEEFF` +`./build/wmbusmeters --verbose /dev/ttyUSB0 MyTapWater multical21 12345678 00112233445566778899AABBCCDDEEFF` -`./build/wmbusmeters --robot auto 12345678 00112233445566778899AABBCCDDEEFF` +`./build/wmbusmeters --robot auto MyElectricity multical302 12345678 00112233445566778899AABBCCDDEEFF MyTapWater multical21 12345678 00112233445566778899AABBCCDDEEFF` Robot output: `{"name":"MyTapWater","id":"12345678","total_m3":6.375,"target_m3":3.040,"current_status":"","time_dry":"22-31 days","time_reversed":"","time_leaking":"","time_bursting":"","timestamp":"2017-08-31T09:07:18Z"}` @@ -60,7 +67,11 @@ Binary generated: `./build_arm_debug/wmbusmeters` If the meter does not use encryption of its meter data, then enter an empty key on the command line. (you must enter "") -`./build/wmbusmeters --robot --meterfiles /dev/ttyUSB0 MyTapWater 12345678 ""` +`./build/wmbusmeters --robot --meterfiles /dev/ttyUSB0 MyTapWater multical21 12345678 ""` + +You can run wmbusmeters with --logtelegrams to get log output that can be placed in a simulation.txt +file. You can then run wmbusmeter and instead of auto (or an usb device) provide the simulationt.xt +file as argument. See test.sh for more info. # System configuration @@ -82,7 +93,7 @@ exact serial port /dev/ttyUSBx. Two usb wmbus receivers are supported: IMST im871A and Amber Wireless AMB8465. -One supported meter: Multical21. +Two supported meters: Multical21 (water meter) and Multical302 (power meter, work in progress). The source code is modular and it should be relatively straightforward to add more receivers and meters. diff --git a/cmdline.cc b/cmdline.cc index 14ba819..74974ac 100644 --- a/cmdline.cc +++ b/cmdline.cc @@ -52,8 +52,39 @@ CommandLine *parseCommandLine(int argc, char **argv) { i++; continue; } - if (!strcmp(argv[i], "--robot")) { - c->robot = true; + if (!strcmp(argv[i], "--logtelegrams")) { + c->logtelegrams = true; + i++; + continue; + } + if (!strncmp(argv[i], "--robot", 7)) { + if (strlen(argv[i]) == 7 || + (strlen(argv[i]) == 12 && + !strncmp(argv[i]+7, "=json", 5))) + { + c->json = true; + c->fields = false; + } + else if (strlen(argv[i]) == 14 && + !strncmp(argv[i]+7, "=fields", 7)) + { + c->json = false; + c->fields = true; + c->separator = ';'; + } else { + error("Unknown output format: \"%s\"\n", argv[i]+7); + } + i++; + continue; + } + if (!strncmp(argv[i], "--separator=", 12)) { + if (!c->fields) { + error("You must specify --robot=fields before --separator=X\n"); + } + if (strlen(argv[i]) != 13) { + error("You must supply a single character as the field separator.\n"); + } + c->separator = argv[i][12]; i++; continue; } @@ -79,20 +110,22 @@ CommandLine *parseCommandLine(int argc, char **argv) { if (!c->usb_device) error("You must supply the usb device to which the wmbus dongle is connected.\n"); verbose("Using usb device: %s\n", c->usb_device); - if ((argc-i) % 3 != 0) { - error("For each meter you must supply a: name,id and key.\n"); + if ((argc-i) % 4 != 0) { + error("For each meter you must supply a: name,type,id and key.\n"); } - int num_meters = (argc-i)/3; + int num_meters = (argc-i)/4; verbose("Number of meters: %d\n", num_meters); for (int m=0; mmeters.push_back(MeterInfo(name,id,key)); + c->meters.push_back(MeterInfo(name,type,id,key)); } return c; diff --git a/cmdline.h b/cmdline.h index efa10f7..4d3d101 100644 --- a/cmdline.h +++ b/cmdline.h @@ -29,12 +29,14 @@ using namespace std; struct MeterInfo { char *name; + char *type; char *id; char *key; Meter *meter; - MeterInfo(char *n, char *i, char *k) { + MeterInfo(char *n, char *t, char *i, char *k) { name = n; + type = t; id = i; key = k; } @@ -45,8 +47,11 @@ struct CommandLine { bool silence {}; bool verbose {}; bool debug {}; + bool logtelegrams {}; bool meterfiles {}; - bool robot {}; + bool json {}; + bool fields {}; + char separator { ';' }; bool oneshot {}; char *usb_device {}; vector meters; diff --git a/main.cc b/main.cc index f383b37..f0842e4 100644 --- a/main.cc +++ b/main.cc @@ -37,8 +37,8 @@ int main(int argc, char **argv) if (cmdline->need_help) { printf("wmbusmeters version: " WMBUSMETERS_VERSION "\n"); - printf("Usage: wmbusmeters [options] (auto | /dev/ttyUSBx) { [meter_name] [meter_id] [meter_key] }* \n\n"); - printf("Add more meter triplets to listen to more meters.\n"); + printf("Usage: wmbusmeters [options] (auto | /dev/ttyUSBx) { [meter_name] [meter_type] [meter_id] [meter_key] }* \n\n"); + printf("Add more meter quadruplets to listen to more meters.\n"); printf("Add --verbose for more detailed information on communication.\n"); printf(" --robot for json output.\n"); printf(" --meterfiles to create status files below tmp,\n" @@ -46,7 +46,7 @@ int main(int argc, char **argv) printf(" --oneshot wait for an update from each meter, then quit.\n\n"); printf("Specifying auto as the device will automatically look for usb\n"); printf("wmbus dongles on /dev/im871a and /dev/amb8465\n\n"); - + printf("Two meter types are supported: multical21 and multical302 (work in progress).\n\n"); exit(0); } // We want the data visible in the log file asap! @@ -55,6 +55,7 @@ int main(int argc, char **argv) warningSilenced(cmdline->silence); verboseEnabled(cmdline->verbose); debugEnabled(cmdline->debug); + logTelegramsEnabled(cmdline->logtelegrams); auto manager = createSerialCommunicationManager(); @@ -73,6 +74,10 @@ int main(int argc, char **argv) verbose("(amb8465) detected on %s\n", type_and_device.second.c_str()); wmbus = openAMB8465(type_and_device.second, manager); break; + case DEVICE_SIMULATOR: + verbose("(simulator) found %s\n", type_and_device.second.c_str()); + wmbus = openSimulator(type_and_device.second, manager); + break; case DEVICE_UNKNOWN: error("No wmbus device found!\n"); exit(1); @@ -82,14 +87,26 @@ int main(int argc, char **argv) wmbus->setLinkMode(LinkModeC1); if (wmbus->getLinkMode()!=LinkModeC1) error("Could not set link mode to receive C1 telegrams.\n"); - Printer *output = new Printer(cmdline->robot, cmdline->meterfiles); + Printer *output = new Printer(cmdline->json, cmdline->fields, + cmdline->separator, cmdline->meterfiles); if (cmdline->meters.size() > 0) { for (auto &m : cmdline->meters) { - m.meter = createMultical21(wmbus, m.name, m.id, m.key); - verbose("(multical21) configured \"%s\" \"%s\" \"%s\"\n", m.name, m.id, m.key); + switch (toMeterType(m.type)) { + case MULTICAL21_METER: + m.meter = createMultical21(wmbus, m.name, m.id, m.key); + verbose("(multical21) configured \"%s\" \"multical21\" \"%s\" \"%s\"\n", m.name, m.id, m.key); + break; + case MULTICAL302_METER: + m.meter = createMultical302(wmbus, m.name, m.id, m.key); + verbose("(multical302) configured \"%s\" \"multical302\" \"%s\" \"%s\"\n", m.name, m.id, m.key); + break; + case UNKNOWN_METER: + error("No such meter type \"%s\"\n", m.type); + break; + } m.meter->onUpdate(calll(output,print,Meter*)); - m.meter->onUpdate([cmdline,manager](Meter*meter) { oneshotCheck(cmdline,manager,meter); }); + m.meter->onUpdate([cmdline,manager](Meter*meter) { oneshotCheck(cmdline,manager,meter); }); } } else { printf("No meters configured. Printing id:s of all telegrams heard!\n\n"); @@ -97,6 +114,10 @@ int main(int argc, char **argv) wmbus->onTelegram([](Telegram *t){t->print();}); } + if (type_and_device.first == DEVICE_SIMULATOR) { + wmbus->simulate(); + } + manager->waitForStop(); } diff --git a/meter_multical21.cc b/meter_multical21.cc index 8256af1..c8d4096 100644 --- a/meter_multical21.cc +++ b/meter_multical21.cc @@ -18,9 +18,10 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. -#include"aes.h" #include"meters.h" +#include"meters_common_implementation.h" #include"wmbus.h" +#include"wmbus_utils.h" #include"util.h" #include @@ -43,11 +44,9 @@ using namespace std; #define INFO_CODE_BURST 0x08 #define INFO_CODE_BURST_SHIFT (4+9) -struct MeterMultical21 : public Meter { +struct MeterMultical21 : public virtual WaterMeter, public virtual MeterCommonImplementation { MeterMultical21(WMBus *bus, const char *name, const char *id, const char *key); - string id(); - string name(); // Total water counted through the meter float totalWaterConsumption(); bool hasTotalWaterConsumption(); @@ -71,68 +70,30 @@ struct MeterMultical21 : public Meter { string timeLeaking(); string timeBursting(); - string datetimeOfUpdateHumanReadable(); - string datetimeOfUpdateRobot(); - void onUpdate(function cb); - int numUpdates(); + void printMeterHumanReadable(FILE *output); + void printMeterFields(FILE *output, char separator); + void printMeterJSON(FILE *output); private: - void handleTelegram(Telegram*t); - void processContent(vector &d); + void handleTelegram(Telegram *t); + void processContent(Telegram *t); string decodeTime(int time); - int 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_; - - time_t datetime_of_update_; - - string name_; - vector id_; - vector key_; - WMBus *bus_; - vector> on_update_; - int num_updates_; - bool use_aes_; + int 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) : - info_codes_(0), total_water_consumption_(0), has_total_water_consumption_(false), - target_volume_(0), has_target_volume_(false), - max_flow_(0), has_max_flow_(false), name_(name), bus_(bus), num_updates_(0), use_aes_(true) + MeterCommonImplementation(bus, name, id, key, MULTICAL21_METER, MANUFACTURER_KAM, 0x16) { - hex2bin(id, &id_); - if (strlen(key) == 0) { - use_aes_ = false; - } else { - hex2bin(key, &key_); - } - bus_->onTelegram(calll(this,handleTelegram,Telegram*)); + MeterCommonImplementation::bus()->onTelegram(calll(this,handleTelegram,Telegram*)); } -string MeterMultical21::id() -{ - return bin2hex(id_); -} - -string MeterMultical21::name() -{ - return name_; -} - -void MeterMultical21::onUpdate(function cb) -{ - on_update_.push_back(cb); -} - -int MeterMultical21::numUpdates() -{ - return num_updates_; -} float MeterMultical21::totalWaterConsumption() { @@ -164,141 +125,46 @@ bool MeterMultical21::hasMaxFlow() return has_max_flow_; } -string MeterMultical21::datetimeOfUpdateHumanReadable() -{ - char datetime[40]; - memset(datetime, 0, sizeof(datetime)); - strftime(datetime, 20, "%Y-%m-%d %H:%M.%S", localtime(&datetime_of_update_)); - return string(datetime); -} - -string MeterMultical21::datetimeOfUpdateRobot() -{ - char datetime[40]; - memset(datetime, 0, sizeof(datetime)); - // This is the date time in the Greenwich timezone (Zulu time), dont get surprised! - strftime(datetime, sizeof(datetime), "%FT%TZ", gmtime(&datetime_of_update_)); - return string(datetime); -} - -Meter *createMultical21(WMBus *bus, const char *name, const char *id, const char *key) { +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 (t->m_field != MANUFACTURER_KAM || - t->a_field_address[3] != id_[3] || - t->a_field_address[2] != id_[2] || - t->a_field_address[1] != id_[1] || - t->a_field_address[0] != id_[0]) - { + if (!isTelegramForMe(t)) { // This telegram is not intended for this meter. return; } - verbose("(multical21) %s %02x%02x%02x%02x ", - name_.c_str(), + 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]); - // This is part of the wmbus protocol, should be moved to wmbus source files! - int cc_field = t->payload[0]; - verbose("CC-field=%02x ( ", cc_field); - if (cc_field & CC_B_BIDIRECTIONAL_BIT) verbose("bidir "); - if (cc_field & CC_RD_RESPONSE_DELAY_BIT) verbose("fast_res "); - else verbose("slow_res "); - if (cc_field & CC_S_SYNCH_FRAME_BIT) verbose("synch "); - if (cc_field & CC_R_RELAYED_BIT) verbose("relayed "); // Relayed by a repeater - if (cc_field & CC_P_HIGH_PRIO_BIT) verbose("prio "); - verbose(") "); - - int acc = t->payload[1]; - verbose("ACC-field=%02x ", acc); - - uchar sn[4]; - sn[0] = t->payload[2]; - sn[1] = t->payload[3]; - sn[2] = t->payload[4]; - sn[3] = t->payload[5]; - - verbose("SN=%02x%02x%02x%02x encrypted=", sn[3], sn[2], sn[1], sn[0]); - // Here is a bug, since it always reports no encryption, but the multicals21 - // so far have all have encryption enabled. - if ((sn[3] & SN_ENC_BITS) == 0) verbose("no"); - else if ((sn[3] & SN_ENC_BITS) == 0x40) verbose("yes"); - else verbose("? %d\n", sn[3] & SN_ENC_BITS); - verbose("\n"); - - // The content begins with the Payload CRC at offset 6. - vector content; - content.insert(content.end(), t->payload.begin()+6, t->payload.end()); - size_t remaining = content.size(); - if (remaining > 16) remaining = 16; - - uchar iv[16]; - int i=0; - // M-field - iv[i++] = t->m_field&255; iv[i++] = t->m_field>>8; - // A-field - for (int j=0; j<6; ++j) { iv[i++] = t->a_field[j]; } - // CC-field - iv[i++] = cc_field; - // SN-field - for (int j=0; j<4; ++j) { iv[i++] = sn[j]; } - // FN - iv[i++] = 0; iv[i++] = 0; - // BC - iv[i++] = 0; - - if (use_aes_) { - vector ivv(iv, iv+16); - string s = bin2hex(ivv); - debug("(multical21) IV %s\n", s.c_str()); - - uchar xordata[16]; - AES_ECB_encrypt(iv, &key_[0], xordata, 16); - - uchar decrypt[16]; - xorit(xordata, &content[0], decrypt, remaining); - - vector dec(decrypt, decrypt+remaining); - debugPayload("(multical21) decrypted", dec); - - if (content.size() > 22) { - warning("(multical21) warning: Received too many bytes of content! " - "Got %zu bytes, expected at most 22.\n", content.size()); - } - if (content.size() > 16) { - // Yay! Lets decrypt a second block. Full frame content is 22 bytes. - // So a second block should enough for everyone! - remaining = content.size()-16; - if (remaining > 16) remaining = 16; // Should not happen. - - incrementIV(iv, sizeof(iv)); - vector ivv2(iv, iv+16); - string s2 = bin2hex(ivv2); - debug("(multical21) IV+1 %s\n", s2.c_str()); - - AES_ECB_encrypt(iv, &key_[0], xordata, 16); - - xorit(xordata, &content[16], decrypt, remaining); - - vector dec2(decrypt, decrypt+remaining); - debugPayload("(multical21) decrypted", dec2); - - // Append the second decrypted block to the first. - dec.insert(dec.end(), dec2.begin(), dec2.end()); - } - content.clear(); - content.insert(content.end(), dec.begin(), dec.end()); + 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)); } - processContent(content); - datetime_of_update_ = time(NULL); + 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); + } - num_updates_++; - for (auto &cb : on_update_) if (cb) cb(this); + 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); } float getScaleFactor(int vif) { @@ -312,115 +178,130 @@ float getScaleFactor(int vif) { return 1000.0; } -void MeterMultical21::processContent(vector &c) { - int crc0 = c[0]; - int crc1 = c[1]; - int frame_type = c[2]; - verbose("(multical21) CRC16: %02x%02x\n", crc1, crc0); - /* - uint16_t crc = crc16(&(c[2]), c.size()-2); - verbose("(multical21) CRC16 calc: %04x\n", crc); - */ +void MeterMultical21::processContent(Telegram *t) { + vector full_content; + full_content.insert(full_content.end(), t->parsed.begin(), t->parsed.end()); + full_content.insert(full_content.end(), t->content.begin(), t->content.end()); + + int crc0 = t->content[0]; + int crc1 = t->content[1]; + t->addExplanation(full_content, 2, "%02x%02x plcrc", crc0, crc1); + int frame_type = t->content[2]; + t->addExplanation(full_content, 1, "%02x frame type (%s)", frame_type, frameTypeKamstrupC1(frame_type).c_str()); + if (frame_type == 0x79) { - verbose("(multical21) Short frame %d bytes\n", c.size()); - if (c.size() != 15) { - warning("(multical21) warning: Unexpected length of frame %zu. Expected 15 bytes!\n", c.size()); + if (t->content.size() != 15) { + warning("(multical21) warning: Unexpected length of short frame %zu. Expected 15 bytes! ", + t->content.size()); + padWithZeroesTo(&t->content, 15, &full_content); + warning("\n"); } - /*int ecrc0 = c[3]; - int ecrc1 = c[4]; - int ecrc2 = c[5]; - int ecrc3 = c[6];*/ - int rec1val0 = c[7]; - int rec1val1 = c[8]; - int rec2val0 = c[9]; - int rec2val1 = c[10]; - int rec2val2 = c[11]; - int rec2val3 = c[12]; - int rec3val0 = c[13]; - int rec3val1 = c[14]; + int ecrc0 = t->content[3]; + int ecrc1 = t->content[4]; + int ecrc2 = t->content[5]; + int ecrc3 = t->content[6]; + t->addExplanation(full_content, 4, "%02x%02x%02x%02x ecrc", ecrc0, ecrc1, ecrc2, ecrc3); + int rec1val0 = t->content[7]; + int rec1val1 = t->content[8]; + + int rec2val0 = t->content[9]; + int rec2val1 = t->content[10]; + int rec2val2 = t->content[11]; + + int rec2val3 = t->content[12]; + int rec3val0 = t->content[13]; + int rec3val1 = t->content[14]; info_codes_ = rec1val1*256+rec1val0; - verbose("(multical21) short rec1 %02x %02x info codes\n", rec1val1, rec1val0); + t->addExplanation(full_content, 2, "%02x%02x info codes (%s)", rec1val0, rec1val1, statusHumanReadable().c_str()); int consumption_raw = rec2val3*256*256*256 + rec2val2*256*256 + rec2val1*256 + rec2val0; - verbose("(multical21) short rec2 %02x %02x %02x %02x = %d total consumption\n", rec2val3, rec2val2, rec2val1, rec2val0, consumption_raw); - // The dif=0x04 vif=0x13 means current volume with scale factor .001 total_water_consumption_ = ((float)consumption_raw) / ((float)1000); + t->addExplanation(full_content, 4, "%02x%02x%02x%02x total consumption (%d)", + rec2val0, rec2val1, rec2val2, rec2val3, consumption_raw); has_total_water_consumption_ = true; // The short frame target volume supplies two low bytes, // the remaining two hi bytes are >>probably<< picked from rec2! int target_volume_raw = rec2val3*256*256*256 + rec2val2*256*256 + rec3val1*256 + rec3val0; - verbose("(multical21) short rec3 (%02x %02x) %02x %02x = %d target volume\n", rec2val3, rec2val2, rec3val1, rec3val0, target_volume_raw); target_volume_ = ((float)target_volume_raw) / ((float)1000); + t->addExplanation(full_content, 2, "%02x%02x target volume (%d)", + rec3val0, rec3val1, target_volume_raw); has_target_volume_ = true; - } else if (frame_type == 0x78) { - verbose("(multical21) Full frame %d bytes\n", c.size()); - if (c.size() != 22) { - warning("(multical21) warning: Unexpected length of frame %zu. Expected 22 bytes!\n", c.size()); + if (t->content.size() != 22) { + warning("(multical21) warning: Unexpected length of long frame %zu. Expected 22 bytes! ", t->content.size()); + padWithZeroesTo(&t->content, 22, &full_content); + warning("\n"); } - int rec1dif = c[3]; - int rec1vif = c[4]; - int rec1vife = c[5]; + int rec1dif = t->content[3]; + int rec1vif = t->content[4]; + int rec1vife = t->content[5]; - int rec1val0 = c[6]; - int rec1val1 = c[7]; + int rec1val0 = t->content[6]; + int rec1val1 = t->content[7]; - int rec2dif = c[8]; - int rec2vif = c[9]; - int rec2val0 = c[10]; - int rec2val1 = c[11]; - int rec2val2 = c[12]; - int rec2val3 = c[13]; + int rec2dif = t->content[8]; + int rec2vif = t->content[9]; + int rec2val0 = t->content[10]; + int rec2val1 = t->content[11]; + int rec2val2 = t->content[12]; + int rec2val3 = t->content[13]; - int rec3dif = c[14]; - int rec3vif = c[15]; - int rec3val0 = c[16]; - int rec3val1 = c[17]; - int rec3val2 = c[18]; - int rec3val3 = c[19]; + int rec3dif = t->content[14]; + int rec3vif = t->content[15]; + int rec3val0 = t->content[16]; + int rec3val1 = t->content[17]; + int rec3val2 = t->content[18]; + int rec3val3 = t->content[19]; // There are two more bytes in the data. Unknown purpose. - int rec4val0 = c[20]; - int rec4val1 = c[21]; + int rec4val0 = t->content[20]; + int rec4val1 = t->content[21]; if (rec1dif != 0x02 || rec1vif != 0xff || rec1vife != 0x20 ) { warning("(multical21) warning: Unexpected field! Expected info codes\n" "with dif=0x02 vif=0xff vife=0x20 but got dif=%02x vif=%02x vife=%02x\n", rec1dif, rec1vif, rec1vife); } + t->addExplanation(full_content, 1, "%02x dif (%s)", rec1dif, difType(rec1dif).c_str()); + t->addExplanation(full_content, 1, "%02x vif (%s)", rec1vif, vifType(rec1vif).c_str()); + t->addExplanation(full_content, 1, "%02x vife (%s)", rec1vife, vifeType(rec1vif, rec1vife).c_str()); info_codes_ = rec1val1*256+rec1val0; - verbose("(multical21) full rec1 dif=%02x vif=%02x vife=%02x\n", rec1dif, rec1vif, rec1vife); - verbose("(multical21) full rec1 %02x %02x info codes\n", rec1val1, rec1val0); + t->addExplanation(full_content, 2, "%02x%02x info codes (%s)", rec1val1, rec1val0, statusHumanReadable().c_str()); if (rec2dif != 0x04 || rec2vif != 0x13) { warning("(multical21) warning: Unexpected field! Expected current volume\n" "with dif=0x04 vif=0x13 but got dif=%02x vif=%02x\n", rec2dif, rec2vif); } + t->addExplanation(full_content, 1, "%02x dif (%s)", rec2dif, difType(rec2dif).c_str()); + t->addExplanation(full_content, 1, "%02x vif (%s)", rec2vif, vifType(rec2vif).c_str()); int consumption_raw = rec2val3*256*256*256 + rec2val2*256*256 + rec2val1*256 + rec2val0; - verbose("(multical21) full rec2 dif=%02x vif=%02x\n", rec2dif, rec2vif); - verbose("(multical21) full rec2 %02x %02x %02x %02x = %d total consumption\n", rec2val3, rec2val2, rec2val1, rec2val0, consumption_raw); // The dif=0x04 vif=0x13 means current volume with scale factor .001 total_water_consumption_ = ((float)consumption_raw) / ((float)1000); has_total_water_consumption_ = true; + t->addExplanation(full_content, 4, "%02x%02x%02x%02x total consumption (%d)", + rec2val3, rec2val2, rec2val1, rec2val0, consumption_raw); if (rec3dif != 0x44 || rec3vif != 0x13) { warning("(multical21) warning: Unexpected field! Expected target volume (ie volume recorded on first day of month)\n" "with dif=0x44 vif=0x13 but got dif=%02x vif=%02x\n", rec3dif, rec3vif); } - int target_volume_raw = rec3val3*256*256*256 + rec3val2*256*256 + rec3val1*256 + rec3val0; - verbose("(multical21) full rec3 dif=%02x vif=%02x\n", rec3dif, rec3vif); - verbose("(multical21) full rec3 %02x %02x %02x %02x = %d target volume\n", rec3val3, rec3val2, rec3val1, rec3val0, target_volume_raw); + t->addExplanation(full_content, 1, "%02x dif (%s)", rec3dif, difType(rec3dif).c_str()); + t->addExplanation(full_content, 1, "%02x vif (%s)", rec3vif, vifType(rec3vif).c_str()); + int target_volume_raw = rec3val3*256*256*256 + rec3val2*256*256 + rec3val1*256 + rec3val0; target_volume_ = ((float)target_volume_raw) / ((float)1000); has_target_volume_ = true; + t->addExplanation(full_content, 4, "%02x%02x%02x%02x target consumption (%d)", + rec3val3, rec3val2, rec3val1, rec3val0, target_volume_raw); + // To unknown bytes, seems to be very constant. - verbose("(multical21) full rec4 %02x %02x = unknown\n", rec4val1, rec4val0); + t->addExplanation(full_content, 2, "%02x%02x unknown", rec4val0, rec4val1); } else { warning("(multical21) warning: Unknown frame %02x\n", frame_type); } @@ -540,3 +421,56 @@ string MeterMultical21::decodeTime(int time) { 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%3.3f%c%3.3f%c%s%c%s\n", + name().c_str(), separator, + id().c_str(), separator, + totalWaterConsumption(), separator, + targetWaterConsumption(), separator, + statusHumanReadable().c_str(), separator, + datetimeOfUpdateHumanReadable().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,%.3f) + Q(target_m3,%.3f) + 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()); +} diff --git a/meter_multical302.cc b/meter_multical302.cc new file mode 100644 index 0000000..a467829 --- /dev/null +++ b/meter_multical302.cc @@ -0,0 +1,225 @@ +// Copyright (c) 2018 Fredrik Öhrström +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +#include"meters.h" +#include"meters_common_implementation.h" +#include"wmbus.h" +#include"wmbus_utils.h" +#include"util.h" + +#include +#include +#include +#include +#include + +struct MeterMultical302 : public virtual HeatMeter, public virtual MeterCommonImplementation { + MeterMultical302(WMBus *bus, const char *name, const char *id, const char *key); + + float totalPowerConsumption(); + float currentPowerConsumption(); + float totalVolume(); + + void printMeterHumanReadable(FILE *output); + void printMeterFields(FILE *output, char separator); + void printMeterJSON(FILE *output); + +private: + void handleTelegram(Telegram *t); + void processContent(Telegram *t); + + float total_power_ {}; + float current_power_ {}; + float total_volume_ {}; +}; + +MeterMultical302::MeterMultical302(WMBus *bus, const char *name, const char *id, const char *key) : + MeterCommonImplementation(bus, name, id, key, MULTICAL302_METER, MANUFACTURER_KAM, 0x04) +{ + MeterCommonImplementation::bus()->onTelegram(calll(this,handleTelegram,Telegram*)); +} + +float MeterMultical302::totalPowerConsumption() +{ + return total_power_; +} + +float MeterMultical302::currentPowerConsumption() +{ + return current_power_; +} + +float MeterMultical302::totalVolume() +{ + return total_volume_; +} + +void MeterMultical302::handleTelegram(Telegram *t) { + + if (!isTelegramForMe(t)) { + // This telegram is not intended for this meter. + return; + } + + verbose("(multical302) %s %02x%02x%02x%02x ", + 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 != 0x04) { + warning("(multical302) expected telegram for heat 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("(multical302) 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("(multical302) log", t->parsed, t->content); + int content_start = t->parsed.size(); + processContent(t); + if (isDebugEnabled()) { + t->explainParse("(multical302)", content_start); + } + triggerUpdate(t); +} + +void MeterMultical302::processContent(Telegram *t) { + vector full_content; + full_content.insert(full_content.end(), t->parsed.begin(), t->parsed.end()); + full_content.insert(full_content.end(), t->content.begin(), t->content.end()); + + int crc0 = t->content[0]; + int crc1 = t->content[1]; + t->addExplanation(full_content, 2, "%02x%02x plcrc", crc0, crc1); + int frame_type = t->content[2]; + t->addExplanation(full_content, 1, "%02x frame type (%s)", frame_type, frameTypeKamstrupC1(frame_type).c_str()); + + if (frame_type == 0x79) { + if (t->content.size() != 17) { + fprintf(stderr, "(multical302) warning: Unexpected length of frame %zu. Expected 17 bytes! ", t->content.size()); + padWithZeroesTo(&t->content, 17, &full_content); + warning("\n"); + } + + t->addExplanation(full_content, 4, "%02x%02x%02x%02x unknown", t->content[3], t->content[4], t->content[5], t->content[6]); + + int rec1val0 = t->content[7]; + int rec1val1 = t->content[8]; + int rec1val2 = t->content[9]; + + t->addExplanation(full_content, 4, "%02x%02x%02x unknown", t->content[10], t->content[11], t->content[12]); + + int total_power_raw = rec1val2*256*256 + rec1val1*256 + rec1val0; + total_power_ = total_power_raw; + t->addExplanation(full_content, 3, "%02x%02x%02x total power (%d)", + rec1val0, rec1val1, rec1val2, total_power_raw); + + int rec2val0 = t->content[13]; + int rec2val1 = t->content[14]; + int rec2val2 = t->content[15]; + + int total_volume_raw = rec2val2*256*256 + rec2val1*256 + rec2val0; + total_volume_ = total_volume_raw; + t->addExplanation(full_content, 3, "%02x%02x%02x total volume (%d)", + rec2val0, rec2val1, rec2val2, total_volume_raw); + } + else if (frame_type == 0x78) + { + if (t->content.size() != 26) { + fprintf(stderr, "(multical302) warning: Unexpected length of frame %zu. Expected 26 bytes! ", t->content.size()); + padWithZeroesTo(&t->content, 26, &full_content); + warning("\n"); + } + + vector unknowns; + unknowns.insert(unknowns.end(), t->content.begin()+3, t->content.begin()+24); + string hex = bin2hex(unknowns); + t->addExplanation(full_content, 23-2, "%s unknown", hex.c_str()); + + int rec1val0 = t->content[24]; + int rec1val1 = t->content[25]; + + int current_power_raw = (rec1val1*256 + rec1val0)*100; + current_power_ = current_power_raw; + t->addExplanation(full_content, 2, "%02x%02x current power (%d)", + rec1val0, rec1val1, current_power_raw); + } + else { + warning("(multical302) warning: unknown frame %02x\n", frame_type); + } +} + +HeatMeter *createMultical302(WMBus *bus, const char *name, const char *id, const char *key) { + return new MeterMultical302(bus,name,id,key); +} + +void MeterMultical302::printMeterHumanReadable(FILE *output) +{ + fprintf(output, "%s\t%s\t% 3.3f kwh\t% 3.3f m3\t% 3.3f kwh\t%s\n", + name().c_str(), + id().c_str(), + totalPowerConsumption(), + totalVolume(), + currentPowerConsumption(), + datetimeOfUpdateHumanReadable().c_str()); +} + +void MeterMultical302::printMeterFields(FILE *output, char separator) +{ + fprintf(output, "%s%c%s%c%3.3f%c%3.3f%c%3.3f%c%s\n", + name().c_str(), separator, + id().c_str(), separator, + totalPowerConsumption(), separator, + totalVolume(), separator, + currentPowerConsumption(), separator, + datetimeOfUpdateHumanReadable().c_str()); +} + +#define Q(x,y) "\""#x"\":"#y"," +#define QS(x,y) "\""#x"\":\""#y"\"," +#define QSE(x,y) "\""#x"\":\""#y"\"" + +void MeterMultical302::printMeterJSON(FILE *output) +{ + fprintf(output, "{media:\"heat\",meter:\"multical302\"," + QS(name,%s) + QS(id,%s) + Q(total_kwh,%.3f) + Q(total_volume_m3,%.3f) + QS(current_kw,%.3f) + QSE(timestamp,%s) + "}\n", + name().c_str(), + id().c_str(), + totalPowerConsumption(), + totalVolume(), + currentPowerConsumption(), + datetimeOfUpdateRobot().c_str()); +} diff --git a/meters.cc b/meters.cc index a2911c2..2c6caef 100644 --- a/meters.cc +++ b/meters.cc @@ -1,15 +1,15 @@ // Copyright (c) 2017 Fredrik Öhrström -// +// // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: -// +// // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. -// +// // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -19,8 +19,111 @@ // SOFTWARE. #include"meters.h" -#include"wmbus.h" +#include"meters_common_implementation.h" -#include -#include +#include +MeterCommonImplementation::MeterCommonImplementation(WMBus *bus, const char *name, const char *id, const char *key, + MeterType type, int manufacturer, int media) : + type_(type), manufacturer_(manufacturer), media_(media), name_(name), bus_(bus) +{ + use_aes_ = true; + hex2bin(id, &id_); + if (strlen(key) == 0) { + use_aes_ = false; + } else { + hex2bin(key, &key_); + } +} + +MeterType MeterCommonImplementation::type() +{ + return type_; +} + +int MeterCommonImplementation::manufacturer() +{ + return manufacturer_; +} + +int MeterCommonImplementation::media() +{ + return media_; +} + +string MeterCommonImplementation::id() +{ + return bin2hex(id_); +} + +string MeterCommonImplementation::name() +{ + return name_; +} + +WMBus *MeterCommonImplementation::bus() +{ + return bus_; +} + +void MeterCommonImplementation::onUpdate(function cb) +{ + on_update_.push_back(cb); +} + +int MeterCommonImplementation::numUpdates() +{ + return num_updates_; +} + +string MeterCommonImplementation::datetimeOfUpdateHumanReadable() +{ + char datetime[40]; + memset(datetime, 0, sizeof(datetime)); + strftime(datetime, 20, "%Y-%m-%d %H:%M.%S", localtime(&datetime_of_update_)); + return string(datetime); +} + +string MeterCommonImplementation::datetimeOfUpdateRobot() +{ + char datetime[40]; + memset(datetime, 0, sizeof(datetime)); + // This is the date time in the Greenwich timezone (Zulu time), dont get surprised! + strftime(datetime, sizeof(datetime), "%FT%TZ", gmtime(&datetime_of_update_)); + return string(datetime); +} + +MeterType toMeterType(const char *type) +{ + if (!strcmp(type, "multical21")) return MULTICAL21_METER; + if (!strcmp(type, "multical302")) return MULTICAL302_METER; + return UNKNOWN_METER; +} + + +bool MeterCommonImplementation::isTelegramForMe(Telegram *t) +{ + return t->m_field == manufacturer_ && + t->a_field_address[3] == id_[3] && + t->a_field_address[2] == id_[2] && + t->a_field_address[1] == id_[1] && + t->a_field_address[0] == id_[0]; +} + +bool MeterCommonImplementation::useAes() +{ + return use_aes_; +} + +vector MeterCommonImplementation::key() +{ + return key_; +} + +void MeterCommonImplementation::triggerUpdate(Telegram *t) +{ + datetime_of_update_ = time(NULL); + num_updates_++; + for (auto &cb : on_update_) if (cb) cb(this); + t->handled = true; +} diff --git a/meters.h b/meters.h index 5f2e194..84175b3 100644 --- a/meters.h +++ b/meters.h @@ -1,15 +1,15 @@ // Copyright (c) 2017 Fredrik Öhrström -// +// // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: -// +// // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. -// +// // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -27,6 +27,14 @@ #include #include +#define LIST_OF_METERS X(MULTICAL21_METER)X(MULTICAL302_METER)X(UNKNOWN_METER) + +enum MeterType { +#define X(name) name, +LIST_OF_METERS +#undef X +}; + using namespace std; typedef unsigned char uchar; @@ -34,13 +42,33 @@ typedef unsigned char uchar; struct Meter { virtual string id() = 0; virtual string name() = 0; + virtual MeterType type() = 0; + virtual int manufacturer() = 0; + virtual int media() = 0; + virtual WMBus *bus() = 0; - virtual float totalWaterConsumption() = 0; + virtual string datetimeOfUpdateHumanReadable() = 0; + virtual string datetimeOfUpdateRobot() = 0; + + virtual void onUpdate(function cb) = 0; + virtual int numUpdates() = 0; + + virtual void printMeterHumanReadable(FILE *output) = 0; + virtual void printMeterFields(FILE *output, char separator) = 0; + virtual void printMeterJSON(FILE *output) = 0; + + virtual bool isTelegramForMe(Telegram *t) = 0; + virtual bool useAes() = 0; + virtual vector key() = 0; +}; + +struct WaterMeter : public virtual Meter { + virtual float totalWaterConsumption() = 0; // m3 virtual bool hasTotalWaterConsumption() = 0; - virtual float targetWaterConsumption() = 0; + virtual float targetWaterConsumption() = 0; // m3 virtual bool hasTargetWaterConsumption() = 0; virtual float maxFlow() = 0; - virtual bool hasMaxFlow() = 0; + virtual bool hasMaxFlow() = 0; virtual string statusHumanReadable() = 0; virtual string status() = 0; @@ -48,14 +76,17 @@ struct Meter { virtual string timeReversed() = 0; virtual string timeLeaking() = 0; virtual string timeBursting() = 0; - - virtual string datetimeOfUpdateHumanReadable() = 0; - virtual string datetimeOfUpdateRobot() = 0; - - virtual void onUpdate(function cb) = 0; - virtual int numUpdates() = 0; }; -Meter *createMultical21(WMBus *bus, const char *name, const char *id, const char *key); +struct HeatMeter : public virtual Meter { + virtual float totalPowerConsumption() = 0; // kwh + virtual float currentPowerConsumption() = 0; // kw + virtual float totalVolume() = 0; // m3 +}; + + +MeterType toMeterType(const char *type); +WaterMeter *createMultical21(WMBus *bus, const char *name, const char *id, const char *key); +HeatMeter *createMultical302(WMBus *bus, const char *name, const char *id, const char *key); #endif diff --git a/meters_common_implementation.h b/meters_common_implementation.h new file mode 100644 index 0000000..1622a6a --- /dev/null +++ b/meters_common_implementation.h @@ -0,0 +1,66 @@ +// Copyright (c) 2017 Fredrik Öhrström +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +#ifndef METERS_COMMON_IMPLEMENTATION_H_ +#define METERS_COMMON_IMPLEMENTATION_H_ + +#include"meters.h" + +struct MeterCommonImplementation : public virtual Meter { + string id(); + string name(); + MeterType type(); + int manufacturer(); + int media(); + WMBus *bus(); + + string datetimeOfUpdateHumanReadable(); + string datetimeOfUpdateRobot(); + + void onUpdate(function cb); + int numUpdates(); + + bool isTelegramForMe(Telegram *t); + bool useAes(); + vector key(); + + MeterCommonImplementation(WMBus *bus, const char *name, const char *id, const char *key, + MeterType type, int manufacturer, int media); + +protected: + + void triggerUpdate(Telegram *t); + +private: + + MeterType type_ {}; + int manufacturer_ {}; + int media_ {}; + string name_; + vector id_; + vector key_; + WMBus *bus_ {}; + vector> on_update_; + int num_updates_ {}; + bool use_aes_ {}; + time_t datetime_of_update_ {}; +}; + +#endif diff --git a/printer.cc b/printer.cc index c03ef4f..b20ada8 100644 --- a/printer.cc +++ b/printer.cc @@ -1,15 +1,15 @@ // Copyright (c) 2017 Fredrik Öhrström -// +// // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: -// +// // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. -// +// // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -22,16 +22,18 @@ using namespace std; -Printer::Printer(bool robot, bool meterfiles) +Printer::Printer(bool json, bool fields, char separator, bool meterfiles) { - robot_ = robot; + json_ = json; + fields_ = fields; + separator_ = separator; meterfiles_ = meterfiles; } -void Printer::print(Meter *meter) +void Printer::print(Meter *meter) { FILE *output = stdout; - + if (meterfiles_) { char filename[128]; memset(filename, 0, sizeof(filename)); @@ -39,51 +41,17 @@ void Printer::print(Meter *meter) output = fopen(filename, "w"); } - if (robot_) printMeterJSON(output, meter); - else printMeterHumanReadable(output, meter); - + if (json_) { + meter->printMeterJSON(output); + } + else if (fields_) { + meter->printMeterFields(output, separator_); + } + else { + meter->printMeterHumanReadable(output); + } + if (output != stdout) { fclose(output); - } -} - -void Printer::printMeterHumanReadable(FILE *output, Meter *meter) -{ - fprintf(output, "%s\t%s\t% 3.3f m3\t%s\t% 3.3f m3\t%s\n", - meter->name().c_str(), - meter->id().c_str(), - meter->totalWaterConsumption(), - meter->datetimeOfUpdateHumanReadable().c_str(), - meter->targetWaterConsumption(), - meter->statusHumanReadable().c_str()); -} - -#define Q(x,y) "\""#x"\":"#y"," -#define QS(x,y) "\""#x"\":\""#y"\"," -#define QSE(x,y) "\""#x"\":\""#y"\"" - -void Printer::printMeterJSON(FILE *output, Meter *meter) -{ - fprintf(output, "{" - QS(name,%s) - QS(id,%s) - Q(total_m3,%.3f) - Q(target_m3,%.3f) - QS(current_status,%s) - QS(time_dry,%s) - QS(time_reversed,%s) - QS(time_leaking,%s) - QS(time_bursting,%s) - QSE(timestamp,%s) - "}\n", - meter->name().c_str(), - meter->id().c_str(), - meter->totalWaterConsumption(), - meter->targetWaterConsumption(), - meter->status().c_str(), // DRY REVERSED LEAK BURST - meter->timeDry().c_str(), - meter->timeReversed().c_str(), - meter->timeLeaking().c_str(), - meter->timeBursting().c_str(), - meter->datetimeOfUpdateRobot().c_str()); + } } diff --git a/printer.h b/printer.h index f58f907..38506e5 100644 --- a/printer.h +++ b/printer.h @@ -1,15 +1,15 @@ // Copyright (c) 2017 Fredrik Öhrström -// +// // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: -// +// // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. -// +// // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -24,14 +24,12 @@ using namespace std; struct Printer { - Printer(bool robot, bool meterfiles); + Printer(bool json, bool fields, char separator, bool meterfiles); void print(Meter *meter); private: - bool robot_, meterfiles_; - - void printMeterHumanReadable(FILE *output, Meter *meter); - void printMeterJSON(FILE *output, Meter *meter); + bool json_, fields_, meterfiles_; + char separator_; }; diff --git a/serial.cc b/serial.cc index 9d91856..a78ee6c 100644 --- a/serial.cc +++ b/serial.cc @@ -254,7 +254,7 @@ void *SerialCommunicationManagerImp::eventLoop() { if (!running_) break; if (activity < 0 && errno!=EINTR) { - error("(serial) internal error after select! errno=%d\n", errno); + warning("(serial) internal error after select! errno=%s\n", strerror(errno)); } if (activity > 0) { for (SerialDevice *d : devices_) { diff --git a/simulation.txt b/simulation.txt new file mode 100644 index 0000000..d3d72c7 --- /dev/null +++ b/simulation.txt @@ -0,0 +1,20 @@ +# Test Multical21 C1 telegrams + +# short telegram +telegram=|23442D2C998734761B168D2093E13CBA20|967F79EDA8047B7100F4180000E918| +{media:"cold water",meter:"multical21","name":"MyTapWater","id":"76348799","total_m3":6.388,"target_m3":6.377,"current_status":"DRY","time_dry":"22-31 days","time_reversed":"","time_leaking":"","time_bursting":"","timestamp":"1111-11-11T11:11:11Z"} + +# full telegram +telegram=|2A442D2C998734761B168D2049F03FBA20|39A17802FF2071000413F41800004413E9180000615B| +{media:"cold water",meter:"multical21","name":"MyTapWater","id":"76348799","total_m3":6.388,"target_m3":6.377,"current_status":"DRY","time_dry":"22-31 days","time_reversed":"","time_leaking":"","time_bursting":"","timestamp":"1111-11-11T11:11:11Z"} + +# Test Multical302 C1 telegrams + +# short telegram, this is not a proper telegram! Please provide the output from --logtelegrams for a Multical302 meter! + +telegram=|25442D2C785634121b048D2093E13CBA20|0000790000000000000000000000000000| +{media:"heat",meter:"multical302","name":"MyHeater","id":"12345678","total_kwh":0.000,"total_volume_m3":0.000,"current_kw":"0.000","timestamp":"1111-11-11T11:11:11Z"} + +# full telegram + +# Test Multical302 T1 telegrams diff --git a/test.sh b/test.sh new file mode 100755 index 0000000..7373cc7 --- /dev/null +++ b/test.sh @@ -0,0 +1,20 @@ +#!/bin/bash + +PROG="$1" + +cat simulation.txt | grep '^{' > test_expected.txt +$PROG --robot=json simulation.txt \ + MyTapWater multical21 76348799 "" \ + MyHeater multical302 12345678 "" \ + > test_output.txt +if [ "$?" == "0" ] +then + cat test_output.txt | sed 's/"timestamp":"....-..-..T..:..:..Z"/"timestamp":"1111-11-11T11:11:11Z"/' > test_responses.txt + diff test_expected.txt test_responses.txt + if [ "$?" == "0" ] + then + echo OK + fi +else + Failure. +fi diff --git a/util.cc b/util.cc index 4f0f826..eebbe37 100644 --- a/util.cc +++ b/util.cc @@ -121,6 +121,7 @@ void error(const char* fmt, ...) { bool warning_enabled_ = true; bool verbose_enabled_ = false; bool debug_enabled_ = false; +bool log_telegrams_enabled_ = false; void warningSilenced(bool b) { warning_enabled_ = !b; @@ -135,6 +136,10 @@ void debugEnabled(bool b) { if (debug_enabled_) verbose_enabled_ = true; } +void logTelegramsEnabled(bool b) { + log_telegrams_enabled_ = b; +} + bool isVerboseEnabled() { return verbose_enabled_; } @@ -143,6 +148,10 @@ bool isDebugEnabled() { return debug_enabled_; } +bool isLogTelegramsEnabled() { + return log_telegrams_enabled_; +} + void warning(const char* fmt, ...) { if (warning_enabled_) { va_list args; @@ -170,6 +179,13 @@ void debug(const char* fmt, ...) { } } +bool isValidType(char *type) +{ + if (!strcmp(type, "multical21")) return true; + if (!strcmp(type, "multical302")) return true; + return false; +} + bool isValidId(char *id) { if (strlen(id) == 0) return true; @@ -224,11 +240,77 @@ bool checkCharacterDeviceExists(const char *tty, bool fail_if_not) return true; } +bool checkIfSimulationFile(const char *file) +{ + struct stat info; + + int rc = stat(file, &info); + if (rc != 0) { + return false; + } + if (!S_ISREG(info.st_mode)) { + return false; + } + if (strncmp(file, "simulation", 10)) { + return false; + } + return true; +} + void debugPayload(string intro, vector &payload) { if (isDebugEnabled()) { string msg = bin2hex(payload); - debug("%s payload \"%s\"\n", intro.c_str(), msg.c_str()); + debug("%s \"%s\"\n", intro.c_str(), msg.c_str()); + } +} + +void logTelegram(string intro, vector &header, vector &content) +{ + if (isLogTelegramsEnabled()) + { + string h = bin2hex(header); + string cntnt = bin2hex(content); + printf("%s \"telegram=|%s|%s|\"\n", intro.c_str(), h.c_str(), cntnt.c_str()); + } +} + +string eatTo(vector &v, vector::iterator &i, int c, size_t max, bool *eof, bool *err) +{ + string s; + + *eof = false; + *err = false; + while (max > 0 && i != v.end() && (c == -1 || *i != c)) + { + s += *i; + i++; + max--; + } + if (c != -1 && *i != c) + { + *err = true; + } + if (i != v.end()) + { + i++; + } + if (i == v.end()) { + *eof = true; + } + return s; +} + +void padWithZeroesTo(vector *content, size_t len, vector *full_content) +{ + if (content->size() < len) { + warning("Padded with zeroes.", (int)len); + size_t old_size = content->size(); + content->resize(len); + for(size_t i = old_size; i < len; ++i) { + (*content)[i] = 0; + } + full_content->insert(full_content->end(), content->begin()+old_size, content->end()); } } diff --git a/util.h b/util.h index bfac6ca..9dde68e 100644 --- a/util.h +++ b/util.h @@ -46,18 +46,26 @@ void warning(const char* fmt, ...); void warningSilenced(bool b); void verboseEnabled(bool b); void debugEnabled(bool b); +void logTelegramsEnabled(bool b); bool isVerboseEnabled(); bool isDebugEnabled(); - +bool isLogTelegramsEnabled(); void debugPayload(std::string intro, std::vector &payload); +void logTelegram(std::string intro, std::vector &header, std::vector &content); +bool isValidType(char *type); bool isValidId(char *id); bool isValidKey(char *key); void incrementIV(uchar *iv, size_t len); bool checkCharacterDeviceExists(const char *tty, bool fail_if_not); +bool checkIfSimulationFile(const char *file); + +std::string eatTo(std::vector &v, std::vector::iterator &i, int c, size_t max, bool *eof, bool *err); + +void padWithZeroesTo(std::vector *content, size_t len, std::vector *full_content); #endif diff --git a/wmbus.cc b/wmbus.cc index 779456d..9c1c3dc 100644 --- a/wmbus.cc +++ b/wmbus.cc @@ -19,6 +19,7 @@ // SOFTWARE. #include"wmbus.h" +#include #include const char *LinkModeNames[] = { @@ -65,7 +66,7 @@ void Telegram::print() { void Telegram::verboseFields() { string man = manufacturerFlag(m_field); - verbose(" %02x%02x%02x%02x C-field=%02x M-field=%04x (%s) A-field-version=%02x A-field-dev-type=%02x (%s) Ci-field=%02x\n", + verbose(" %02x%02x%02x%02x C-field=%02x M-field=%04x (%s) A-field-version=%02x A-field-dev-type=%02x (%s) Ci-field=%02x (%s)", a_field_address[0], a_field_address[1], a_field_address[2], a_field_address[3], c_field, m_field, @@ -73,7 +74,16 @@ void Telegram::verboseFields() { a_field_version, a_field_device_type, deviceType(m_field, a_field_device_type).c_str(), - ci_field); + ci_field, + ciType(ci_field).c_str()); + + if (ci_field == 0x8d) { + verbose(" CC-field=%02x (%s) ACC=%02x SN=%02x%02x%02x%02x", + cc_field, ccType(cc_field).c_str(), + acc, + sn[3],sn[2],sn[1],sn[0]); + } + verbose("\n"); } string manufacturer(int m_field) { @@ -122,6 +132,33 @@ string deviceType(int m_field, int a_field_device_type) { return "Unknown"; } +string mediaType(int m_field, int a_field_device_type) { + switch (a_field_device_type) { + case 0: return "other"; + case 1: return "oil"; + case 2: return "electricity"; + case 3: return "gas"; + case 4: return "heat"; + case 5: return "steam"; + case 6: return "warm water"; + case 7: return "water"; + case 8: return "heat cost"; + case 9: return "compressed air"; + case 0x0a: return "cooling load volume at outlet"; + case 0x0b: return "cooling load volume at inlet"; + case 0x0c: return "heat volume at inlet"; + case 0x0d: return "heat/cooling load"; + case 0x0e: return "bus/system component"; + case 0x0f: return "unknown"; + case 0x15: return "hot water"; + case 0x16: return "cold water"; + case 0x17: return "hot/cold water"; + case 0x18: return "pressure"; + case 0x19: return "a/d converter"; + } + return "Unknown"; +} + bool detectIM871A(string device, SerialCommunicationManager *handler); bool detectAMB8465(string device, SerialCommunicationManager *handler); @@ -145,6 +182,11 @@ pair detectMBusDevice(string device, SerialCommunicationM return { DEVICE_UNKNOWN, "" }; } + if (checkIfSimulationFile(device.c_str())) + { + return { DEVICE_SIMULATOR, device }; + } + // If not auto, then test the device, is it a character device? checkCharacterDeviceExists(device.c_str(), true); @@ -169,3 +211,361 @@ pair detectMBusDevice(string device, SerialCommunicationM } return { DEVICE_UNKNOWN, "" }; } + +string ciType(int ci_field) +{ + if (ci_field >= 0xA0 && ci_field <= 0xB7) { + return "Mfct specific"; + } + switch (ci_field) { + case 0x60: return "COSEM Data sent by the Readout device to the meter with long Transport Layer"; + case 0x61: return "COSEM Data sent by the Readout device to the meter with short Transport Layer"; + case 0x64: return "Reserved for OBIS-based Data sent by the Readout device to the meter with long Transport Layer"; + case 0x65: return "Reserved for OBIS-based Data sent by the Readout device to the meter with short Transport Layer"; + case 0x69: return "EN 13757-3 Application Layer with Format frame and no Transport Layer"; + case 0x6A: return "EN 13757-3 Application Layer with Format frame and with short Transport Layer"; + case 0x6B: return "EN 13757-3 Application Layer with Format frame and with long Transport Layer"; + case 0x6C: return "Clock synchronisation (absolute)"; + case 0x6D: return "Clock synchronisation (relative)"; + case 0x6E: return "Application error from device with short Transport Layer"; + case 0x6F: return "Application error from device with long Transport Layer"; + case 0x70: return "Application error from device without Transport Layer"; + case 0x71: return "Reserved for Alarm Report"; + case 0x72: return "EN 13757-3 Application Layer with long Transport Layer"; + case 0x73: return "EN 13757-3 Application Layer with Compact frame and long Transport Layer"; + case 0x74: return "Alarm from device with short Transport Layer"; + case 0x75: return "Alarm from device with long Transport Layer"; + case 0x78: return "EN 13757-3 Application Layer without Transport Layer (to be defined)"; + case 0x79: return "EN 13757-3 Application Layer with Compact frame and no header"; + case 0x7A: return "EN 13757-3 Application Layer with short Transport Layer"; + case 0x7B: return "EN 13757-3 Application Layer with Compact frame and short header"; + case 0x7C: return "COSEM Application Layer with long Transport Layer"; + case 0x7D: return "COSEM Application Layer with short Transport Layer"; + case 0x7E: return "Reserved for OBIS-based Application Layer with long Transport Layer"; + case 0x7F: return "Reserved for OBIS-based Application Layer with short Transport Layer"; + case 0x80: return "EN 13757-3 Transport Layer (long) from other device to the meter"; + case 0x81: return "Network Layer data"; + case 0x82: return "For future use"; + case 0x83: return "Network Management application"; + case 0x8A: return "EN 13757-3 Transport Layer (short) from the meter to the other device"; + case 0x8B: return "EN 13757-3 Transport Layer (long) from the meter to the other device"; + case 0x8C: return "Extended Link Layer I (2 Byte)"; + case 0x8D: return "Extended Link Layer II (8 Byte)"; + } + return "?"; +} + +void Telegram::addExplanation(vector &payload, int len, const char* fmt, ...) +{ + char buf[1024]; + + buf[1023] = 0; + + va_list args; + va_start(args, fmt); + vsnprintf(buf, 1023, fmt, args); + va_end(args); + + explanations.push_back({parsed.size(), buf}); + parsed.insert(parsed.end(), payload.begin()+parsed.size(), payload.begin()+parsed.size()+len); +} + +void Telegram::parse(vector &frame) +{ + parsed.clear(); + len = frame[0]; + addExplanation(frame, 1, "%02x length (%d bytes)", len, len); + c_field = frame[1]; + addExplanation(frame, 1, "%02x c-field (%s)", c_field, cType(c_field).c_str()); + m_field = frame[3]<<8 | frame[2]; + string man = manufacturerFlag(m_field); + addExplanation(frame, 2, "%02x%02x m-field (%02x=%s)", frame[2], frame[3], m_field, man.c_str()); + a_field.resize(6); + a_field_address.resize(4); + for (int i=0; i<6; ++i) { + a_field[i] = frame[4+i]; + if (i<4) { a_field_address[i] = frame[4+3-i]; } + } + addExplanation(frame, 4, "%02x%02x%02x%02x a-field-addr (%02x%02x%02x%02x)", frame[4], frame[5], frame[6], frame[7], + frame[7], frame[6], frame[5], frame[4]); + + a_field_version = frame[4+4]; + a_field_device_type = frame[4+5]; + addExplanation(frame, 1, "%02x a-field-version", frame[8]); + addExplanation(frame, 1, "%02x a-field-type (%s)", frame[9], deviceType(m_field, a_field_device_type).c_str()); + + ci_field=frame[10]; + addExplanation(frame, 1, "%02x ci-field (%s)", ci_field, ciType(ci_field).c_str()); + + if (ci_field == 0x8d) { + cc_field = frame[11]; + addExplanation(frame, 1, "%02x cc-field (%s)", cc_field, ccType(cc_field).c_str()); + acc = frame[12]; + addExplanation(frame, 1, "%02x acc", acc); + sn[0] = frame[13]; + sn[1] = frame[14]; + sn[2] = frame[15]; + sn[3] = frame[16]; + addExplanation(frame, 4, "%02x%02x%02x%02x sn", sn[0], sn[1], sn[2], sn[3]); + } + + payload.clear(); + payload.insert(payload.end(), frame.begin()+17, frame.end()); + verbose("(wmbus) received telegram"); + verboseFields(); + debugPayload("(wmbus) frame", frame); + debugPayload("(wmbus) payload", payload); + if (isDebugEnabled()) { + explainParse("(wmbus)", 0); + } +} + +void Telegram::explainParse(string intro, int from) +{ + for (auto& p : explanations) { + if (p.first < from) continue; + printf("%s ", intro.c_str()); + for (int i=0; i -#define LIST_OF_LINK_MODES X(LinkModeC1)X(UNKNOWN_LINKMODE) +#define LIST_OF_LINK_MODES X(LinkModeC1)X(LinkModeT1)X(UNKNOWN_LINKMODE) enum LinkMode { #define X(name) name, @@ -48,9 +48,8 @@ LIST_OF_LINK_MODES using namespace std; -//extern const char *LinkModeNames[]; - struct Telegram { + int len; // The length of the telegram, 1 byte. int c_field; // 1 byte (0x44=telegram, no response expected!) int m_field; // Manufacturer 2 bytes vector a_field; // A field 6 bytes @@ -58,15 +57,33 @@ struct Telegram { vector a_field_address; // Address in BCD = 8 decimal 00000000...99999999 digits. int a_field_version; // 1 byte int a_field_device_type; // 1 byte - int ci_field; // 1 byte - vector payload; // All payload data after the ci field byte. + int ci_field; // 1 byte + // When ci_field==0x8d then there are 8 extra header bytes (ELL header?) + int cc_field; // 1 byte + int acc; // 1 byte + uchar sn[4]; // 4 bytes + // That is 6 bytes (not 8), perhaps the next two bytes (the plcrc?) are + // part of this ELL header, even though they are inside the encrypted payload? + + vector parsed; // Parsed fields + vector payload; // To be parsed. + vector content; // Decrypted content. + + bool handled {}; // Set to true, when a meter has accepted the telegram. // The id as written on the physical meter device. string id() { return bin2hex(a_field_address); } + void parse(vector &payload); void print(); void verboseFields(); + + // A vector of indentations and explanations, to be printed + // below the raw data bytes to explain the telegram content. + vector> explanations; + void addExplanation(vector &payload, int len, const char* fmt, ...); + void explainParse(string intro, int from); }; struct WMBus { @@ -76,9 +93,10 @@ struct WMBus { virtual void setLinkMode(LinkMode lm) = 0; virtual void onTelegram(function cb) = 0; virtual SerialDevice *serial() = 0; + virtual void simulate() = 0; }; -#define LIST_OF_MBUS_DEVICES X(DEVICE_IM871A)X(DEVICE_AMB8465)X(DEVICE_UNKNOWN) +#define LIST_OF_MBUS_DEVICES X(DEVICE_IM871A)X(DEVICE_AMB8465)X(DEVICE_SIMULATOR)X(DEVICE_UNKNOWN) enum MBusDeviceType { #define X(name) name, @@ -92,9 +110,18 @@ pair detectMBusDevice(string device, SerialCommunicationM WMBus *openIM871A(string device, SerialCommunicationManager *manager); WMBus *openAMB8465(string device, SerialCommunicationManager *manager); +struct WMBusSimulator; +WMBus *openSimulator(string file, SerialCommunicationManager *manager); string manufacturer(int m_field); string manufacturerFlag(int m_field); -string deviceType(int a_field, int ); +string deviceType(int m_field, int a_field_device_type); +string mediaType(int m_field, int a_field_device_type); +string ciType(int ci_field); +string cType(int c_field); +string ccType(int cc_field); +string difType(int dif); +string vifType(int vif); +string vifeType(int vif, int vife); #endif diff --git a/wmbus_amb8465.cc b/wmbus_amb8465.cc index 6353142..a49e62d 100644 --- a/wmbus_amb8465.cc +++ b/wmbus_amb8465.cc @@ -41,6 +41,7 @@ struct WMBusAmber : public WMBus { void processSerialData(); void getConfiguration(); SerialDevice *serial() { return serial_; } + void simulate() { } WMBusAmber(SerialDevice *serial, SerialCommunicationManager *manager); private: @@ -58,7 +59,7 @@ private: void waitForResponse(); FrameStatus checkAMB8465Frame(vector &data, size_t *frame_length, int *msgid_out, int *payload_len_out, int *payload_offset); - void handleMessage(int msgid, vector &payload); + void handleMessage(int msgid, vector &frame); }; WMBus *openAMB8465(string device, SerialCommunicationManager *manager) @@ -182,7 +183,7 @@ void WMBusAmber::getConfiguration() } void WMBusAmber::setLinkMode(LinkMode lm) { - if (lm != LinkModeC1) { + if (lm != LinkModeC1 && lm != LinkModeT1) { error("LinkMode %d is not implemented\n", (int)lm); } @@ -193,14 +194,18 @@ void WMBusAmber::setLinkMode(LinkMode lm) { msg[1] = CMD_SET_MODE_REQ; sent_command_ = msg[1]; msg[2] = 1; // Len - msg[3] = 0x0E; // Reception of C1 and C2 messages + if (lm == LinkModeC1) { + msg[3] = 0x0E; // Reception of C1 and C2 messages + } else { + msg[3] = 0x05; // T1-Meter + } msg[4] = xorChecksum(msg, 4); verbose("(amb8465) set link mode %02x\n", msg[3]); serial()->send(msg); waitForResponse(); - link_mode_ = LinkModeC1; + link_mode_ = lm; pthread_mutex_unlock(&command_lock_); } @@ -274,7 +279,16 @@ void WMBusAmber::processSerialData() vector payload; if (payload_len > 0) { - payload.insert(payload.end(), read_buffer_.begin()+payload_offset, read_buffer_.begin()+payload_offset+payload_len); + uchar l = payload_len; + int minus = 0; + payload.insert(payload.end(), &l, &l+1); // Re-insert the len byte. + if (msgid == 0) { + // Copy the telegram payload minus 4 bytes at the end. Could these extra bytes be some + // AMB8465 crc/rssi/else specific data that is dependent on the non-volatile + // bit settings in the usb stick? Perhaps. + minus = 4; + } + payload.insert(payload.end(), read_buffer_.begin()+payload_offset, read_buffer_.begin()+payload_offset+payload_len-minus); } read_buffer_.erase(read_buffer_.begin(), read_buffer_.begin()+frame_length); @@ -283,34 +297,18 @@ void WMBusAmber::processSerialData() } } -void WMBusAmber::handleMessage(int msgid, vector &payload) +void WMBusAmber::handleMessage(int msgid, vector &frame) { switch (msgid) { case (0): { Telegram t; - t.c_field = payload[0]; - t.m_field = payload[2]<<8 | payload[1]; - t.a_field.resize(6); - t.a_field_address.resize(4); - for (int i=0; i<6; ++i) { - t.a_field[i] = payload[3+i]; - if (i<4) { t.a_field_address[i] = payload[3+3-i]; } - } - t.a_field_version = payload[3+4]; - t.a_field_device_type=payload[3+5]; - t.ci_field=payload[9]; - t.payload.clear(); - // TODO! Figure out why there are 4 extra bytes at the end which are not part - // of the message. Is it wmbus crc bytes that the imst dongle removes but - // the amber dongle allows to be visible? Or some other config specific setting? - t.payload.insert(t.payload.end(), payload.begin()+10, payload.end()-4); - verbose("(amb8465) received telegram"); - t.verboseFields(); - debugPayload("(amb8465) telegram", t.payload); - + t.parse(frame); for (auto f : telegram_listeners_) { if (f) f(&t); + if (isVerboseEnabled() && !t.handled) { + verbose("(amb8465) telegram ignored by all configured meters!\n"); + } } break; } @@ -319,7 +317,7 @@ void WMBusAmber::handleMessage(int msgid, vector &payload) verbose("(amb8465) set link mode completed\n"); received_command_ = msgid; received_payload_.clear(); - received_payload_.insert(received_payload_.end(), payload.begin(), payload.end()); + received_payload_.insert(received_payload_.end(), frame.begin(), frame.end()); debugPayload("(amb8465) set link mode", received_payload_); sem_post(&command_wait_); break; @@ -329,7 +327,7 @@ void WMBusAmber::handleMessage(int msgid, vector &payload) verbose("(amb8465) get config completed\n"); received_command_ = msgid; received_payload_.clear(); - received_payload_.insert(received_payload_.end(), payload.begin(), payload.end()); + received_payload_.insert(received_payload_.end(), frame.begin(), frame.end()); debugPayload("(amb8465) get config", received_payload_); sem_post(&command_wait_); break; @@ -339,7 +337,7 @@ void WMBusAmber::handleMessage(int msgid, vector &payload) verbose("(amb8465) get device id completed\n"); received_command_ = msgid; received_payload_.clear(); - received_payload_.insert(received_payload_.end(), payload.begin(), payload.end()); + received_payload_.insert(received_payload_.end(), frame.begin(), frame.end()); debugPayload("(amb8465) get device id", received_payload_); sem_post(&command_wait_); break; @@ -347,7 +345,7 @@ void WMBusAmber::handleMessage(int msgid, vector &payload) default: verbose("(amb8465) unhandled device message %d\n", msgid); received_payload_.clear(); - received_payload_.insert(received_payload_.end(), payload.begin(), payload.end()); + received_payload_.insert(received_payload_.end(), frame.begin(), frame.end()); debugPayload("(amb8465) unknown", received_payload_); } } diff --git a/wmbus_im871a.cc b/wmbus_im871a.cc index 0dc327d..ba73de7 100644 --- a/wmbus_im871a.cc +++ b/wmbus_im871a.cc @@ -40,6 +40,7 @@ struct WMBusIM871A : public WMBus { void processSerialData(); SerialDevice *serial() { return serial_; } + void simulate() { } WMBusIM871A(SerialDevice *serial, SerialCommunicationManager *manager); private: @@ -165,6 +166,9 @@ LinkMode WMBusIM871A::getLinkMode() { if (received_payload_[offset] == im871a_C1a) { lm = LinkModeC1; } + if (received_payload_[offset] == im871a_T1) { + lm = LinkModeT1; + } offset++; } if (has_wmbus_c_field) { @@ -245,7 +249,7 @@ LinkMode WMBusIM871A::getLinkMode() { void WMBusIM871A::setLinkMode(LinkMode lm) { - if (lm != LinkModeC1) { + if (lm != LinkModeC1 && lm != LinkModeT1) { error("LinkMode %d is not implemented\n", (int)lm); } pthread_mutex_lock(&command_lock_); @@ -257,7 +261,11 @@ void WMBusIM871A::setLinkMode(LinkMode lm) msg[3] = 3; // Len msg[4] = 0; // Temporary msg[5] = 2; // iff1 bits: Set Radio Mode only - msg[6] = (int)im871a_C1a; + if (lm == LinkModeC1) { + msg[6] = (int)im871a_C1a; + } else { + msg[6] = (int)im871a_T1; + } msg[7] = 0; // iff2 bits: Set nothing verbose("(im871a) set link mode %02x\n", msg[6]); @@ -338,12 +346,19 @@ void WMBusIM871A::processSerialData() string msg = bin2hex(read_buffer_); debug("(im871a) protocol error \"%s\"\n", msg.c_str()); read_buffer_.clear(); - } else + } + else if (status == FullFrame) { vector payload; if (payload_len > 0) { - payload.insert(payload.begin(), read_buffer_.begin()+payload_offset, read_buffer_.begin()+payload_len); + if (endpoint == RADIOLINK_ID && + msgid == RADIOLINK_MSG_WMBUSMSG_IND) + { + uchar l = payload_len; + payload.insert(payload.begin(), &l, &l+1); // Re-insert the len byte. + } + payload.insert(payload.end(), read_buffer_.begin()+payload_offset, read_buffer_.begin()+payload_len); } read_buffer_.erase(read_buffer_.begin(), read_buffer_.begin()+frame_length); @@ -399,25 +414,14 @@ void WMBusIM871A::handleRadioLink(int msgid, vector &payload) case RADIOLINK_MSG_WMBUSMSG_IND: // 0x03 { Telegram t; - t.c_field = payload[0]; - t.m_field = payload[2]<<8 | payload[1]; - t.a_field.resize(6); - t.a_field_address.resize(4); - for (int i=0; i<6; ++i) { - t.a_field[i] = payload[3+i]; - if (i<4) { t.a_field_address[i] = payload[3+3-i]; } - } - t.a_field_version = payload[3+4]; - t.a_field_device_type=payload[3+5]; - t.ci_field=payload[9]; - t.payload.clear(); - t.payload.insert(t.payload.end(), payload.begin()+10, payload.end()); - verbose("(im871a) received telegram "); - t.verboseFields(); - debugPayload("(im871a) telegram", t.payload); + t.parse(payload); + for (auto f : telegram_listeners_) { if (f) f(&t); } + if (isVerboseEnabled() && !t.handled) { + verbose("(im871a) telegram ignored by all configured meters!\n"); + } } break; default: diff --git a/wmbus_simulator.cc b/wmbus_simulator.cc new file mode 100644 index 0000000..6da03af --- /dev/null +++ b/wmbus_simulator.cc @@ -0,0 +1,179 @@ +// Copyright (c) 2018 Fredrik Öhrström +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +#include"wmbus.h" +#include"serial.h" + +#include +#include +#include +#include +#include +#include + +using namespace std; + +struct WMBusSimulator : public WMBus { + bool ping(); + uint32_t getDeviceId(); + LinkMode getLinkMode(); + void setLinkMode(LinkMode lm); + void onTelegram(function cb); + + void processSerialData(); + SerialDevice *serial() { return NULL; } + void simulate(); + + WMBusSimulator(string file, SerialCommunicationManager *manager); + +private: + vector received_payload_; + vector> telegram_listeners_; + + string file_; + SerialCommunicationManager *manager_; + LinkMode link_mode_; + vector lines_; +}; + +int loadFile(string file, vector *lines); + +WMBus *openSimulator(string device, SerialCommunicationManager *manager) +{ + WMBusSimulator *imp = new WMBusSimulator(device, manager); + return imp; +} + +WMBusSimulator::WMBusSimulator(string file, SerialCommunicationManager *manager) + : file_(file), manager_(manager) +{ + vector lines; + loadFile(file, &lines_); +} + +bool WMBusSimulator::ping() { + verbose("(simulator) ping\n"); + verbose("(simulator) pong\n"); + return true; +} + +uint32_t WMBusSimulator::getDeviceId() { + verbose("(simulator) get device info\n"); + verbose("(simulator) device info: 11111111\n"); + return 0x11111111; +} + +LinkMode WMBusSimulator::getLinkMode() { + verbose("(simulator) get link mode\n"); + verbose("(simulator) config: link mode %02x\n", link_mode_); + return link_mode_; +} + +void WMBusSimulator::setLinkMode(LinkMode lm) +{ + if (lm != LinkModeC1 && lm != LinkModeT1) { + error("LinkMode %d is not implemented\n", (int)lm); + } + link_mode_ = lm; + verbose("(simulator) set link mode %02x\n", lm); + verbose("(simulator) set link mode completed\n"); +} + +void WMBusSimulator::onTelegram(function cb) { + telegram_listeners_.push_back(cb); +} + +int loadFile(string file, vector *lines) +{ + char block[32768+1]; + vector buf; + + int fd = open(file.c_str(), O_RDONLY); + if (fd == -1) { + return -1; + } + while (true) { + ssize_t n = read(fd, block, sizeof(block)); + if (n == -1) { + if (errno == EINTR) { + continue; + } + error("Could not read file %s errno=%d\n", file.c_str(), errno); + close(fd); + return -1; + } + buf.insert(buf.end(), block, block+n); + if (n < (ssize_t)sizeof(block)) { + break; + } + } + close(fd); + + bool eof, err; + auto i = buf.begin(); + for (;;) { + string line = eatTo(buf, i, '\n', 32768, &eof, &err); + if (err) { + error("Error parsing simulation file.\n"); + } + if (line.length() > 0) { + lines->push_back(line); + } + if (eof) break; + } + + return 0; +} + +void WMBusSimulator::simulate() +{ + for (auto l : lines_) { + string hex = ""; + if (l.substr(0,9) == "telegram=") { + for (size_t i=9; i payload; + bool ok = hex2bin(hex.c_str(), &payload); + if (!ok) { + error("Not a valid string of hex bytes! \"%s\"\n", l.c_str()); + } + Telegram t; + t.parse(payload); + for (auto f : telegram_listeners_) { + if (f) f(&t); + } + if (isVerboseEnabled() && !t.handled) { + verbose("(wmbus simulator) telegram ignored by all configured meters!\n"); + } + } + manager_->stop(); +} + +bool detectSimulator(string device, SerialCommunicationManager *manager) +{ + return true; +} diff --git a/wmbus_utils.cc b/wmbus_utils.cc new file mode 100644 index 0000000..5e9b98c --- /dev/null +++ b/wmbus_utils.cc @@ -0,0 +1,97 @@ +// Copyright (c) 2018 Fredrik Öhrström +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +#ifndef WMBUS_UTILS_H +#define WMBUS_UTILS_H + +#include"aes.h" +#include"wmbus.h" + +void decryptKamstrupC1(Telegram *t, vector &aeskey) +{ + vector content; + content.insert(content.end(), t->payload.begin(), t->payload.end()); + size_t remaining = content.size(); + if (remaining > 16) remaining = 16; + + uchar iv[16]; + int i=0; + // M-field + iv[i++] = t->m_field&255; iv[i++] = t->m_field>>8; + // A-field + for (int j=0; j<6; ++j) { iv[i++] = t->a_field[j]; } + // CC-field + iv[i++] = t->cc_field; + // SN-field + for (int j=0; j<4; ++j) { iv[i++] = t->sn[j]; } + // FN + iv[i++] = 0; iv[i++] = 0; + // BC + iv[i++] = 0; + + vector ivv(iv, iv+16); + string s = bin2hex(ivv); + debug("(multical21) IV %s\n", s.c_str()); + + uchar xordata[16]; + AES_ECB_encrypt(iv, &aeskey[0], xordata, 16); + + uchar decrypt[16]; + xorit(xordata, &content[0], decrypt, remaining); + + vector dec(decrypt, decrypt+remaining); + debugPayload("(multical21) decrypted", dec); + + if (content.size() > 22) { + warning("(multical21) warning: Received too many bytes of content! " + "Got %zu bytes, expected at most 22.\n", content.size()); + } + if (content.size() > 16) { + // Yay! Lets decrypt a second block. Full frame content is 22 bytes. + // So a second block should enough for everyone! + remaining = content.size()-16; + if (remaining > 16) remaining = 16; // Should not happen. + + incrementIV(iv, sizeof(iv)); + vector ivv2(iv, iv+16); + string s2 = bin2hex(ivv2); + debug("(multical21) IV+1 %s\n", s2.c_str()); + + AES_ECB_encrypt(iv, &aeskey[0], xordata, 16); + + xorit(xordata, &content[16], decrypt, remaining); + + vector dec2(decrypt, decrypt+remaining); + debugPayload("(multical21) decrypted", dec2); + + // Append the second decrypted block to the first. + dec.insert(dec.end(), dec2.begin(), dec2.end()); + } + t->content.clear(); + t->content.insert(t->content.end(), dec.begin(), dec.end()); +} + +string frameTypeKamstrupC1(int ft) { + if (ft == 0x78) return "long frame"; + if (ft == 0x79) return "short frame"; + return "?"; +} + +#endif diff --git a/wmbus_utils.h b/wmbus_utils.h new file mode 100644 index 0000000..ed47b8b --- /dev/null +++ b/wmbus_utils.h @@ -0,0 +1,27 @@ +// Copyright (c) 2017-2018 Fredrik Öhrström +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +#ifndef WMBUS_UTILS_H +#define WMBUS_UTILS_H + +void decryptKamstrupC1(Telegram *t, vector &aeskey); +string frameTypeKamstrupC1(int ft); + +#endif