Added initial support for Multical302. Not yet complete. Modularized the source and added tests.

pull/5/head
weetmuts 2018-03-05 11:29:25 +01:00
rodzic 081e5c2468
commit 6bff880a53
25 zmienionych plików z 1674 dodań i 406 usunięć

Wyświetl plik

@ -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:

Wyświetl plik

@ -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

Wyświetl plik

@ -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.

Wyświetl plik

@ -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; m<num_meters; ++m) {
char *name = argv[m*3+i+0];
char *id = argv[m*3+i+1];
char *key = argv[m*3+i+2];
char *name = argv[m*4+i+0];
char *type = argv[m*4+i+1];
char *id = argv[m*4+i+2];
char *key = argv[m*4+i+3];
if (!isValidType(type)) error("Not a valid meter type \"%s\"\n", type);
if (!isValidId(id)) error("Not a valid meter id \"%s\"\n", id);
if (!isValidKey(key)) error("Not a valid meter key \"%s\"\n", key);
c->meters.push_back(MeterInfo(name,id,key));
c->meters.push_back(MeterInfo(name,type,id,key));
}
return c;

Wyświetl plik

@ -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<MeterInfo> meters;

35
main.cc
Wyświetl plik

@ -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();
}

Wyświetl plik

@ -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<memory.h>
@ -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<void(Meter*)> 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<uchar> &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<uchar> id_;
vector<uchar> key_;
WMBus *bus_;
vector<function<void(Meter*)>> 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<void(Meter*)> 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<uchar> 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<uchar> 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<uchar> 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<uchar> 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<uchar> 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<uchar> 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<uchar> &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<uchar> 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());
}

Wyświetl plik

@ -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<memory.h>
#include<stdio.h>
#include<string>
#include<time.h>
#include<vector>
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<uchar> 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<uchar> 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<uchar> 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());
}

115
meters.cc
Wyświetl plik

@ -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<string>
#include<vector>
#include<memory.h>
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<void(Meter*)> 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<uchar> 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;
}

Wyświetl plik

@ -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<string>
#include<vector>
#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<void(Meter*)> 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<uchar> 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<void(Meter*)> 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

Wyświetl plik

@ -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<void(Meter*)> cb);
int numUpdates();
bool isTelegramForMe(Telegram *t);
bool useAes();
vector<uchar> 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<uchar> id_;
vector<uchar> key_;
WMBus *bus_ {};
vector<function<void(Meter*)>> on_update_;
int num_updates_ {};
bool use_aes_ {};
time_t datetime_of_update_ {};
};
#endif

Wyświetl plik

@ -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());
}
}

Wyświetl plik

@ -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_;
};

Wyświetl plik

@ -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_) {

20
simulation.txt 100644
Wyświetl plik

@ -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

20
test.sh 100755
Wyświetl plik

@ -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

84
util.cc
Wyświetl plik

@ -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<uchar> &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<uchar> &header, vector<uchar> &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<uchar> &v, vector<uchar>::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<uchar> *content, size_t len, vector<uchar> *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());
}
}

10
util.h
Wyświetl plik

@ -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<uchar> &payload);
void logTelegram(std::string intro, std::vector<uchar> &header, std::vector<uchar> &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<uchar> &v, std::vector<uchar>::iterator &i, int c, size_t max, bool *eof, bool *err);
void padWithZeroesTo(std::vector<uchar> *content, size_t len, std::vector<uchar> *full_content);
#endif

404
wmbus.cc
Wyświetl plik

@ -19,6 +19,7 @@
// SOFTWARE.
#include"wmbus.h"
#include<stdarg.h>
#include<unistd.h>
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<MBusDeviceType,string> 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<MBusDeviceType,string> 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<uchar> &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<uchar> &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<p.first; ++i) {
printf(" ");
}
printf("%s\n", p.second.c_str());
}
string hex = bin2hex(parsed);
printf("%s %s\n", intro.c_str(), hex.c_str());
}
string cType(int c_field)
{
switch (c_field) {
case 0x44: return "SND_NR";
case 0x46: return "SND_IR";
case 0x48: return "RSP_UD";
}
return "?";
}
string ccType(int cc_field)
{
string s = "";
if (cc_field & CC_B_BIDIRECTIONAL_BIT) s += "bidir ";
if (cc_field & CC_RD_RESPONSE_DELAY_BIT) s += "fast_resp ";
else s += "slow_resp ";
if (cc_field & CC_S_SYNCH_FRAME_BIT) s += "sync ";
if (cc_field & CC_R_RELAYED_BIT) s+= "relayed "; // Relayed by a repeater
if (cc_field & CC_P_HIGH_PRIO_BIT) s+= "prio ";
if (s.back() == ' ') s.pop_back();
return s;
}
string difType(int dif)
{
string s;
int t = dif & 0x0f;
switch (t) {
case 0x0: s+= "No data"; break;
case 0x1: s+= "8 Bit Integer/Binary"; break;
case 0x2: s+= "16 Bit Integer/Binary"; break;
case 0x3: s+= "24 Bit Integer/Binary"; break;
case 0x4: s+= "32 Bit Integer/Binary"; break;
case 0x5: s+= "32 Bit Real"; break;
case 0x6: s+= "48 Bit Integer/Binary"; break;
case 0x7: s+= "64 Bit Integer/Binary"; break;
case 0x8: s+= "Selection for Readout"; break;
case 0x9: s+= "2 digit BCD"; break;
case 0xA: s+= "4 digit BCD"; break;
case 0xB: s+= "6 digit BCD"; break;
case 0xC: s+= "8 digit BCD"; break;
case 0xD: s+= "variable length"; break;
case 0xE: s+= "12 digit BCD"; break;
case 0xF: s+= "Special Functions"; break;
default: s+= "?"; break;
}
t = dif & 0x30;
switch (t) {
case 0x00: s += " Instantaneous value"; break;
case 0x10: s += "Maximum value"; break;
case 0x20: s += "Minimum value"; break;
case 0x30: s+= "Value during error state"; break;
default: s += "?"; break;
}
return s;
}
string vifType(int vif)
{
int extension = vif & 0x80;
int t = vif & 0x7f;
if (extension) {
switch(vif) {
case 0xfb: return "First extension of VIF-codes";
case 0xfd: return "Second extension of VIF-codes";
case 0xef: return "Reserved extension";
}
}
switch (t) {
case 0x00: return "Energy mWh";
case 0x01: return "Energy 10⁻² Wh";
case 0x02: return "Energy 10⁻¹ Wh";
case 0x03: return "Energy Wh";
case 0x04: return "Energy 10¹ Wh";
case 0x05: return "Energy 10² Wh";
case 0x06: return "Energy kWh";
case 0x07: return "Energy 10⁴ Wh";
case 0x08: return "Energy J";
case 0x09: return "Energy 10¹ J";
case 0x0A: return "Energy 10² J";
case 0x0B: return "Energy kJ";
case 0x0C: return "Energy 10⁴ J";
case 0x0D: return "Energy 10⁵ J";
case 0x0E: return "Energy MJ";
case 0x0F: return "Energy 10⁷ J";
case 0x10: return "Volume cm³";
case 0x11: return "Volume 10⁻⁵ m³";
case 0x12: return "Volume 10⁻⁴ m³";
case 0x13: return "Volume l";
case 0x14: return "Volume 10⁻² m³";
case 0x15: return "Volume 10⁻¹ m³";
case 0x16: return "Volume m³";
case 0x17: return "Volume 10¹ m³";
case 0x18: return "Mass g";
case 0x19: return "Mass 10⁻² kg";
case 0x1A: return "Mass 10⁻¹ kg";
case 0x1B: return "Mass kg";
case 0x1C: return "Mass 10¹ kg";
case 0x1D: return "Mass 10² kg";
case 0x1E: return "Mass t";
case 0x1F: return "Mass 10⁴ kg";
case 0x20: return "On time seconds";
case 0x21: return "On time minutes";
case 0x22: return "On time hours";
case 0x23: return "On time days";
case 0x24: return "Operating time seconds";
case 0x25: return "Operating time minutes";
case 0x26: return "Operating time hours";
case 0x27: return "Operating time days";
case 0x28: return "Power mW";
case 0x29: return "Power 10⁻² W";
case 0x2A: return "Power 10⁻¹ W";
case 0x2B: return "Power W";
case 0x2C: return "Power 10¹ W";
case 0x2D: return "Power 10² W";
case 0x2E: return "Power kW";
case 0x2F: return "Power 10⁴ W";
case 0x30: return "Power J/h";
case 0x31: return "Power 10¹ J/h";
case 0x32: return "Power 10² J/h";
case 0x33: return "Power kJ/h";
case 0x34: return "Power 10⁴ J/h";
case 0x35: return "Power 10⁵ J/h";
case 0x36: return "Power MJ/h";
case 0x37: return "Power 10⁷ J/h";
case 0x38: return "Volume flow cm³/h";
case 0x39: return "Volume flow 10⁻⁵ m³/h";
case 0x3A: return "Volume flow 10⁻⁴ m³/h";
case 0x3B: return "Volume flow l/h";
case 0x3C: return "Volume flow 10⁻² m³/h";
case 0x3D: return "Volume flow 10⁻¹ m³/h";
case 0x3E: return "Volume flow m³/h";
case 0x3F: return "Volume flow 10¹ m³/h";
case 0x40: return "Volume flow ext. 10⁻⁷ m³/min";
case 0x41: return "Volume flow ext. cm³/min";
case 0x42: return "Volume flow ext. 10⁻⁵ m³/min";
case 0x43: return "Volume flow ext. 10⁻⁴ m³/min";
case 0x44: return "Volume flow ext. l/min";
case 0x45: return "Volume flow ext. 10⁻² m³/min";
case 0x46: return "Volume flow ext. 10⁻¹ m³/min";
case 0x47: return "Volume flow ext. m³/min";
case 0x48: return "Volume flow ext. mm³/s";
case 0x49: return "Volume flow ext. 10⁻⁸ m³/s";
case 0x4A: return "Volume flow ext. 10⁻⁷ m³/s";
case 0x4B: return "Volume flow ext. cm³/s";
case 0x4C: return "Volume flow ext. 10⁻⁵ m³/s";
case 0x4D: return "Volume flow ext. 10⁻⁴ m³/s";
case 0x4E: return "Volume flow ext. l/s";
case 0x4F: return "Volume flow ext. 10⁻² m³/s";
case 0x50: return "Mass g/h";
case 0x51: return "Mass 10⁻² kg/h";
case 0x52: return "Mass 10⁻¹ kg/h";
case 0x53: return "Mass kg/h";
case 0x54: return "Mass 10¹ kg/h";
case 0x55: return "Mass 10² kg/h";
case 0x56: return "Mass t/h";
case 0x57: return "Mass 10⁴ kg/h";
case 0x58: return "Flow temperature 10⁻³ °C";
case 0x59: return "Flow temperature 10⁻² °C";
case 0x5A: return "Flow temperature 10⁻¹ °C";
case 0x5B: return "Flow temperature °C";
case 0x5C: return "Return temperature 10⁻³ °C";
case 0x5D: return "Return temperature 10⁻² °C";
case 0x5E: return "Return temperature 10⁻¹ °C";
case 0x5F: return "Return temperature °C";
case 0x60: return "Temperature difference mK";
case 0x61: return "Temperature difference 10⁻² K";
case 0x62: return "Temperature difference 10⁻¹ K";
case 0x63: return "Temperature difference K";
case 0x64: return "External temperature 10⁻³ °C";
case 0x65: return "External temperature 10⁻² °C";
case 0x66: return "External temperature 10⁻¹ °C";
case 0x67: return "External temperature °C";
case 0x68: return "Pressure mbar";
case 0x69: return "Pressure 10⁻² bar";
case 0x6A: return "Pressure 10⁻1 bar";
case 0x6B: return "Pressure bar";
case 0x6C: return "Date type G";
case 0x6E: return "Units for H.C.A.";
case 0x6F: return "Reserved";
case 0x70: return "Averaging duration seconds";
case 0x71: return "Averaging duration minutes";
case 0x72: return "Averaging duration hours";
case 0x73: return "Averaging duration days";
case 0x74: return "Actuality duration seconds";
case 0x75: return "Actuality duration minutes";
case 0x76: return "Actuality duration hours";
case 0x77: return "Actuality duration days";
case 0x78: return "Fabrication no";
case 0x79: return "Enhanced identification";
case 0x80: return "Address";
case 0x7C: return "VIF in following string (length in first byte)";
case 0x7E: return "Any VIF";
case 0x7F: return "Manufacturer specific";
default: return "?";
}
}
string vifeType(int vif, int vife)
{
//int extension = vif & 0x80;
//int t = vif & 0x7f;
if (vif == 0xff) {
return "?";
}
return "?";
}

41
wmbus.h
Wyświetl plik

@ -27,7 +27,7 @@
#include<inttypes.h>
#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<uchar> a_field; // A field 6 bytes
@ -58,15 +57,33 @@ struct Telegram {
vector<uchar> 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<uchar> 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<uchar> parsed; // Parsed fields
vector<uchar> payload; // To be parsed.
vector<uchar> 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<uchar> &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<pair<int,string>> explanations;
void addExplanation(vector<uchar> &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<void(Telegram*)> 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<MBusDeviceType,string> 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

Wyświetl plik

@ -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<uchar> &data,
size_t *frame_length, int *msgid_out, int *payload_len_out, int *payload_offset);
void handleMessage(int msgid, vector<uchar> &payload);
void handleMessage(int msgid, vector<uchar> &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<uchar> 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<uchar> &payload)
void WMBusAmber::handleMessage(int msgid, vector<uchar> &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<uchar> &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<uchar> &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<uchar> &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<uchar> &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_);
}
}

Wyświetl plik

@ -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<uchar> 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<uchar> &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:

179
wmbus_simulator.cc 100644
Wyświetl plik

@ -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<assert.h>
#include<fcntl.h>
#include<pthread.h>
#include<semaphore.h>
#include<sys/types.h>
#include<unistd.h>
using namespace std;
struct WMBusSimulator : public WMBus {
bool ping();
uint32_t getDeviceId();
LinkMode getLinkMode();
void setLinkMode(LinkMode lm);
void onTelegram(function<void(Telegram*)> cb);
void processSerialData();
SerialDevice *serial() { return NULL; }
void simulate();
WMBusSimulator(string file, SerialCommunicationManager *manager);
private:
vector<uchar> received_payload_;
vector<function<void(Telegram*)>> telegram_listeners_;
string file_;
SerialCommunicationManager *manager_;
LinkMode link_mode_;
vector<string> lines_;
};
int loadFile(string file, vector<string> *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<string> 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<void(Telegram*)> cb) {
telegram_listeners_.push_back(cb);
}
int loadFile(string file, vector<string> *lines)
{
char block[32768+1];
vector<uchar> 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<l.length(); ++i) {
if (l[i] == '|') continue;
hex += l[i];
}
verbose("(simulator) from file \"%s\"\n", hex.c_str());
} else {
continue;
}
vector<uchar> 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;
}

97
wmbus_utils.cc 100644
Wyświetl plik

@ -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<uchar> &aeskey)
{
vector<uchar> 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<uchar> 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<uchar> 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<uchar> 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<uchar> 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

27
wmbus_utils.h 100644
Wyświetl plik

@ -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<uchar> &aeskey);
string frameTypeKamstrupC1(int ft);
#endif