kopia lustrzana https://github.com/weetmuts/wmbusmeters
Merge pull request #247 from don-vip/add-sharky
Add Sharky 775 + fix Diehl address swappingpull/249/head
commit
4f5d51b891
1
Makefile
1
Makefile
|
@ -156,6 +156,7 @@ METER_OBJS:=\
|
|||
$(BUILD)/meter_qcaloric.o \
|
||||
$(BUILD)/meter_rfmamb.o \
|
||||
$(BUILD)/meter_rfmtx1.o \
|
||||
$(BUILD)/meter_sharky.o \
|
||||
$(BUILD)/meter_supercom587.o \
|
||||
$(BUILD)/meter_sontex868.o \
|
||||
$(BUILD)/meter_topaseskr.o \
|
||||
|
|
|
@ -6,12 +6,12 @@ telegram=|1944304C72242421D401A2|013D4013DD8B46A4999C1293E582CC|
|
|||
# Test new version of IZAR
|
||||
|
||||
telegram=|2944A511780729662366A20118001378D3B3DB8CEDD77731F25832AAF3DA8CADF9774EA673172E8C61F2|
|
||||
{"media":"water","meter":"izar","name":"IzarWater2","id":"23662907","total_m3":16.76,"last_month_total_m3":11.84,"last_month_measure_date":"2019-11-30","remaining_battery_life_y":12,"current_alarms":"no_alarm","previous_alarms":"no_alarm","transmit_period_s":8,"timestamp":"1111-11-11T11:11:11Z"}
|
||||
{"media":"water","meter":"izar","name":"IzarWater2","id":"66236629","total_m3":16.76,"last_month_total_m3":11.84,"last_month_measure_date":"2019-11-30","remaining_battery_life_y":12,"current_alarms":"no_alarm","previous_alarms":"no_alarm","transmit_period_s":8,"timestamp":"1111-11-11T11:11:11Z"}
|
||||
|
||||
# Yet another version of IZAR
|
||||
|
||||
telegram=|1944A511780779194820A1|21170013355F8EDB2D03C6912B1E37
|
||||
{"media":"water","meter":"izar","name":"IzarWater3","id":"48197907","total_m3":4.366,"last_month_total_m3":0,"last_month_measure_date":"2020-12-31","remaining_battery_life_y":11.5,"current_alarms":"no_alarm","previous_alarms":"no_alarm","transmit_period_s":8,"timestamp":"1111-11-11T11:11:11Z"}
|
||||
{"media":"water","meter":"izar","name":"IzarWater3","id":"20481979","total_m3":4.366,"last_month_total_m3":0,"last_month_measure_date":"2020-12-31","remaining_battery_life_y":11.5,"current_alarms":"no_alarm","previous_alarms":"no_alarm","transmit_period_s":8,"timestamp":"1111-11-11T11:11:11Z"}
|
||||
|
||||
# And another izar, with a mfct specific tpl ci field a3.
|
||||
|
||||
|
|
|
@ -220,3 +220,9 @@ telegram=||7844731e78610418010278046d0f13bc21040394030000841003690300008420032b0
|
|||
telegram=||9e44731e17011020010278046d0813bc21040300000000841003000000008420030000000084300300000000848010030000000084016d0000bc218401030000000084110300000000842103000000008431030000000084811003000000008440fd4825090000848040fd480000000084c040fd48000000008440fd5b00000000848040fd5b0000000084c040fd5b0000000002fb2d881304fd1700000101|
|
||||
{"media":"electricity","meter":"gransystems","name":"Gran301","id":"20100117","total_energy_consumption_kwh":0,"voltage_at_phase_1_v":234.1,"voltage_at_phase_2_v":0,"voltage_at_phase_3_v":0,"currrent_at_phase_1_a":0,"currrent_at_phase_2_a":0,"currrent_at_phase_3_a":0,"frequency_hz":50,"status":"OK","timestamp":"1111-11-11T11:11:11Z"}
|
||||
|Gran301;20100117;0.000000;234.100000;0.000000;0.000000;0.000000;0.000000;0.000000;50.000000;OK;1111-11-11 11:11.11
|
||||
|
||||
# Test Hydrometer/Diehl Metering Sharky 775 heat meter
|
||||
telegram=|534424232004256092687A370045752235854DEEEA5939FAD81C25FEEF5A23C38FB9168493C563F08DB10BAF87F660FBA91296BA2397E8F4220B86D3A192FB51E0BFCF24DCE72118E0C75A9E89F43BDFE370824B|
|
||||
{"media":"heat","meter":"sharky","name":"Sharky775","id":"68926025","total_energy_consumption_kwh":2651,"total_energy_consumption_tariff1_kwh":0,"total_volume_m3":150.347,"total_volume_m3":0.018,"volume_flow_m3h":0,"power_kw":0,"flow_temperature_c":42.3,"return_temperature_c":28.1,"temperature_difference_c":14.1,"timestamp":"1111-11-11T11:11:11Z"}
|
||||
|Sharky775;68926025;2651.000000;0.000000;150.347000;0.018000;0.000000;0.000000;42.300000;28.100000;14.100000;1111-11-11 11:11.11
|
||||
|
||||
|
|
|
@ -31,7 +31,8 @@
|
|||
X(Volume,0x10,0x17) \
|
||||
X(VolumeFlow,0x38,0x3F) \
|
||||
X(FlowTemperature,0x58,0x5B) \
|
||||
X(ReturnTemperature,0x5D,0x5E) \
|
||||
X(ReturnTemperature,0x5C,0x5F) \
|
||||
X(TemperatureDifference,0x60,0x63) \
|
||||
X(ExternalTemperature,0x64,0x67) \
|
||||
X(HeatCostAllocation,0x6E,0x6E) \
|
||||
X(Date,0x6C,0x6C) \
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright (C) 2021 Vincent Privat
|
||||
Copyright (C) 2019 Jacek Tomasiak
|
||||
2021 Vincent Privat
|
||||
|
||||
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
|
||||
|
@ -27,8 +28,35 @@ std::set<int> diehl_manufacturers = {
|
|||
MANUFACTURER_SPL
|
||||
};
|
||||
|
||||
// Diehl: is "A field" coded as version/type/serialnumber instead of standard serialnumber/version/type?
|
||||
DiehlAddressTransformMethod mustTransformDiehlAddress(uchar c_field, int m_field, uchar ci_field, int tpl_cfg)
|
||||
// Default keys for Izar/PRIOS and Sharky meters
|
||||
#define PRIOS_DEFAULT_KEY1 "39BC8A10E66D83F8"
|
||||
#define PRIOS_DEFAULT_KEY2 "51728910E66D83F8"
|
||||
|
||||
// Diehl: Is "A field" coded differently from standard?
|
||||
DiehlAddressTransformMethod mustTransformDiehlAddress(DiehlFrameInterpretation interpretation)
|
||||
{
|
||||
switch (interpretation) {
|
||||
case DiehlFrameInterpretation::PRIOS:
|
||||
case DiehlFrameInterpretation::PRIOS_SCR:
|
||||
case DiehlFrameInterpretation::REAL_DATA:
|
||||
return DiehlAddressTransformMethod::SWAPPING;
|
||||
|
||||
case DiehlFrameInterpretation::SAP_PRIOS:
|
||||
return DiehlAddressTransformMethod::SAP_PRIOS;
|
||||
|
||||
case DiehlFrameInterpretation::SAP_PRIOS_STD:
|
||||
return DiehlAddressTransformMethod::SAP_PRIOS_STANDARD;
|
||||
|
||||
case DiehlFrameInterpretation::RESERVED:
|
||||
case DiehlFrameInterpretation::OMS:
|
||||
case DiehlFrameInterpretation::NA:
|
||||
default:
|
||||
return DiehlAddressTransformMethod::NONE;
|
||||
}
|
||||
}
|
||||
|
||||
// Diehl: Determines how to interpret frame
|
||||
DiehlFrameInterpretation detectDiehlFrameInterpretation(uchar c_field, int m_field, uchar ci_field, int tpl_cfg)
|
||||
{
|
||||
if (diehl_manufacturers.find(m_field) != diehl_manufacturers.end())
|
||||
{
|
||||
|
@ -36,13 +64,13 @@ DiehlAddressTransformMethod mustTransformDiehlAddress(uchar c_field, int m_field
|
|||
{
|
||||
switch (ci_field) {
|
||||
case 0x71: // Alarm
|
||||
return DiehlAddressTransformMethod::SWAPPING; // Real Data
|
||||
return DiehlFrameInterpretation::REAL_DATA;
|
||||
case 0x7A: // EN 13757-3 Application Layer (short tplh)
|
||||
if (((tpl_cfg >> 8) & 0x10) == 0x10)
|
||||
if (((tpl_cfg >> 8) & 0x10) == 0x10) // Bit 12 from MMMMM bits of CFG field
|
||||
{
|
||||
return DiehlAddressTransformMethod::SWAPPING; // Real Data
|
||||
return DiehlFrameInterpretation::REAL_DATA;
|
||||
}
|
||||
break; // OMS
|
||||
return DiehlFrameInterpretation::OMS;
|
||||
case 0xA0: // Manufacturer specific
|
||||
case 0xA1: // Manufacturer specific
|
||||
case 0xA2: // Manufacturer specific
|
||||
|
@ -53,15 +81,15 @@ DiehlAddressTransformMethod mustTransformDiehlAddress(uchar c_field, int m_field
|
|||
case 0xA7: // Manufacturer specific
|
||||
if (m_field == MANUFACTURER_SAP)
|
||||
{
|
||||
return DiehlAddressTransformMethod::SAP_PRIOS; // SAP PRIOS
|
||||
return DiehlFrameInterpretation::SAP_PRIOS;
|
||||
}
|
||||
return DiehlAddressTransformMethod::SWAPPING; // PRIOS
|
||||
return DiehlFrameInterpretation::PRIOS;
|
||||
case 0xB0: // Manufacturer specific
|
||||
if (m_field == MANUFACTURER_SAP)
|
||||
{
|
||||
return DiehlAddressTransformMethod::SAP_PRIOS_STANDARD; // SAP PRIOS STD
|
||||
return DiehlFrameInterpretation::SAP_PRIOS_STD;
|
||||
}
|
||||
break; // Reserved
|
||||
return DiehlFrameInterpretation::RESERVED;
|
||||
case 0xA8: // Manufacturer specific
|
||||
case 0xA9: // Manufacturer specific
|
||||
case 0xAA: // Manufacturer specific
|
||||
|
@ -74,21 +102,40 @@ DiehlAddressTransformMethod mustTransformDiehlAddress(uchar c_field, int m_field
|
|||
case 0xB5: // Manufacturer specific
|
||||
case 0xB6: // Manufacturer specific
|
||||
case 0xB7: // Manufacturer specific
|
||||
break; // Reserved
|
||||
return DiehlFrameInterpretation::RESERVED;
|
||||
case 0xB1: // Manufacturer specific
|
||||
case 0xB2: // Manufacturer specific
|
||||
case 0xB3: // Manufacturer specific
|
||||
return DiehlAddressTransformMethod::SWAPPING; // PRIOS SCR
|
||||
return DiehlFrameInterpretation::PRIOS_SCR;
|
||||
default:
|
||||
break; // OMS
|
||||
return DiehlFrameInterpretation::OMS;
|
||||
}
|
||||
}
|
||||
}
|
||||
return DiehlAddressTransformMethod::NONE;
|
||||
return DiehlFrameInterpretation::NA;
|
||||
}
|
||||
|
||||
// Diehl: Determines how to interpret frame
|
||||
DiehlFrameInterpretation detectDiehlFrameInterpretation(const vector<uchar>& frame)
|
||||
{
|
||||
if (frame.size() < 15)
|
||||
return DiehlFrameInterpretation::NA;
|
||||
|
||||
uchar c_field = frame[1];
|
||||
int m_field = frame[3] <<8 | frame[2];
|
||||
uchar ci_field = frame[10];
|
||||
int tpl_cfg = frame[14] <<8 | frame[13];
|
||||
return detectDiehlFrameInterpretation(c_field, m_field, ci_field, tpl_cfg);
|
||||
}
|
||||
|
||||
// Diehl: Is "A field" coded differently from standard?
|
||||
DiehlAddressTransformMethod mustTransformDiehlAddress(const vector<uchar>& frame)
|
||||
{
|
||||
return mustTransformDiehlAddress(detectDiehlFrameInterpretation(frame));
|
||||
}
|
||||
|
||||
// Diehl: transform "A field" to make it compliant to standard
|
||||
void transformDiehlAddress(std::vector<uchar>& frame, DiehlAddressTransformMethod transform_method)
|
||||
void transformDiehlAddress(vector<uchar>& frame, DiehlAddressTransformMethod transform_method)
|
||||
{
|
||||
if (transform_method == DiehlAddressTransformMethod::SWAPPING)
|
||||
{
|
||||
|
@ -97,7 +144,7 @@ void transformDiehlAddress(std::vector<uchar>& frame, DiehlAddressTransformMetho
|
|||
uchar type = frame[5];
|
||||
for (int i = 4; i < 8; i++)
|
||||
{
|
||||
frame[i] = frame[i+1];
|
||||
frame[i] = frame[i+2];
|
||||
}
|
||||
frame[8] = version;
|
||||
frame[9] = type;
|
||||
|
@ -114,3 +161,127 @@ void transformDiehlAddress(std::vector<uchar>& frame, DiehlAddressTransformMetho
|
|||
}
|
||||
}
|
||||
|
||||
// Diehl: decode LFSR encrypted data used in Izar/PRIOS and Sharky meters
|
||||
vector<uchar> decodeDiehlLfsr(const vector<uchar> &origin, const vector<uchar> &frame, uint32_t key, DiehlLfsrCheckMethod check_method, uint32_t check_value)
|
||||
{
|
||||
// modify seed key with header values
|
||||
key ^= uint32FromBytes(origin, 2); // manufacturer + address[0-1]
|
||||
key ^= uint32FromBytes(origin, 6); // address[2-3] + version + type
|
||||
key ^= uint32FromBytes(frame, 10); // ci + some more bytes from the telegram...
|
||||
|
||||
int size = frame.size() - 15;
|
||||
vector<uchar> decoded(size);
|
||||
|
||||
for (int i = 0; i < size; ++i) {
|
||||
// calculate new key (LFSR)
|
||||
// https://en.wikipedia.org/wiki/Linear-feedback_shift_register
|
||||
for (int j = 0; j < 8; ++j) {
|
||||
// calculate new bit value (xor of selected bits from previous key)
|
||||
uchar bit = ((key & 0x2) != 0) ^ ((key & 0x4) != 0) ^ ((key & 0x800) != 0) ^ ((key & 0x80000000) != 0);
|
||||
// shift key bits and add new one at the end
|
||||
key = (key << 1) | bit;
|
||||
}
|
||||
// decode i-th content byte with fresh/last 8-bits of key
|
||||
decoded[i] = frame[i + 15] ^ (key & 0xFF);
|
||||
|
||||
if (check_method == DiehlLfsrCheckMethod::HEADER_1_BYTE)
|
||||
{
|
||||
// check-byte doesn't match?
|
||||
if (decoded[0] != 0x4B) {
|
||||
decoded.clear();
|
||||
return decoded;
|
||||
}
|
||||
}
|
||||
else if (check_method == DiehlLfsrCheckMethod::CHECKSUM_AND_0XEF)
|
||||
{
|
||||
if (i == size - 1) {
|
||||
uint32_t checksum = 0;
|
||||
for (int index = 0; index < size; index++) {
|
||||
checksum += (decoded[index] & 0xFF);
|
||||
}
|
||||
if ((checksum & 0xEF) != check_value) {
|
||||
decoded.clear();
|
||||
return decoded;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return decoded;
|
||||
}
|
||||
|
||||
uint32_t uint32FromBytes(const vector<uchar> &data, int offset, bool reverse)
|
||||
{
|
||||
if (reverse)
|
||||
return ((uint32_t)data[offset + 3] << 24) |
|
||||
((uint32_t)data[offset + 2] << 16) |
|
||||
((uint32_t)data[offset + 1] << 8) |
|
||||
(uint32_t)data[offset];
|
||||
else
|
||||
return ((uint32_t)data[offset] << 24) |
|
||||
((uint32_t)data[offset + 1] << 16) |
|
||||
((uint32_t)data[offset + 2] << 8) |
|
||||
(uint32_t)data[offset + 3];
|
||||
}
|
||||
|
||||
uint32_t convertKey(const vector<uchar> &bytes)
|
||||
{
|
||||
uint32_t key1 = uint32FromBytes(bytes, 0);
|
||||
uint32_t key2 = uint32FromBytes(bytes, 4);
|
||||
uint32_t key = key1 ^ key2;
|
||||
return key;
|
||||
}
|
||||
|
||||
uint32_t convertKey(const char *hex)
|
||||
{
|
||||
vector<uchar> bytes;
|
||||
hex2bin(hex, &bytes);
|
||||
return convertKey(bytes);
|
||||
}
|
||||
|
||||
// Diehl: initialize support of default keys in a meter
|
||||
void initializeDiehlDefaultKeySupport(const vector<uchar> &confidentiality_key, vector<uint32_t>& keys)
|
||||
{
|
||||
if (!confidentiality_key.empty())
|
||||
keys.push_back(convertKey(confidentiality_key));
|
||||
|
||||
// fallback to default keys if no custom key provided
|
||||
if (keys.empty())
|
||||
{
|
||||
keys.push_back(convertKey(PRIOS_DEFAULT_KEY1));
|
||||
keys.push_back(convertKey(PRIOS_DEFAULT_KEY2));
|
||||
}
|
||||
}
|
||||
|
||||
// Diehl: Is payload real data crypted (LFSR)?
|
||||
bool mustDecryptDiehlRealData(const vector<uchar>& frame)
|
||||
{
|
||||
return detectDiehlFrameInterpretation(frame) == DiehlFrameInterpretation::REAL_DATA;
|
||||
}
|
||||
|
||||
// Diehl: decrypt real data payload (LFSR)
|
||||
bool decryptDielhRealData(Telegram *t, vector<uchar> &frame, vector<uchar>::iterator &pos, const vector<uchar> &confidentiality_key)
|
||||
{
|
||||
vector<uint32_t> keys;
|
||||
initializeDiehlDefaultKeySupport(confidentiality_key, keys);
|
||||
|
||||
vector<uchar> decoded_content;
|
||||
for (auto& key : keys) {
|
||||
decoded_content = decodeDiehlLfsr(t->original.empty() ? frame : t->original, frame, key, DiehlLfsrCheckMethod::CHECKSUM_AND_0XEF, frame[14] & 0xEF);
|
||||
if (!decoded_content.empty())
|
||||
break;
|
||||
}
|
||||
|
||||
if (decoded_content.empty())
|
||||
{
|
||||
warning("(diehl) Decoding LFSR real data failed.\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
debug("(diehl) Decoded LFSR real data: %s\n", bin2hex(decoded_content).c_str());
|
||||
|
||||
frame.erase(pos, frame.end());
|
||||
frame.insert(frame.end(), decoded_content.begin(), decoded_content.end());
|
||||
|
||||
return true;
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright (C) 2021 Vincent Privat
|
||||
Copyright (C) 2019 Jacek Tomasiak
|
||||
2021 Vincent Privat
|
||||
|
||||
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
|
||||
|
@ -19,18 +20,54 @@
|
|||
#define MANUFACTURER_SPECIFICITIES_H
|
||||
|
||||
#include"util.h"
|
||||
#include"wmbus.h"
|
||||
|
||||
enum class DiehlAddressTransformMethod {
|
||||
NONE,
|
||||
SWAPPING,
|
||||
SAP_PRIOS,
|
||||
SAP_PRIOS_STANDARD
|
||||
using namespace std;
|
||||
|
||||
uint32_t uint32FromBytes(const vector<uchar> &data, int offset, bool reverse = false);
|
||||
|
||||
// Diehl: initialize support of default keys in a meter
|
||||
void initializeDiehlDefaultKeySupport(const vector<uchar> &confidentiality_key, vector<uint32_t>& keys);
|
||||
|
||||
// Diehl: check method of LFSR decryption algorithm
|
||||
enum class DiehlLfsrCheckMethod {
|
||||
CHECKSUM_AND_0XEF,
|
||||
HEADER_1_BYTE
|
||||
};
|
||||
|
||||
// Diehl: Is "A field" coded as version/type/serialnumber instead of standard serialnumber/version/type?
|
||||
DiehlAddressTransformMethod mustTransformDiehlAddress(uchar c_field, int m_field, uchar ci_field, int tpl_cfg);
|
||||
// Diehl: decode LFSR encrypted data used in Izar/PRIOS and Sharky meters
|
||||
vector<uchar> decodeDiehlLfsr(const vector<uchar> &origin, const vector<uchar> &frame, uint32_t key, DiehlLfsrCheckMethod check_method, uint32_t check_value);
|
||||
|
||||
// Diehl: swap "A field" to make it compliant to standard
|
||||
void transformDiehlAddress(std::vector<uchar>& frame, DiehlAddressTransformMethod method);
|
||||
// Diehl: frame interpretation
|
||||
enum class DiehlFrameInterpretation {
|
||||
NA, // N/A: not a Diehl frame
|
||||
REAL_DATA,
|
||||
OMS,
|
||||
PRIOS,
|
||||
SAP_PRIOS,
|
||||
SAP_PRIOS_STD,
|
||||
PRIOS_SCR,
|
||||
RESERVED
|
||||
};
|
||||
|
||||
// Diehl: address transformation method
|
||||
enum class DiehlAddressTransformMethod {
|
||||
NONE, // "A field" coded as per standard
|
||||
SWAPPING, // "A field" coded as version/type/serialnumber instead of standard serialnumber/version/type
|
||||
SAP_PRIOS, // Version and type not included in telegram. Must be hardcoded to 0 and 7
|
||||
SAP_PRIOS_STANDARD // ?
|
||||
};
|
||||
|
||||
// Diehl: Is "A field" coded differently from standard?
|
||||
DiehlAddressTransformMethod mustTransformDiehlAddress(const vector<uchar>& frame);
|
||||
|
||||
// Diehl: transform "A field" to make it compliant to standard
|
||||
void transformDiehlAddress(vector<uchar>& frame, DiehlAddressTransformMethod method);
|
||||
|
||||
// Diehl: Is payload real data crypted (LFSR)?
|
||||
bool mustDecryptDiehlRealData(const vector<uchar>& frame);
|
||||
|
||||
// Diehl: decrypt real data payload (LFSR)
|
||||
bool decryptDielhRealData(Telegram *t, vector<uchar> &frame, vector<uchar>::iterator &pos, const vector<uchar> &meterkey);
|
||||
|
||||
#endif
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
/*
|
||||
Copyright (C) 2019 Jacek Tomasiak
|
||||
2020 Fredrik Öhrström
|
||||
2021 Vincent Privat
|
||||
|
||||
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
|
||||
|
@ -20,15 +21,11 @@
|
|||
#include"meters_common_implementation.h"
|
||||
#include"wmbus.h"
|
||||
#include"wmbus_utils.h"
|
||||
#include"manufacturer_specificities.h"
|
||||
|
||||
#include<algorithm>
|
||||
#include<stdbool.h>
|
||||
|
||||
using namespace std;
|
||||
|
||||
#define PRIOS_DEFAULT_KEY1 "39BC8A10E66D83F8"
|
||||
#define PRIOS_DEFAULT_KEY2 "51728910E66D83F8"
|
||||
|
||||
/** Contains all the booleans required to store the alarms of a PRIOS device. */
|
||||
typedef struct _izar_alarms {
|
||||
bool general_alarm;
|
||||
|
@ -60,9 +57,6 @@ struct MeterIzar : public virtual WaterMeter, public virtual MeterCommonImplemen
|
|||
private:
|
||||
|
||||
void processContent(Telegram *t);
|
||||
uint32_t convertKey(const char *hex);
|
||||
uint32_t convertKey(const vector<uchar> &bytes);
|
||||
uint32_t uint32FromBytes(const vector<uchar> &data, int offset, bool reverse = false);
|
||||
vector<uchar> decodePrios(const vector<uchar> &origin, const vector<uchar> &payload, uint32_t key);
|
||||
|
||||
double remaining_battery_life;
|
||||
|
@ -85,17 +79,7 @@ shared_ptr<WaterMeter> createIzar(MeterInfo &mi)
|
|||
MeterIzar::MeterIzar(MeterInfo &mi) :
|
||||
MeterCommonImplementation(mi, MeterType::IZAR)
|
||||
{
|
||||
MeterKeys *mk = meterKeys();
|
||||
if (!mk->confidentiality_key.empty())
|
||||
keys.push_back(convertKey(mk->confidentiality_key));
|
||||
|
||||
// fallback to default keys if no custom key provided
|
||||
if (keys.empty())
|
||||
{
|
||||
keys.push_back(convertKey(PRIOS_DEFAULT_KEY1));
|
||||
keys.push_back(convertKey(PRIOS_DEFAULT_KEY2));
|
||||
}
|
||||
|
||||
initializeDiehlDefaultKeySupport(meterKeys()->confidentiality_key, keys);
|
||||
addLinkMode(LinkMode::T1);
|
||||
|
||||
addPrint("total", Quantity::Volume,
|
||||
|
@ -214,35 +198,6 @@ string MeterIzar::previousAlarmsText()
|
|||
return "no_alarm";
|
||||
}
|
||||
|
||||
uint32_t MeterIzar::uint32FromBytes(const vector<uchar> &data, int offset, bool reverse)
|
||||
{
|
||||
if (reverse)
|
||||
return ((uint32_t)data[offset + 3] << 24) |
|
||||
((uint32_t)data[offset + 2] << 16) |
|
||||
((uint32_t)data[offset + 1] << 8) |
|
||||
(uint32_t)data[offset];
|
||||
else
|
||||
return ((uint32_t)data[offset] << 24) |
|
||||
((uint32_t)data[offset + 1] << 16) |
|
||||
((uint32_t)data[offset + 2] << 8) |
|
||||
(uint32_t)data[offset + 3];
|
||||
}
|
||||
|
||||
uint32_t MeterIzar::convertKey(const char *hex)
|
||||
{
|
||||
vector<uchar> bytes;
|
||||
hex2bin(hex, &bytes);
|
||||
return convertKey(bytes);
|
||||
}
|
||||
|
||||
uint32_t MeterIzar::convertKey(const vector<uchar> &bytes)
|
||||
{
|
||||
uint32_t key1 = uint32FromBytes(bytes, 0);
|
||||
uint32_t key2 = uint32FromBytes(bytes, 4);
|
||||
uint32_t key = key1 ^ key2;
|
||||
return key;
|
||||
}
|
||||
|
||||
void MeterIzar::processContent(Telegram *t)
|
||||
{
|
||||
vector<uchar> frame;
|
||||
|
@ -297,31 +252,5 @@ void MeterIzar::processContent(Telegram *t)
|
|||
|
||||
vector<uchar> MeterIzar::decodePrios(const vector<uchar> &origin, const vector<uchar> &frame, uint32_t key)
|
||||
{
|
||||
// modify seed key with header values
|
||||
key ^= uint32FromBytes(origin, 2); // manufacturer + address[0-1]
|
||||
key ^= uint32FromBytes(origin, 6); // address[2-3] + version + type
|
||||
key ^= uint32FromBytes(frame, 10); // ci + some more bytes from the telegram...
|
||||
|
||||
int size = frame.size() - 15;
|
||||
vector<uchar> decoded(size);
|
||||
|
||||
for (int i = 0; i < size; ++i) {
|
||||
// calculate new key (LFSR)
|
||||
// https://en.wikipedia.org/wiki/Linear-feedback_shift_register
|
||||
for (int j = 0; j < 8; ++j) {
|
||||
// calculate new bit value (xor of selected bits from previous key)
|
||||
uchar bit = ((key & 0x2) != 0) ^ ((key & 0x4) != 0) ^ ((key & 0x800) != 0) ^ ((key & 0x80000000) != 0);
|
||||
// shift key bits and add new one at the end
|
||||
key = (key << 1) | bit;
|
||||
}
|
||||
// decode i-th content byte with fresh/last 8-bits of key
|
||||
decoded[i] = frame[i + 15] ^ (key & 0xFF);
|
||||
// check-byte doesn't match?
|
||||
if (decoded[0] != 0x4B) {
|
||||
decoded.clear();
|
||||
return decoded;
|
||||
}
|
||||
}
|
||||
|
||||
return decoded;
|
||||
return decodeDiehlLfsr(origin, frame, key, DiehlLfsrCheckMethod::HEADER_1_BYTE, 0x4B);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,255 @@
|
|||
/*
|
||||
Copyright (C) 2021 Vincent Privat
|
||||
|
||||
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"meters.h"
|
||||
#include"meters_common_implementation.h"
|
||||
#include"dvparser.h"
|
||||
#include"wmbus.h"
|
||||
#include"wmbus_utils.h"
|
||||
|
||||
struct MeterSharky : public virtual HeatMeter, public virtual MeterCommonImplementation {
|
||||
MeterSharky(MeterInfo &mi);
|
||||
|
||||
double totalEnergyConsumption(Unit u);
|
||||
double totalEnergyConsumptionTariff1(Unit u);
|
||||
double totalVolume(Unit u);
|
||||
double totalVolumeTariff2(Unit u);
|
||||
double volumeFlow(Unit u);
|
||||
double power(Unit u);
|
||||
double flowTemperature(Unit u);
|
||||
double returnTemperature(Unit u);
|
||||
double temperatureDifference(Unit u);
|
||||
|
||||
private:
|
||||
void processContent(Telegram *t);
|
||||
|
||||
double total_energy_kwh_ {};
|
||||
double total_energy_tariff1_kwh_ {};
|
||||
double total_volume_m3_ {};
|
||||
double total_volume_tariff2_m3_ {};
|
||||
double volume_flow_m3h_ {};
|
||||
double power_w_ {};
|
||||
double flow_temperature_c_ {};
|
||||
double return_temperature_c_ {};
|
||||
double temperature_difference_c_ {};
|
||||
};
|
||||
|
||||
MeterSharky::MeterSharky(MeterInfo &mi) :
|
||||
MeterCommonImplementation(mi, MeterType::SHARKY)
|
||||
{
|
||||
addLinkMode(LinkMode::T1);
|
||||
|
||||
addPrint("total_energy_consumption", Quantity::Energy,
|
||||
[&](Unit u){ return totalEnergyConsumption(u); },
|
||||
"The total energy consumption recorded by this meter.",
|
||||
true, true);
|
||||
|
||||
addPrint("total_energy_consumption_tariff1", Quantity::Energy,
|
||||
[&](Unit u){ return totalEnergyConsumptionTariff1(u); },
|
||||
"The total energy consumption recorded by this meter on tariff 1.",
|
||||
true, true);
|
||||
|
||||
addPrint("total_volume", Quantity::Volume,
|
||||
[&](Unit u){ return totalVolume(u); },
|
||||
"The total volume recorded by this meter.",
|
||||
true, true);
|
||||
|
||||
addPrint("total_volume", Quantity::Volume,
|
||||
[&](Unit u){ return totalVolumeTariff2(u); },
|
||||
"The total volume recorded by this meter on tariff 2.",
|
||||
true, true);
|
||||
|
||||
addPrint("volume_flow", Quantity::Flow,
|
||||
[&](Unit u){ return volumeFlow(u); },
|
||||
"The current flow.",
|
||||
true, true);
|
||||
|
||||
addPrint("power", Quantity::Power,
|
||||
[&](Unit u){ return power(u); },
|
||||
"The power.",
|
||||
true, true);
|
||||
|
||||
addPrint("flow_temperature", Quantity::Temperature,
|
||||
[&](Unit u){ return flowTemperature(u); },
|
||||
"The flow temperature.",
|
||||
true, true);
|
||||
|
||||
addPrint("return_temperature", Quantity::Temperature,
|
||||
[&](Unit u){ return returnTemperature(u); },
|
||||
"The return temperature.",
|
||||
true, true);
|
||||
|
||||
addPrint("temperature_difference", Quantity::Temperature,
|
||||
[&](Unit u){ return temperatureDifference(u); },
|
||||
"The temperature difference.",
|
||||
true, true);
|
||||
}
|
||||
|
||||
shared_ptr<HeatMeter> createSharky(MeterInfo &mi) {
|
||||
return shared_ptr<HeatMeter>(new MeterSharky(mi));
|
||||
}
|
||||
|
||||
double MeterSharky::totalEnergyConsumption(Unit u)
|
||||
{
|
||||
assertQuantity(u, Quantity::Energy);
|
||||
return convert(total_energy_kwh_, Unit::KWH, u);
|
||||
}
|
||||
|
||||
double MeterSharky::totalEnergyConsumptionTariff1(Unit u)
|
||||
{
|
||||
assertQuantity(u, Quantity::Energy);
|
||||
return convert(total_energy_tariff1_kwh_, Unit::KWH, u);
|
||||
}
|
||||
|
||||
double MeterSharky::totalVolume(Unit u)
|
||||
{
|
||||
assertQuantity(u, Quantity::Volume);
|
||||
return convert(total_volume_m3_, Unit::M3, u);
|
||||
}
|
||||
|
||||
double MeterSharky::totalVolumeTariff2(Unit u)
|
||||
{
|
||||
assertQuantity(u, Quantity::Volume);
|
||||
return convert(total_volume_tariff2_m3_, Unit::M3, u);
|
||||
}
|
||||
|
||||
double MeterSharky::volumeFlow(Unit u)
|
||||
{
|
||||
assertQuantity(u, Quantity::Flow);
|
||||
return convert(volume_flow_m3h_, Unit::M3H, u);
|
||||
}
|
||||
|
||||
double MeterSharky::power(Unit u)
|
||||
{
|
||||
assertQuantity(u, Quantity::Power);
|
||||
return convert(power_w_, Unit::KW, u);
|
||||
}
|
||||
|
||||
double MeterSharky::flowTemperature(Unit u)
|
||||
{
|
||||
assertQuantity(u, Quantity::Temperature);
|
||||
return convert(flow_temperature_c_, Unit::C, u);
|
||||
}
|
||||
|
||||
double MeterSharky::returnTemperature(Unit u)
|
||||
{
|
||||
assertQuantity(u, Quantity::Temperature);
|
||||
return convert(return_temperature_c_, Unit::C, u);
|
||||
}
|
||||
|
||||
double MeterSharky::temperatureDifference(Unit u)
|
||||
{
|
||||
assertQuantity(u, Quantity::Temperature);
|
||||
return convert(temperature_difference_c_, Unit::C, u);
|
||||
}
|
||||
|
||||
void MeterSharky::processContent(Telegram *t)
|
||||
{
|
||||
/*
|
||||
(wmbus) 0f: 0C dif (8 digit BCD Instantaneous value)
|
||||
(wmbus) 10: 06 vif (Energy kWh)
|
||||
(wmbus) 11: 51260000
|
||||
(wmbus) 15: 8C dif (8 digit BCD Instantaneous value)
|
||||
(wmbus) 16: 10 dife (subunit=0 tariff=1 storagenr=0)
|
||||
(wmbus) 17: 06 vif (Energy kWh)
|
||||
(wmbus) 18: 00000000
|
||||
(wmbus) 1c: 0C dif (8 digit BCD Instantaneous value)
|
||||
(wmbus) 1d: 13 vif (Volume l)
|
||||
(wmbus) 1e: 47031500
|
||||
(wmbus) 22: 8C dif (8 digit BCD Instantaneous value)
|
||||
(wmbus) 23: 20 dife (subunit=0 tariff=2 storagenr=0)
|
||||
(wmbus) 24: 13 vif (Volume l)
|
||||
(wmbus) 25: 18000000
|
||||
(wmbus) 29: 8C dif (8 digit BCD Instantaneous value)
|
||||
(wmbus) 2a: 40 dife (subunit=1 tariff=0 storagenr=0)
|
||||
(wmbus) 2b: 13 vif (Volume l)
|
||||
(wmbus) 2c: 00000000
|
||||
(wmbus) 30: 8C dif (8 digit BCD Instantaneous value)
|
||||
(wmbus) 31: 80 dife (subunit=0 tariff=0 storagenr=0)
|
||||
(wmbus) 32: 40 dife (subunit=2 tariff=0 storagenr=0)
|
||||
(wmbus) 33: 13 vif (Volume l)
|
||||
(wmbus) 34: 00000000
|
||||
(wmbus) 38: 02 dif (16 Bit Integer/Binary Instantaneous value)
|
||||
(wmbus) 39: FD vif (Second extension FD of VIF-codes)
|
||||
(wmbus) 3a: 17 vife (Error flags (binary))
|
||||
(wmbus) 3b: 0000
|
||||
(wmbus) 3d: 0B dif (6 digit BCD Instantaneous value)
|
||||
(wmbus) 3e: 3B vif (Volume flow l/h)
|
||||
(wmbus) 3f: 000000
|
||||
(wmbus) 42: 0C dif (8 digit BCD Instantaneous value)
|
||||
(wmbus) 43: 2B vif (Power W)
|
||||
(wmbus) 44: 00000000
|
||||
(wmbus) 48: 0A dif (4 digit BCD Instantaneous value)
|
||||
(wmbus) 49: 5A vif (Flow temperature 10⁻¹ °C)
|
||||
(wmbus) 4a: 2304
|
||||
(wmbus) 4c: 0A dif (4 digit BCD Instantaneous value)
|
||||
(wmbus) 4d: 5E vif (Return temperature 10⁻¹ °C)
|
||||
(wmbus) 4e: 8102
|
||||
(wmbus) 50: 0A dif (4 digit BCD Instantaneous value)
|
||||
(wmbus) 51: 62 vif (Temperature difference 10⁻¹ K)
|
||||
(wmbus) 52: 4101
|
||||
*/
|
||||
|
||||
int offset;
|
||||
string key;
|
||||
|
||||
if (findKey(MeasurementType::Instantaneous, ValueInformation::EnergyWh, 0, 0, &key, &t->values)) {
|
||||
extractDVdouble(&t->values, key, &offset, &total_energy_kwh_);
|
||||
t->addMoreExplanation(offset, " total energy consumption (%f kWh)", total_energy_kwh_);
|
||||
}
|
||||
|
||||
if (findKey(MeasurementType::Instantaneous, ValueInformation::EnergyWh, 0, 1, &key, &t->values)) {
|
||||
extractDVdouble(&t->values, key, &offset, &total_energy_tariff1_kwh_);
|
||||
t->addMoreExplanation(offset, " total energy tariff 1 (%f kwh)", total_energy_tariff1_kwh_);
|
||||
}
|
||||
|
||||
if (findKey(MeasurementType::Instantaneous, ValueInformation::Volume, 0, 0, &key, &t->values)) {
|
||||
extractDVdouble(&t->values, key, &offset, &total_volume_m3_);
|
||||
t->addMoreExplanation(offset, " total volume (%f ㎥)", total_volume_m3_);
|
||||
}
|
||||
|
||||
if (findKey(MeasurementType::Instantaneous, ValueInformation::Volume, 0, 2, &key, &t->values)) {
|
||||
extractDVdouble(&t->values, key, &offset, &total_volume_tariff2_m3_);
|
||||
t->addMoreExplanation(offset, " total volume tariff 2 (%f ㎥)", total_volume_tariff2_m3_);
|
||||
}
|
||||
|
||||
if (findKey(MeasurementType::Instantaneous, ValueInformation::VolumeFlow, 0, 0, &key, &t->values)) {
|
||||
extractDVdouble(&t->values, key, &offset, &volume_flow_m3h_);
|
||||
t->addMoreExplanation(offset, " volume flow (%f ㎥/h)", volume_flow_m3h_);
|
||||
}
|
||||
|
||||
if (findKey(MeasurementType::Instantaneous, ValueInformation::PowerW, 0, 0, &key, &t->values)) {
|
||||
extractDVdouble(&t->values, key, &offset, &power_w_);
|
||||
t->addMoreExplanation(offset, " power (%f W)", power_w_);
|
||||
}
|
||||
|
||||
if (findKey(MeasurementType::Instantaneous, ValueInformation::FlowTemperature, 0, 0, &key, &t->values)) {
|
||||
extractDVdouble(&t->values, key, &offset, &flow_temperature_c_);
|
||||
t->addMoreExplanation(offset, " flow temperature (%f °C)", flow_temperature_c_);
|
||||
}
|
||||
|
||||
if (findKey(MeasurementType::Instantaneous, ValueInformation::ReturnTemperature, 0, 0, &key, &t->values)) {
|
||||
extractDVdouble(&t->values, key, &offset, &return_temperature_c_);
|
||||
t->addMoreExplanation(offset, " return temperature (%f °C)", return_temperature_c_);
|
||||
}
|
||||
|
||||
if (findKey(MeasurementType::Instantaneous, ValueInformation::TemperatureDifference, 0, 0, &key, &t->values)) {
|
||||
extractDVdouble(&t->values, key, &offset, &temperature_difference_c_);
|
||||
t->addMoreExplanation(offset, " temperature difference (%f °C)", temperature_difference_c_);
|
||||
}
|
||||
}
|
||||
|
|
@ -67,6 +67,7 @@
|
|||
X(tsd2, T1_bit, SmokeDetector, TSD2, TSD2) \
|
||||
X(q400, T1_bit, WaterMeter, Q400, Q400) \
|
||||
X(qcaloric, C1_bit, HeatCostAllocationMeter, QCALORIC, QCaloric) \
|
||||
X(sharky, T1_bit, HeatMeter, SHARKY, Sharky) \
|
||||
X(sontex868, T1_bit, HeatCostAllocationMeter, SONTEX868, Sontex868) \
|
||||
X(supercom587,T1_bit, WaterMeter, SUPERCOM587, Supercom587) \
|
||||
X(ultrimis, T1_bit, WaterMeter, ULTRIMIS, Ultrimis) \
|
||||
|
@ -150,6 +151,7 @@
|
|||
X(TSD2, MANUFACTURER_TCH, 0xf0, 0x76) \
|
||||
X(Q400, MANUFACTURER_AXI, 0x07, 0x10) \
|
||||
X(QCALORIC, MANUFACTURER_QDS, 0x08, 0x35) \
|
||||
X(SHARKY, MANUFACTURER_HYD, 0x04, 0x20) \
|
||||
X(SUPERCOM587,MANUFACTURER_SON, 0x06, 0x3c) \
|
||||
X(SUPERCOM587,MANUFACTURER_SON, 0x07, 0x3c) \
|
||||
X(SONTEX868, MANUFACTURER_SON, 0x08, 0x16) \
|
||||
|
|
25
src/wmbus.cc
25
src/wmbus.cc
|
@ -1430,6 +1430,16 @@ bool Telegram::potentiallyDecrypt(vector<uchar>::iterator &pos)
|
|||
}
|
||||
addExplanationAndIncrementPos(pos, 2, "%02x%02x decrypt check bytes", *(pos+0), *(pos+1));
|
||||
}
|
||||
else if (tpl_sec_mode == TPLSecurityMode::SPECIFIC_16_31)
|
||||
{
|
||||
if (mustDecryptDiehlRealData(frame))
|
||||
{
|
||||
if (!meter_keys) return false;
|
||||
bool ok = decryptDielhRealData(this, frame, pos, meter_keys->confidentiality_key);
|
||||
if (!ok) return false;
|
||||
// Now the frame from pos and onwards has been decrypted.
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -1582,18 +1592,11 @@ bool Telegram::parseTPL(vector<uchar>::iterator &pos)
|
|||
|
||||
void Telegram::preProcess()
|
||||
{
|
||||
if (frame.size() >= 15)
|
||||
DiehlAddressTransformMethod diehl_method = mustTransformDiehlAddress(frame);
|
||||
if (diehl_method != DiehlAddressTransformMethod::NONE)
|
||||
{
|
||||
uchar c_field = frame[1];
|
||||
int m_field = frame[3] <<8 | frame[2];
|
||||
uchar ci_field = frame[10];
|
||||
int tpl_cfg = frame[14] <<8 | frame[13];
|
||||
DiehlAddressTransformMethod diehl_method = mustTransformDiehlAddress(c_field, m_field, ci_field, tpl_cfg);
|
||||
if (diehl_method != DiehlAddressTransformMethod::NONE)
|
||||
{
|
||||
original = vector<uchar>(frame.begin(), frame.begin() + 10);
|
||||
transformDiehlAddress(frame, diehl_method);
|
||||
}
|
||||
original = vector<uchar>(frame.begin(), frame.begin() + 10);
|
||||
transformDiehlAddress(frame, diehl_method);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@ PROG="$1"
|
|||
mkdir -p testoutput
|
||||
TEST=testoutput
|
||||
|
||||
TESTNAME="Test Izas meters"
|
||||
TESTNAME="Test Izars meters"
|
||||
TESTRESULT="ERROR"
|
||||
LOGFILE=$TEST/logfile
|
||||
LOGFILE_EXPECTED=$TEST/logfile_expected
|
||||
|
@ -14,8 +14,8 @@ mkdir -p $TEST
|
|||
rm -f $LOGFILE
|
||||
|
||||
METERS="IzarWater izar 21242472 NOKEY
|
||||
IzarWater2 izar 23662907 NOKEY
|
||||
IzarWater3 izar 48197907 NOKEY
|
||||
IzarWater2 izar 66236629 NOKEY
|
||||
IzarWater3 izar 20481979 NOKEY
|
||||
IzarWater4 izar 2124589c NOKEY"
|
||||
|
||||
cat simulations/simulation_izars.txt | grep '^{' > $TEST/test_expected.txt
|
||||
|
@ -43,7 +43,7 @@ then
|
|||
exit 1
|
||||
fi
|
||||
|
||||
TESTNAME="Test listen to all izas"
|
||||
TESTNAME="Test listen to all izars"
|
||||
TESTRESULT="ERROR"
|
||||
|
||||
cat > $LOGFILE_EXPECTED <<EOF
|
||||
|
@ -53,12 +53,12 @@ Received telegram from: 21242472
|
|||
type: Water meter (0x07)
|
||||
ver: 0x00
|
||||
driver: izar
|
||||
Received telegram from: 23662907
|
||||
Received telegram from: 66236629
|
||||
manufacturer: (DME) DIEHL Metering, Germany (0x11a5)
|
||||
type: Water meter (0x07)
|
||||
ver: 0x78
|
||||
driver: izar
|
||||
Received telegram from: 48197907
|
||||
Received telegram from: 20481979
|
||||
manufacturer: (DME) DIEHL Metering, Germany (0x11a5)
|
||||
type: Water meter (0x07)
|
||||
ver: 0x78
|
||||
|
|
|
@ -241,6 +241,11 @@ Received telegram from: 20100117
|
|||
type: Electricity meter (0x02)
|
||||
ver: 0x01
|
||||
driver: gransystems
|
||||
Received telegram from: 68926025
|
||||
manufacturer: (HYD) Hydrometer (0x2324)
|
||||
type: Heat meter (0x04)
|
||||
ver: 0x20
|
||||
driver: sharky
|
||||
EOF
|
||||
|
||||
RES=$($PROG --logfile=$LOGFILE --t1 simulations/simulation_t1.txt 2>&1)
|
||||
|
|
|
@ -48,6 +48,7 @@ METERS="MyWarmWater supercom587 12345678 NOKEY
|
|||
Voda ev200 99993030 NOKEY
|
||||
Vodda emerlin868 95949392 NOKEY
|
||||
Smokey tsd2 91633569 NOKEY
|
||||
Sharky775 sharky 68926025 NOKEY
|
||||
Heating compact5 62626262 NOKEY"
|
||||
|
||||
|
||||
|
|
Ładowanie…
Reference in New Issue