Added iPerl water meter.

pull/22/head
weetmuts 2018-11-23 09:04:31 +01:00
rodzic 5616a51792
commit ebf9440102
15 zmienionych plików z 343 dodań i 20 usunięć

Wyświetl plik

@ -1,4 +1,8 @@
Version 0.7: 2018-11-23
David Mallon contributed the iPerl water meter! Thanks David!
Version 0.6: 2018-11-02
Added --shell command to invoke for example: mosquitto to send an MQTT message

Wyświetl plik

@ -27,7 +27,7 @@ endif
$(shell mkdir -p $(BUILD))
CXXFLAGS := $(DEBUG_FLAGS) -fPIC -fmessage-length=0 -std=c++11 -Wall -Wno-maybe-uninitialized -Wno-unused-function "-DWMBUSMETERS_VERSION=\"0.6\""
CXXFLAGS := $(DEBUG_FLAGS) -fPIC -fmessage-length=0 -std=c++11 -Wall -Wno-maybe-uninitialized -Wno-unused-function "-DWMBUSMETERS_VERSION=\"0.7\""
$(BUILD)/%.o: %.cc $(wildcard %.h)
$(CXX) $(CXXFLAGS) $< -c -o $@
@ -41,6 +41,7 @@ METERS_OBJS:=\
$(BUILD)/meter_multical302.o \
$(BUILD)/meter_omnipower.o \
$(BUILD)/meter_supercom587.o \
$(BUILD)/meter_iperl.o \
$(BUILD)/printer.o \
$(BUILD)/serial.o \
$(BUILD)/shell.o \

Wyświetl plik

@ -8,7 +8,7 @@ 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.6
wmbusmeters version: 0.7
Usage: wmbusmeters {options} (auto | /dev/ttyUSBx)] { [meter_name] [meter_type] [meter_id] [meter_key] }*
Add more meter quadruplets to listen to more meters.
@ -29,9 +29,8 @@ Add --verbose for detailed debug information.
Specifying auto as the device will automatically look for usb
wmbus dongles on /dev/im871a and /dev/amb8465.
The meter types: multical21,flowiq3100 (water meters) are supported.
The meter types: multical302 (heat), omnipower (electricity) and supercom587 (water)
are work in progress.
The meter types: multical21,flowiq3100,supercom587,iperl (water meters) are supported.
The meter types: multical302 (heat), omnipower (electricity) are work in progress.
```
Currently the meters are hardcoded for the European default setting

Wyświetl plik

@ -50,7 +50,7 @@ int main(int argc, char **argv)
" or 10m for ten minutes or 5s for five seconds.\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("The meter types: multical21,flowiq3100,supercom587 (water meters) are supported.\n"
printf("The meter types: multical21,flowiq3100,supercom587,iperl (water meters) are supported.\n"
"The meter types: multical302 (heat) and omnipower (electricity)\n"
"are work in progress.\n\n");
exit(0);
@ -150,6 +150,10 @@ int main(int argc, char **argv)
m.meter = createSupercom587(wmbus, m.name, m.id, m.key);
verbose("(supercom587) configured \"%s\" \"supercom587\" \"%s\" \"%s\"\n", m.name, m.id, m.key);
break;
case IPERL_METER:
m.meter = createIperl(wmbus, m.name, m.id, m.key);
verbose("(iperl) configured \"%s\" \"iperl\" \"%s\" \"%s\"\n", m.name, m.id, m.key);
break;
case UNKNOWN_METER:
error("No such meter type \"%s\"\n", m.type);
break;

257
meter_iperl.cc 100644
Wyświetl plik

@ -0,0 +1,257 @@
/*
Copyright (C) 2017-2018 Fredrik Öhrström
Copyright (C) 2018 David Mallon
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include"dvparser.h"
#include"meters.h"
#include"meters_common_implementation.h"
#include"wmbus.h"
#include"wmbus_utils.h"
#include"util.h"
#include<assert.h>
#include<map>
#include<memory.h>
#include<stdio.h>
#include<string>
#include<time.h>
#include<vector>
using namespace std;
struct MeterIperl : public virtual WaterMeter, public virtual MeterCommonImplementation {
MeterIperl(WMBus *bus, const char *name, const char *id, const char *key);
// Total water counted through the meter
double totalWaterConsumption();
bool hasTotalWaterConsumption();
double targetWaterConsumption();
bool hasTargetWaterConsumption();
double maxFlow();
bool hasMaxFlow();
string statusHumanReadable();
string status();
string timeDry();
string timeReversed();
string timeLeaking();
string timeBursting();
void printMeter(string *human_readable,
string *fields, char separator,
string *json,
vector<string> *envs);
private:
void handleTelegram(Telegram *t);
void processContent(Telegram *t);
string decodeTime(int time);
double total_water_consumption_ {};
};
MeterIperl::MeterIperl(WMBus *bus, const char *name, const char *id, const char *key) :
MeterCommonImplementation(bus, name, id, key, IPERL_METER, MANUFACTURER_SEN, 0x16, LinkModeT1)
{
MeterCommonImplementation::bus()->onTelegram(calll(this,handleTelegram,Telegram*));
}
double MeterIperl::totalWaterConsumption()
{
return total_water_consumption_;
}
WaterMeter *createIperl(WMBus *bus, const char *name, const char *id, const char *key)
{
return new MeterIperl(bus,name,id,key);
}
void MeterIperl::handleTelegram(Telegram *t)
{
if (!isTelegramForMe(t)) {
// This telegram is not intended for this meter.
return;
}
verbose("(%s) telegram for %s %02x%02x%02x%02x\n", "iperl",
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 != 0x07 && t->a_field_device_type != 0x06) {
warning("(%s) expected telegram for cold or warm water media, but got \"%s\"!\n", "iperl",
mediaType(t->m_field, t->a_field_device_type).c_str());
}
updateMedia(t->a_field_device_type);
if (t->m_field != manufacturer() ||
t->a_field_version != 0x68) {
warning("(%s) expected telegram from SEN meter with version 0x%02x, "
"but got \"%s\" meter with version 0x%02x !\n", "iperl",
0x68,
manufacturerFlag(t->m_field).c_str(),
t->a_field_version);
}
if (useAes()) {
vector<uchar> aeskey = key();
decryptMode5_AES_CBC(t, aeskey, "iperl");
verbose("$\n");
} else {
t->content = t->payload;
}
char log_prefix[256];
snprintf(log_prefix, 255, "(%s) log", "iperl");
logTelegram(log_prefix, t->parsed, t->content);
int content_start = t->parsed.size();
processContent(t);
if (isDebugEnabled()) {
snprintf(log_prefix, 255, "(%s)", "iperl");
t->explainParse(log_prefix, content_start);
}
triggerUpdate(t);
}
void MeterIperl::processContent(Telegram *t)
{
vector<uchar>::iterator bytes = t->content.begin();
map<string,pair<int,string>> values;
parseDV(t, t->content.begin(), t->content.size(), &values);
int offset;
extractDVdouble(&values, "0413", &offset, &total_water_consumption_);
t->addMoreExplanation(offset, " total consumption (%f m3)", total_water_consumption_);
}
void MeterIperl::printMeter(string *human_readable,
string *fields, char separator,
string *json,
vector<string> *envs)
{
char buf[65536];
buf[65535] = 0;
snprintf(buf, sizeof(buf)-1,
"%s\t"
"%s\t"
"% 3.3f m3\t"
"%s",
name().c_str(),
id().c_str(),
totalWaterConsumption(),
datetimeOfUpdateHumanReadable().c_str());
*human_readable = buf;
snprintf(buf, sizeof(buf)-1,
"%s%c"
"%s%c"
"%f%c"
"%s",
name().c_str(), separator,
id().c_str(), separator,
totalWaterConsumption(), separator,
datetimeOfUpdateRobot().c_str());
*fields = buf;
#define Q(x,y) "\""#x"\":"#y","
#define QS(x,y) "\""#x"\":\""#y"\","
#define QSE(x,y) "\""#x"\":\""#y"\""
snprintf(buf, sizeof(buf)-1, "{"
QS(media,%s)
QS(meter,iperl)
QS(name,%s)
QS(id,%s)
Q(total_m3,%f)
QSE(timestamp,%s)
"}",
mediaType(manufacturer(), media()).c_str(),
name().c_str(),
id().c_str(),
totalWaterConsumption(),
datetimeOfUpdateRobot().c_str());
*json = buf;
envs->push_back(string("METER_JSON=")+*json);
envs->push_back(string("METER_TYPE=iperl"));
envs->push_back(string("METER_ID=")+id());
envs->push_back(string("METER_TOTAL_M3=")+to_string(totalWaterConsumption()));
envs->push_back(string("METER_TIMESTAMP=")+datetimeOfUpdateRobot());
}
bool MeterIperl::hasTotalWaterConsumption()
{
return true;
}
double MeterIperl::targetWaterConsumption()
{
return 0.0;
}
bool MeterIperl::hasTargetWaterConsumption()
{
return false;
}
double MeterIperl::maxFlow()
{
return 0.0;
}
bool MeterIperl::hasMaxFlow()
{
return false;
}
string MeterIperl::statusHumanReadable()
{
return "";
}
string MeterIperl::status()
{
return "";
}
string MeterIperl::timeDry()
{
return "";
}
string MeterIperl::timeReversed()
{
return "";
}
string MeterIperl::timeLeaking()
{
return "";
}
string MeterIperl::timeBursting()
{
return "";
}

Wyświetl plik

@ -174,7 +174,7 @@ void MeterMultical21::handleTelegram(Telegram *t)
if (useAes()) {
vector<uchar> aeskey = key();
decryptKamstrupC1(t, aeskey);
decryptMode1_AES_CTR(t, aeskey, meter_name_);
} else {
t->content = t->payload;
}

Wyświetl plik

@ -96,7 +96,7 @@ void MeterMultical302::handleTelegram(Telegram *t) {
if (useAes()) {
vector<uchar> aeskey = key();
decryptKamstrupC1(t, aeskey);
decryptMode1_AES_CTR(t, aeskey, "multical302");
} else {
t->content = t->payload;
}

Wyświetl plik

@ -89,8 +89,7 @@ void MeterOmnipower::handleTelegram(Telegram *t) {
if (useAes()) {
vector<uchar> aeskey = key();
// Proper decryption not yet implemented!
decryptKamstrupC1(t, aeskey);
decryptMode5_AES_CBC(t, aeskey, "omnipower");
} else {
t->content = t->payload;
}

Wyświetl plik

@ -110,7 +110,7 @@ void MeterSupercom587::handleTelegram(Telegram *t)
if (useAes()) {
vector<uchar> aeskey = key();
decryptKamstrupC1(t, aeskey);
decryptMode1_AES_CTR(t, aeskey, "supercom587");
} else {
t->content = t->payload;
}

Wyświetl plik

@ -104,6 +104,7 @@ MeterType toMeterType(const char *type)
if (!strcmp(type, "multical302")) return MULTICAL302_METER;
if (!strcmp(type, "omnipower")) return OMNIPOWER_METER;
if (!strcmp(type, "supercom587")) return SUPERCOM587_METER;
if (!strcmp(type, "iperl")) return IPERL_METER;
return UNKNOWN_METER;
}
@ -114,6 +115,7 @@ LinkMode toMeterLinkMode(const char *type)
if (!strcmp(type, "multical302")) return LinkModeC1;
if (!strcmp(type, "omnipower")) return LinkModeC1;
if (!strcmp(type, "supercom587")) return LinkModeT1;
if (!strcmp(type, "iperl")) return LinkModeT1;
return UNKNOWN_LINKMODE;
}

Wyświetl plik

@ -24,7 +24,7 @@
#include<string>
#include<vector>
#define LIST_OF_METERS X(MULTICAL21_METER)X(FLOWIQ3100_METER)X(MULTICAL302_METER)X(OMNIPOWER_METER)X(SUPERCOM587_METER)X(UNKNOWN_METER)
#define LIST_OF_METERS X(MULTICAL21_METER)X(FLOWIQ3100_METER)X(MULTICAL302_METER)X(OMNIPOWER_METER)X(SUPERCOM587_METER)X(IPERL_METER)X(UNKNOWN_METER)
enum MeterType {
#define X(name) name,
@ -102,6 +102,7 @@ WaterMeter *createMultical21(WMBus *bus, const char *name, const char *id, const
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);
WaterMeter *createSupercom587(WMBus *bus, const char *name, const char *id, const char *key);
WaterMeter *createIperl(WMBus *bus, const char *name, const char *id, const char *key);
GenericMeter *createGeneric(WMBus *bus, const char *name, const char *id, const char *key);
#endif

Wyświetl plik

@ -5,3 +5,9 @@ telegram=|A244EE4D785634123C067A73000000|0C1334190000426CE1F14C130000000082046C2
telegram=|A244EE4D111111113C077A72000000|0C1374140000426CE1F14C130000000082046C21298C0413010000008D04931E3A3CFE0100000001000000010000000100000001000000010000000100000001000000010000000100000001000000010000001600000031130000046D0113412B03FD6CDF120082206C5C290BFD0F0200018C4079629885238310FD3100000082106C01018110FD610002FD66020002|
{"media":"water","meter":"supercom587","name":"MyColdWater","id":"11111111","total_m3":1.474000,"timestamp":"1111-11-11T11:11:11Z"}
# Test iPerl T1 telegram
# 12345678 "1234567890ABCDEF1234567890ABCDEF"
telegram=|1E44AE4C7856341268077AB5000000|53cb05c02cf40b38e4cfe6a9f6565ec27261ae620df9257179197ef2dc6512f2|
{"media":"water","meter":"iperl","name":"MoreWater","id":"12345678","total_m3":3.699000,"timestamp":"1111-11-11T11:11:11Z"}

Wyświetl plik

@ -24,6 +24,7 @@ cat simulation_t1.txt | grep '^{' > test_expected.txt
$PROG --robot=json simulation_t1.txt \
MyWarmWater supercom587 12345678 "" \
MyColdWater supercom587 11111111 "" \
MoreWater iperl 12345678 "1234567890ABCDEF1234567890ABCDEF" \
> test_output.txt
if [ "$?" == "0" ]
then

Wyświetl plik

@ -21,7 +21,7 @@
#include"aes.h"
#include"wmbus.h"
void decryptKamstrupC1(Telegram *t, vector<uchar> &aeskey)
void decryptMode1_AES_CTR(Telegram *t, vector<uchar> &aeskey, const char *meter_name)
{
vector<uchar> content;
content.insert(content.end(), t->payload.begin(), t->payload.end());
@ -45,7 +45,7 @@ void decryptKamstrupC1(Telegram *t, vector<uchar> &aeskey)
vector<uchar> ivv(iv, iv+16);
string s = bin2hex(ivv);
debug("(multical21) IV %s\n", s.c_str());
debug("(%s) IV %s\n", meter_name, s.c_str());
uchar xordata[16];
AES_ECB_encrypt(iv, &aeskey[0], xordata, 16);
@ -54,11 +54,11 @@ void decryptKamstrupC1(Telegram *t, vector<uchar> &aeskey)
xorit(xordata, &content[0], decrypt, remaining);
vector<uchar> dec(decrypt, decrypt+remaining);
debugPayload("(multical21) decrypted", dec);
debugPayload("(C1) 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());
warning("(%s) warning: C1 decryption received too many bytes of content! "
"Got %zu bytes, expected at most 22.\n", meter_name, content.size());
}
if (content.size() > 16) {
// Yay! Lets decrypt a second block. Full frame content is 22 bytes.
@ -69,14 +69,14 @@ void decryptKamstrupC1(Telegram *t, vector<uchar> &aeskey)
incrementIV(iv, sizeof(iv));
vector<uchar> ivv2(iv, iv+16);
string s2 = bin2hex(ivv2);
debug("(multical21) IV+1 %s\n", s2.c_str());
debug("(%s) IV+1 %s\n", meter_name, 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);
debugPayload("(C1) decrypted", dec2);
// Append the second decrypted block to the first.
dec.insert(dec.end(), dec2.begin(), dec2.end());
@ -91,4 +91,52 @@ string frameTypeKamstrupC1(int ft) {
return "?";
}
void decryptMode5_AES_CBC(Telegram *t, vector<uchar> &aeskey, const char *meter_name)
{
vector<uchar> content;
content.insert(content.end(), t->payload.begin(), t->payload.end());
// The content should be a multiple of 16 since we are using AES CBC mode.
if (content.size() % 16 != 0)
{
warning("(%s) warning: T1 decryption received non-multiple of 16 bytes! "
"Got %zu bytes shrinking message to %zu bytes.\n",
meter_name, content.size(), content.size() - content.size() % 16);
while (content.size() % 16 != 0)
{
content.pop_back();
}
}
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]; }
// ACC
iv[i++] = t->acc;
// SN-field
for (int j=0; j<4; ++j) { iv[i++] = t->acc; }
// FN
iv[i++] = t->acc; iv[i++] = t->acc;
// BC
iv[i++] = t->acc;
vector<uchar> ivv(iv, iv+16);
string s = bin2hex(ivv);
verbose("(%s) IV %s\n", meter_name, s.c_str());
uchar decrypted_data[16];
AES_CBC_decrypt_buffer(decrypted_data, &content[0], 16, &aeskey[0], iv);
vector<uchar> decrypted(decrypted_data, decrypted_data+16);
if (decrypted_data[0] != 0x2F || decrypted_data[1] != 0x2F) {
verbose("(%s) decrypt failed!\n", meter_name);
}
t->content.clear();
t->content.insert(t->content.end(), decrypted.begin(), decrypted.end());
debugPayload("(T1) decrypted", t->content);
}
#endif

Wyświetl plik

@ -18,7 +18,8 @@
#ifndef WMBUS_UTILS_H
#define WMBUS_UTILS_H
void decryptKamstrupC1(Telegram *t, vector<uchar> &aeskey);
void decryptMode1_AES_CTR(Telegram *t, vector<uchar> &aeskey, const char *meter_name);
void decryptMode5_AES_CBC(Telegram *t, vector<uchar> &aeskey, const char *meter_name);
string frameTypeKamstrupC1(int ft);
#endif