kopia lustrzana https://github.com/weetmuts/wmbusmeters
Added initial support for Multical302. Not yet complete. Modularized the source and added tests.
rodzic
081e5c2468
commit
6bff880a53
8
CHANGES
8
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:
|
||||
|
||||
|
|
7
Makefile
7
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
|
||||
|
|
31
README.md
31
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.
|
||||
|
||||
|
|
51
cmdline.cc
51
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; 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;
|
||||
|
|
|
@ -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
35
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();
|
||||
}
|
||||
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
|
|
@ -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
115
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<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;
|
||||
}
|
||||
|
|
57
meters.h
57
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<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
|
||||
|
|
|
@ -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
|
72
printer.cc
72
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());
|
||||
}
|
||||
}
|
||||
|
|
14
printer.h
14
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_;
|
||||
};
|
||||
|
|
|
@ -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_) {
|
||||
|
|
|
@ -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
|
|
@ -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
84
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<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
10
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<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
404
wmbus.cc
|
@ -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
41
wmbus.h
|
@ -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
|
||||
|
|
|
@ -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_);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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
|
|
@ -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
|
Ładowanie…
Reference in New Issue