Initial support for electricity meters.

pull/5/head
weetmuts 2018-03-16 11:48:04 +01:00
rodzic e9cb0322f0
commit 6cc37271dc
15 zmienionych plików z 338 dodań i 53 usunięć

Wyświetl plik

@ -39,6 +39,7 @@ METERS_OBJS:=\
$(BUILD)/meters.o \
$(BUILD)/meter_multical21.o \
$(BUILD)/meter_multical302.o \
$(BUILD)/meter_omnipower.o \
$(BUILD)/printer.o \
$(BUILD)/serial.o \
$(BUILD)/util.o \

Wyświetl plik

@ -26,14 +26,32 @@ Add --verbose for detailed debug information.
Specifying auto as the device will automatically look for usb
wmbus dongles on /dev/im871a and /dev/amb8465.
Two meter types are supported: multical21 and multical302 (multical302 is still work in progress).
The meter type: multical21 (a water meter) is supported.
The meter types: multical302 (heat) and omnipower (electricity)
are work in progress.
```
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, where wrong
values might be printed instead!).
If this happens to someone, then we need to implement a way to pass the meter configuration as a parameter.
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, where wrong values might be printed instead!). If
this happens to someone, then we need to implement a way to pass the
meter configuration as a parameter.
Actually, the mbus (and consequently the wmbus) protocol is a standard
that is self-describing. Thus in reality it should not be necessary
to supply exactly which kind of meter we expect for a given id. This
should be possible to figure out when we receive the first telegram.
Thus, strictly speaking, it should not be necessary to specify the
exact meter type. A more generic meter type might be just "water",
"heat" or electricity. But for the moment, the separation of meter
types will remain in the code. Thus even though the meter type right
now is named multical302, the other heat meters (multical-402 and
multical-602) might be compatible as well. The same is true for the
omnipower meter type, which might include the electricity meters
Kamstrup-162 Kamstrup-382, Kamstrup-351 etc).
No meter quadruplets means listen for telegram traffic and print any id heard.

Wyświetl plik

@ -19,6 +19,7 @@
// SOFTWARE.
#include"cmdline.h"
#include"meters.h"
#include"util.h"
using namespace std;
@ -142,7 +143,9 @@ CommandLine *parseCommandLine(int argc, char **argv) {
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);
MeterType mt = toMeterType(type);
if (mt == UNKNOWN_METER) 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,type,id,key));

Wyświetl plik

@ -51,7 +51,9 @@ int main(int argc, char **argv)
" or 10m for ten minutes or 5s for five seconds.\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");
printf("The meter type: multical21 (a water meter) is supported.\n"
"The meter types: multical302 (heat) and omnipower (electricity)\n"
"are work in progress.\n\n");
exit(0);
}
// We want the data visible in the log file asap!
@ -116,6 +118,10 @@ int main(int argc, char **argv)
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 OMNIPOWER_METER:
m.meter = createOmnipower(wmbus, m.name, m.id, m.key);
verbose("(omnipower) configured \"%s\" \"omnipower\" \"%s\" \"%s\"\n", m.name, m.id, m.key);
break;
case UNKNOWN_METER:
error("No such meter type \"%s\"\n", m.type);
break;

Wyświetl plik

@ -185,7 +185,8 @@ void MeterMultical21::processContent(Telegram *t) {
int crc0 = t->content[0];
int crc1 = t->content[1];
t->addExplanation(full_content, 2, "%02x%02x plcrc", crc0, crc1);
t->addExplanation(full_content, 2, "%02x%02x payload crc %02x%02x", crc0, crc1);
int frame_type = t->content[2];
t->addExplanation(full_content, 1, "%02x frame type (%s)", frame_type, frameTypeKamstrupC1(frame_type).c_str());

Wyświetl plik

@ -33,7 +33,7 @@
struct MeterMultical302 : public virtual HeatMeter, public virtual MeterCommonImplementation {
MeterMultical302(WMBus *bus, const char *name, const char *id, const char *key);
float totalPowerConsumption();
float totalEnergyConsumption();
float currentPowerConsumption();
float totalVolume();
@ -45,7 +45,7 @@ private:
void handleTelegram(Telegram *t);
void processContent(Telegram *t);
float total_power_ {};
float total_energy_ {};
float current_power_ {};
float total_volume_ {};
};
@ -56,9 +56,9 @@ MeterMultical302::MeterMultical302(WMBus *bus, const char *name, const char *id,
MeterCommonImplementation::bus()->onTelegram(calll(this,handleTelegram,Telegram*));
}
float MeterMultical302::totalPowerConsumption()
float MeterMultical302::totalEnergyConsumption()
{
return total_power_;
return total_energy_;
}
float MeterMultical302::currentPowerConsumption()
@ -117,7 +117,7 @@ void MeterMultical302::processContent(Telegram *t) {
int crc0 = t->content[0];
int crc1 = t->content[1];
t->addExplanation(full_content, 2, "%02x%02x plcrc", crc0, crc1);
t->addExplanation(full_content, 2, "%02x%02x payload crc", crc0, crc1);
int frame_type = t->content[2];
t->addExplanation(full_content, 1, "%02x frame type (%s)", frame_type, frameTypeKamstrupC1(frame_type).c_str());
@ -136,10 +136,10 @@ void MeterMultical302::processContent(Telegram *t) {
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;
int total_energy_raw = rec1val2*256*256 + rec1val1*256 + rec1val0;
total_energy_ = total_energy_raw;
t->addExplanation(full_content, 3, "%02x%02x%02x total power (%d)",
rec1val0, rec1val1, rec1val2, total_power_raw);
rec1val0, rec1val1, rec1val2, total_energy_raw);
int rec2val0 = t->content[13];
int rec2val1 = t->content[14];
@ -185,7 +185,7 @@ 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(),
totalEnergyConsumption(),
totalVolume(),
currentPowerConsumption(),
datetimeOfUpdateHumanReadable().c_str());
@ -196,7 +196,7 @@ 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,
totalEnergyConsumption(), separator,
totalVolume(), separator,
currentPowerConsumption(), separator,
datetimeOfUpdateRobot().c_str());
@ -218,7 +218,7 @@ void MeterMultical302::printMeterJSON(FILE *output)
"}\n",
name().c_str(),
id().c_str(),
totalPowerConsumption(),
totalEnergyConsumption(),
totalVolume(),
currentPowerConsumption(),
datetimeOfUpdateRobot().c_str());

171
meter_omnipower.cc 100644
Wyświetl plik

@ -0,0 +1,171 @@
// 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 MeterOmnipower : public virtual ElectricityMeter, public virtual MeterCommonImplementation {
MeterOmnipower(WMBus *bus, const char *name, const char *id, const char *key);
float totalEnergyConsumption();
float currentPowerConsumption();
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_energy_ {};
float current_power_ {};
};
MeterOmnipower::MeterOmnipower(WMBus *bus, const char *name, const char *id, const char *key) :
MeterCommonImplementation(bus, name, id, key, OMNIPOWER_METER, MANUFACTURER_KAM, 0x04)
{
MeterCommonImplementation::bus()->onTelegram(calll(this,handleTelegram,Telegram*));
}
float MeterOmnipower::totalEnergyConsumption()
{
return total_energy_;
}
float MeterOmnipower::currentPowerConsumption()
{
return current_power_;
}
void MeterOmnipower::handleTelegram(Telegram *t) {
if (!isTelegramForMe(t)) {
// This telegram is not intended for this meter.
return;
}
verbose("(omnipower) %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 != 0x02) {
warning("(omnipower) expected telegram for electricity 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 != 0x01) {
warning("(omnipower) expected telegram from KAM meter with version 0x01, but got \"%s\" version 0x2x !\n",
manufacturerFlag(t->m_field).c_str(), t->a_field_version);
}
if (useAes()) {
vector<uchar> aeskey = key();
// Proper decryption not yet implemented!
decryptKamstrupC1(t, aeskey);
} else {
t->content = t->payload;
}
logTelegram("(omnipower) log", t->parsed, t->content);
int content_start = t->parsed.size();
processContent(t);
if (isDebugEnabled()) {
t->explainParse("(omnipower)", content_start);
}
triggerUpdate(t);
}
void MeterOmnipower::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 rec1dif = t->content[0];
t->addExplanation(full_content, 1, "%02x dif (%s)", rec1dif, difType(rec1dif).c_str());
int rec1vif = t->content[1];
t->addExplanation(full_content, 1, "%02x vif (%s)", rec1vif, vifType(rec1vif).c_str());
int rec1vife = t->content[2];
t->addExplanation(full_content, 1, "%02x vife (%s)", rec1vife, vifeType(rec1vif, rec1vife).c_str());
int rec1val0 = t->content[3];
int rec1val1 = t->content[4];
int rec1val2 = t->content[5];
int rec1val3 = t->content[6];
int total_energy_raw = rec1val3*256*256*256 + rec1val2*256*256 + rec1val1*256 + rec1val0;
total_energy_ = ((float)total_energy_raw)/1000.0;
t->addExplanation(full_content, 4, "%02x%02x%02x%02x total power (%d)",
rec1val0, rec1val1, rec1val2, rec1val3, total_energy_raw);
}
ElectricityMeter *createOmnipower(WMBus *bus, const char *name, const char *id, const char *key) {
return new MeterOmnipower(bus,name,id,key);
}
void MeterOmnipower::printMeterHumanReadable(FILE *output)
{
fprintf(output, "%s\t%s\t% 3.3f kwh\t% 3.3f kwh\t%s\n",
name().c_str(),
id().c_str(),
totalEnergyConsumption(),
currentPowerConsumption(),
datetimeOfUpdateHumanReadable().c_str());
}
void MeterOmnipower::printMeterFields(FILE *output, char separator)
{
fprintf(output, "%s%c%s%c%3.3f%c%3.3f%c%s\n",
name().c_str(), separator,
id().c_str(), separator,
totalEnergyConsumption(), separator,
currentPowerConsumption(), separator,
datetimeOfUpdateRobot().c_str());
}
#define Q(x,y) "\""#x"\":"#y","
#define QS(x,y) "\""#x"\":\""#y"\","
#define QSE(x,y) "\""#x"\":\""#y"\""
void MeterOmnipower::printMeterJSON(FILE *output)
{
fprintf(output, "{media:\"electricity\",meter:\"omnipower\","
QS(name,%s)
QS(id,%s)
Q(total_kwh,%.3f)
QS(current_kw,%.3f)
QSE(timestamp,%s)
"}\n",
name().c_str(),
id().c_str(),
totalEnergyConsumption(),
currentPowerConsumption(),
datetimeOfUpdateRobot().c_str());
}

Wyświetl plik

@ -97,6 +97,10 @@ MeterType toMeterType(const char *type)
{
if (!strcmp(type, "multical21")) return MULTICAL21_METER;
if (!strcmp(type, "multical302")) return MULTICAL302_METER;
if (!strcmp(type, "omnipower")) return OMNIPOWER_METER;
if (!strcmp(type, "water")) return MULTICAL21_METER;
if (!strcmp(type, "heat")) return MULTICAL302_METER;
if (!strcmp(type, "electricity")) return OMNIPOWER_METER;
return UNKNOWN_METER;
}

Wyświetl plik

@ -1,4 +1,4 @@
// Copyright (c) 2017 Fredrik Öhrström
// 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
@ -27,7 +27,7 @@
#include<string>
#include<vector>
#define LIST_OF_METERS X(MULTICAL21_METER)X(MULTICAL302_METER)X(UNKNOWN_METER)
#define LIST_OF_METERS X(MULTICAL21_METER)X(MULTICAL302_METER)X(OMNIPOWER_METER)X(UNKNOWN_METER)
enum MeterType {
#define X(name) name,
@ -79,14 +79,20 @@ struct WaterMeter : public virtual Meter {
};
struct HeatMeter : public virtual Meter {
virtual float totalPowerConsumption() = 0; // kwh
virtual float totalEnergyConsumption() = 0; // kwh
virtual float currentPowerConsumption() = 0; // kw
virtual float totalVolume() = 0; // m3
};
struct ElectricityMeter : public virtual Meter {
virtual float totalEnergyConsumption() = 0; // kwh
virtual float currentPowerConsumption() = 0; // kw
};
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);
ElectricityMeter *createOmnipower(WMBus *bus, const char *name, const char *id, const char *key);
#endif

Wyświetl plik

@ -18,3 +18,8 @@ telegram=|25442D2C785634121b048D2093E13CBA20|0000790000000000000000000000000000|
# full telegram
# Test Multical302 T1 telegrams
# Test Omnipower C1 telegrams
telegram=|1E442D2C0771941501027A|B300108504833B08340500|
{media:"electricity",meter:"omnipower","name":"MyElectricity","id":"15947107","total_kwh":341.000,"current_kw":"0.000","timestamp":"1111-11-11T11:11:11Z"}

Wyświetl plik

@ -4,8 +4,9 @@ PROG="$1"
cat simulation.txt | grep '^{' > test_expected.txt
$PROG --robot=json simulation.txt \
MyTapWater multical21 76348799 "" \
MyHeater multical302 12345678 "" \
MyTapWater water 76348799 "" \
MyHeater heat 12345678 "" \
MyElectricity electricity 15947107 "" \
> test_output.txt
if [ "$?" == "0" ]
then

38
util.cc
Wyświetl plik

@ -185,13 +185,6 @@ 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;
@ -359,3 +352,34 @@ int parseTime(string time) {
int n = atoi(time.c_str());
return n*mul;
}
#define CRC16_EN_13757 0x3D65
uint16_t crc16_EN13757_per_byte(uint16_t crc, uchar b)
{
unsigned char i;
for (i = 0; i < 8; i++) {
if (((crc & 0x8000) >> 8) ^ (b & 0x80)){
crc = (crc << 1) ^ CRC16_EN_13757;
}else{
crc = (crc << 1);
}
b <<= 1;
}
return crc;
}
uint16_t crc16_EN13757(uchar *data, size_t len)
{
uint16_t crc = 0x0000;
for (size_t i=0; i<len; ++i) {
crc = crc16_EN13757_per_byte(crc, data[i]);
}
return (~crc);
}

3
util.h
Wyświetl plik

@ -55,7 +55,6 @@ 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);
@ -71,4 +70,6 @@ void padWithZeroesTo(std::vector<uchar> *content, size_t len, std::vector<uchar>
int parseTime(std::string time);
uint16_t crc_16_EN_13757(uchar *data, size_t len);
#endif

Wyświetl plik

@ -77,12 +77,29 @@ void Telegram::verboseFields() {
ci_field,
ciType(ci_field).c_str());
if (ci_field == 0x78) {
// No data error and no encryption possible.
}
if (ci_field == 0x7a) {
// Short data header
verbose(" CC-field=%02x (%s) ACC=%02x ",
cc_field, ccType(cc_field).c_str(),
acc,
sn[3],sn[2],sn[1],sn[0]);
}
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]);
}
if (ci_field == 0x8c) {
verbose(" CC-field=%02x (%s) ACC=%02x",
cc_field, ccType(cc_field).c_str(),
acc);
}
verbose("\n");
}
@ -297,20 +314,39 @@ void Telegram::parse(vector<uchar> &frame)
ci_field=frame[10];
addExplanation(frame, 1, "%02x ci-field (%s)", ci_field, ciType(ci_field).c_str());
if (ci_field == 0x8d) {
int header_size = 0;
if (ci_field == 0x78) {
header_size = 0; // And no encryption possible.
} else
if (ci_field == 0x7a) {
acc = frame[11];
addExplanation(frame, 1, "%02x acc", acc);
status = frame[12];
addExplanation(frame, 1, "%02x status ()", status);
configuration = frame[13]<<8 | frame[14];
addExplanation(frame, 2, "%02x%02x configuration ()", frame[13], frame[14]);
header_size = 4;
} else
if (ci_field == 0x8d || ci_field == 0x8c) {
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]);
header_size = 2;
if (ci_field == 0x8d) {
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]);
header_size = 6;
}
} else {
warning("Unknown ci-field %02x\n", ci_field);
}
payload.clear();
payload.insert(payload.end(), frame.begin()+17, frame.end());
payload.insert(payload.end(), frame.begin()+(11+header_size), frame.end());
verbose("(wmbus) received telegram");
verboseFields();
debugPayload("(wmbus) frame", frame);
@ -564,6 +600,9 @@ string vifeType(int vif, int vife)
//int extension = vif & 0x80;
//int t = vif & 0x7f;
if (vif == 0x83 && vife == 0x3b) {
return "Forward flow contribution only";
}
if (vif == 0xff) {
return "?";
}

31
wmbus.h
Wyświetl plik

@ -49,22 +49,28 @@ LIST_OF_LINK_MODES
using namespace std;
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
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
// The 6 a field bytes are composed of:
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 a_field_version {}; // 1 byte
int a_field_device_type {}; // 1 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?
int ci_field {}; // 1 byte
// When ci_field==0x7a then there are 4 extra header bytes, short data header
int acc {}; // 1 byte
int status {}; // 1 byte
int configuration {}; // 2 bytes
// When ci_field==0x8d then there are 8 extra header bytes (ELL header)
int cc_field {}; // 1 byte
// acc; // 1 byte
uchar sn[4] {}; // 4 bytes
// That is 6 bytes (not 8), the next two bytes, the payload crc
// part of this ELL header, even though they are inside the encrypted payload.
vector<uchar> parsed; // Parsed fields
vector<uchar> payload; // To be parsed.
@ -74,7 +80,6 @@ struct 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();