diff --git a/.github/renovate.json b/.github/renovate.json
index 328cb592..d40dce06 100644
--- a/.github/renovate.json
+++ b/.github/renovate.json
@@ -10,10 +10,20 @@
"fileFormat": "yaml",
"customType": "jsonata",
"matchStrings": [
- "jobs.build.steps[id=\"esphome_build\"].{\"currentValue\":with.version}"
+ "{\"currentValue\": env.ESPHOME_VERSION}"
],
"packageNameTemplate": "esphome/esphome",
"datasourceTemplate": "github-releases"
+ },
+ {
+ "managerFilePatterns": [".github/workflows/build.yaml"],
+ "fileFormat": "yaml",
+ "customType": "jsonata",
+ "matchStrings": [
+ "{\"currentValue\": env.PYTHON_VERSION}"
+ ],
+ "depNameTemplate": "python",
+ "datasourceTemplate": "python-version"
}
]
}
diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml
index fbe8cf3b..d83ddc0c 100644
--- a/.github/workflows/build.yaml
+++ b/.github/workflows/build.yaml
@@ -2,6 +2,10 @@ name: Compilation check
on:
pull_request:
+env:
+ ESPHOME_VERSION: "2026.9.1"
+ PYTHON_VERSION: "3.14"
+
jobs:
build:
runs-on: ubuntu-latest
@@ -23,5 +27,23 @@ jobs:
uses: esphome/build-action@v8.1.0
with:
yaml-file: tests/${{ matrix.config }}.yaml
- version: "2026.9.1"
+ version: ${{ env.ESPHOME_VERSION }}
substitutions: variant=${{ matrix.variant }}
+
+ telegrams:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v7
+ - name: Set up Python
+ uses: actions/setup-python@v6
+ with:
+ python-version: ${{ env.PYTHON_VERSION }}
+ - name: Install esphome
+ run: pip install "esphome==$ESPHOME_VERSION" pytest
+ - name: Build the telegram runner
+ run: |
+ esphome compile tests/wmbus/host/test.yaml
+ install -D "$(esphome idedata tests/wmbus/host/test.yaml | jq -r .prog_path)" ~/.local/bin/wmbus-test-runner
+ - name: Decode the upstream telegrams
+ run: pytest tests/wmbus/host -q
diff --git a/.gitignore b/.gitignore
index dc7a18ed..b3a59aea 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,6 @@
__pycache__
.vscode
.esphome
+secrets.yaml
+tests/**/.gitignore
+.pytest_cache
diff --git a/README.md b/README.md
index 3032c8e4..69f05168 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
# ESPHome Components
-
+

This repository contains custom [ESPHome](https://esphome.io/) components for devices developed by [IoTLabs](https://iotlabs.pl).
@@ -74,7 +74,7 @@ interval:
## `wmbus_common`
-This component is a port of [wmbusmeters](https://wmbusmeters.org/) library to ESPHome. It provides common functionality for working with wM-Bus devices, including parsing and handling wM-Bus packets.
+This component decodes wM-Bus telegrams with the driver definitions of [wmbusmeters](https://wmbusmeters.org/), compiled into the firmware at build time. See [components/wmbus_common/README.md](components/wmbus_common/README.md) for details.
**Example configuration:**
@@ -99,13 +99,7 @@ wmbus_common:
If you use `wmbus_meter` component, you may not need to configure `wmbus_common` component, as it will be automatically included as a dependency. Dependent components will automatically load required drivers and fields based on their configuration, so specifying them on this level is redundant.
-However, if you want to load specific drivers or fields that are not required by any dependent component, you should specify them in `wmbus_common` configuration.
-
-`wmbusmeters` is included as a git subtree. To sync version from upstream repository, run:
-
-```bash
-./scripts/pull_wmbusmeters.py [GIT_REF]
-```
+However, if you want to load specific drivers or fields that are not required by any dependent component, you should specify them in `wmbus_common` configuration. These are also the drivers a meter with `type: auto` chooses from.
## `wmbus_meter`
@@ -120,7 +114,6 @@ wmbus_meter:
meter_id: 12345678
type: apator162
key: "00000000000000000000000000000000"
- mode: T1
on_telegram:
- socket_transmitter.send:
id: transmitter
@@ -129,7 +122,7 @@ wmbus_meter:
topic: test/topic
```
-`mode` parameter is optional and allows to filter received packets by mode. It can be set to `T1` or `C1`. If not set, all packets will be processed.
+`type` is the driver name, or `auto` to detect the driver from the first telegram among the drivers listed in `wmbus_common`.
`key` parameter is optional and allows to decrypt packets using AES-128-CBC encryption. It should be provided as hexadecimal or ASCII encoded string. If not set, packets will be processed as unencrypted.
**Attention!**
@@ -138,6 +131,8 @@ By default, component will not load any fields to reduce memory footprint. If yo
Component provides `on_telegram` trigger that can be used to send data to a remote server or process it in any other way. It can be used to send data to MQTT broker, HTTP server, or any other service. `meter` variable is available in the following lambdas.
Additionally, `wmbus_meter.send_telegram_with_mqtt` action can be used to send JSON-encoded meter data to MQTT broker. It requires `mqtt` component to be configured in ESPHome.
+`meter->as_json()` returns the media and the meter's fields: `{"media":"water","total_m3":123.456,"status":"OK"}`.
+
On the `wmbus_meter` platform, you can use the following sensors to provide data to Home Assistant/MQTT:
- **sensor**: as abstraction for numerical values from the meter, such as consumption, volume, etc.
@@ -151,7 +146,7 @@ On the `wmbus_meter` platform, you can use the following sensors to provide data
field: total_m3
```
- By default, unit of measurement is picked from the field name, but you can override it by specifying `unit_of_measurement` parameter. Especially, if you want to do this, ESPHome's `multiply` filter may be useful to change numerical value to the desired unit.
+ By default, unit of measurement and accuracy decimals are picked from the field name, but you can override them. To publish the value in another unit, ESPHome's `multiply` filter may be useful.
- **text_sensor**
```yaml
@@ -163,6 +158,8 @@ On the `wmbus_meter` platform, you can use the following sensors to provide data
field: timestamp
```
+ Besides the driver's fields, `timestamp` and `media` are available.
+
When you use `sensor` and `text_sensor` platforms, required fields will be automatically loaded for corresponding driver, so you don't need to specify them in `wmbus_common` component configuration.
For both `sensor` and `text_sensor`, all config from generic [Sensor](https://esphome.io/components/sensor/index.html) and [Text Sensor](https://esphome.io/components/text_sensor/index.html) components is available, so you can use filters, icons, etc.
diff --git a/components/wmbus_common/.wmbusmeters_tag b/components/wmbus_common/.wmbusmeters_tag
index f1e7f065..649f3d54 100644
--- a/components/wmbus_common/.wmbusmeters_tag
+++ b/components/wmbus_common/.wmbusmeters_tag
@@ -1 +1 @@
-1.20.0
\ No newline at end of file
+3.0.0-188-gac3d579
diff --git a/components/wmbus_common/README.md b/components/wmbus_common/README.md
new file mode 100644
index 00000000..c1898c40
--- /dev/null
+++ b/components/wmbus_common/README.md
@@ -0,0 +1,22 @@
+# `wmbus_common`
+
+The wM-Bus decoder behind `wmbus_radio` and `wmbus_meter`, built on [wmbusmeters](https://github.com/wmbusmeters/wmbusmeters).
+
+`drivers/src/*.xmq` are wmbusmeters' driver definitions and `tables/*.yaml` its unit and VIF tables, both verbatim from the ref in `.wmbusmeters_tag`. `drivers/loader/` turns the drivers into C++ tables at codegen time; no upstream C++ is compiled.
+
+## Licence
+
+wmbusmeters is GPL-3.0, and so is this repository (see [LICENSE](../../LICENSE)). Please credit [wmbusmeters](https://wmbusmeters.org/) when you use this.
+
+## Updating upstream
+
+```bash
+./scripts/wmbus_common/fetch_upstream.py --diff master # upstream's C++ changes since the recorded ref
+./scripts/wmbus_common/fetch_upstream.py master # a tag, branch or commit of wmbusmeters
+python3 scripts/wmbus_common/check_drivers.py # which drivers the generator handles
+esphome compile tests/wmbus/host/test.yaml # upstream's test telegrams still decode:
+install -D "$(esphome idedata tests/wmbus/host/test.yaml | jq -r .prog_path)" ~/.local/bin/wmbus-test-runner
+pytest tests/wmbus/host
+```
+
+The fetch commits nothing: review `git diff`, run the checks, then commit or revert.
diff --git a/components/wmbus_common/__init__.py b/components/wmbus_common/__init__.py
index 9b16547f..c145155c 100644
--- a/components/wmbus_common/__init__.py
+++ b/components/wmbus_common/__init__.py
@@ -6,21 +6,25 @@
from esphome.core import CORE
from esphome.yaml_util import ESPHomeDumper
-from .driver_loader import CppDriver, Driver, DriverManager
+from .drivers.loader import GENERATED_DIR, AutoDriver, Driver, DriverManager, validate_driver_name
CODEOWNERS = ["@kubasaw"]
+# to_json renders through ArduinoJson.
+AUTO_LOAD = ["json"]
+
+UPSTREAM_REF = (Path(__file__).parent / ".wmbusmeters_tag").read_text().strip()
+
CONF_ALL = "all"
CONF_DRIVERS = "drivers"
CONF_FIELDS = "fields"
-CURRENT_DIR = Path(__file__).parent
-
wmbus_common_ns = cg.esphome_ns.namespace("wmbus_common")
WMBusCommon = wmbus_common_ns.class_("WMBusCommon", cg.Component)
-ESPHomeDumper.add_multi_representer(
- Driver, lambda s, v: s.represent_stringify(v.name)
-)
+for driver_class in (Driver, AutoDriver):
+ ESPHomeDumper.add_multi_representer(
+ driver_class, lambda s, v: s.represent_stringify(v.name)
+ )
def maybe_all(replacements):
@@ -37,7 +41,7 @@ def driver_field_validator(conf):
driver = conf[CONF_NAME]
conf[CONF_FIELDS] = cv.All(
- maybe_all(driver.available_fields),
+ maybe_all(driver.available_fields()),
[driver.request_field],
)(conf[CONF_FIELDS])
@@ -46,9 +50,7 @@ def driver_field_validator(conf):
DRIVER_ENTRY_SCHEMA = cv.maybe_simple_value(
{
- cv.Required(CONF_NAME): cv.All(
- cv.one_of(*DriverManager.available_drivers), DriverManager.request_driver
- ),
+ cv.Required(CONF_NAME): validate_driver_name,
cv.Optional(CONF_FIELDS, default=CONF_ALL): cv.valid,
},
driver_field_validator,
@@ -65,22 +67,27 @@ def driver_field_validator(conf):
)
+def validate_auto_has_candidates(config):
+ if DriverManager.auto_requested and not DriverManager.registered_drivers:
+ raise cv.Invalid(
+ "a meter with 'type: auto' picks among the drivers this firmware "
+ "compiles, and this one compiles none -- name them under "
+ "'wmbus_common: drivers:'"
+ )
+
+ return config
+
+
+FINAL_VALIDATE_SCHEMA = validate_auto_has_candidates
+
+
async def to_code(config):
- cg.add_define(
- "WMBUSMETERS_TAG",
- CURRENT_DIR.joinpath(".wmbusmeters_tag").read_text(),
- )
+ # Host AES comes from OpenSSL; ESP32 uses mbedTLS.
+ if CORE.is_host:
+ cg.add_build_flag("-lcrypto")
- target_dir = CORE.relative_src_path("wmbusmeters_drivers")
- DriverManager.sync_to_directory(target_dir)
+ cg.add_define("WMBUSMETERS_VERSION", UPSTREAM_REF)
+ DriverManager.sync_to_directory(CORE.relative_src_path(GENERATED_DIR))
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
-
-
-def FILTER_SOURCE_FILES() -> list[str]:
- return [
- d.source_path.name
- for d in DriverManager._all_drivers.values()
- if isinstance(d, CppDriver)
- ]
diff --git a/components/wmbus_common/address.cpp b/components/wmbus_common/address.cpp
deleted file mode 100644
index 12613fa8..00000000
--- a/components/wmbus_common/address.cpp
+++ /dev/null
@@ -1,654 +0,0 @@
-/*
- Copyright (C) 2017-2024 Fredrik Öhrström (gpl-3.0-or-later)
-
- 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 .
-*/
-
-#include"address.h"
-#include"manufacturers.h"
-
-#include
-#include
-#include
-
-std::vector splitSequenceOfAddressExpressionsAtCommas(const std::string& mes);
-bool isValidMatchExpression(const std::string& s, bool *has_wildcard);
-bool doesIdMatchExpression(const std::string& id, std::string match_rule);
-bool doesAddressMatchExpressions(Address &address,
- std::vector& address_expressions,
- bool *used_wildcard,
- bool *filtered_out,
- bool *required_found,
- bool *required_failed);
-
-bool isValidMatchExpression(const std::string& s, bool *has_wildcard)
-{
- std::string me = s;
-
- // Examples of valid match expressions:
- // 12345678
- // *
- // 123*
- // !12345677
- // 2222222*
- // !22222222
- // We also accept an secondary libmbus address:
- // 100002842941011B
-
- // A match expression cannot be empty.
- if (me.length() == 0) return false;
-
- // An me can be filtered out with an exclamation mark first.
- if (me.front() == '!') me.erase(0, 1);
-
- // More than one negation is not allowed.
- if (me.front() == '!') return false;
-
- // A match expression cannot be only a negation mark.
- if (me.length() == 0) return false;
-
- int count = 0;
- // Some non-compliant meters have full hex in the id,
- // but according to the standard there should only be bcd here...
- // We accept hex anyway.
- while (me.length() > 0 &&
- ((me.front() >= '0' && me.front() <= '9') ||
- (me.front() >= 'A' && me.front() <= 'F') ||
- (me.front() >= 'a' && me.front() <= 'f')))
- {
- me.erase(0,1);
- count++;
- }
-
- if (me.length() == 0 && count == 16)
- {
- // A secondary libmbus address: 100002842941011B
- // Strictly speaking the leading 8 digits should be bcd,
- // but we accept hex as well.
- *has_wildcard = false;
- return true;
- }
-
- bool wildcard_used = false;
- // An expression can end with a *
- if (me.length() > 0 && me.front() == '*')
- {
- me.erase(0,1);
- wildcard_used = true;
- if (has_wildcard) *has_wildcard = true;
- }
-
- // Now we should have eaten the whole expression.
- if (me.length() > 0) return false;
-
- // Check the length of the matching bcd/hex
- // If no wildcard is used, then the match expression must be exactly 8 digits.
- if (!wildcard_used) return count == 8;
-
- // If wildcard is used, then the match expressions must be 7 or less digits,
- // even zero is allowed which means a single *, which matches any bcd/hex id.
- return count <= 7;
-}
-
-std::vector splitSequenceOfAddressExpressionsAtCommas(const std::string& mes)
-{
- std::vector r;
- bool eof, err;
- std::vector v (mes.begin(), mes.end());
- auto i = v.begin();
-
- for (;;) {
- auto id = eatTo(v, i, ',', 64, &eof, &err);
- if (err) break;
- trimWhitespace(&id);
- if (id == "ANYID") id = "*";
- r.push_back(id);
- if (eof) break;
- }
- return r;
-}
-
-bool isValidSequenceOfAddressExpressions(const std::string& mes)
-{
- std::vector v = splitSequenceOfAddressExpressionsAtCommas(mes);
-
- for (std::string me : v)
- {
- AddressExpression ae;
- if (!ae.parse(me)) return false;
- }
- return true;
-}
-
-std::vector splitAddressExpressions(const std::string &aes)
-{
- std::vector v = splitSequenceOfAddressExpressionsAtCommas(aes);
-
- std::vector r;
-
- for (std::string me : v)
- {
- AddressExpression ae;
- if (ae.parse(me))
- {
- r.push_back(ae);
- }
- }
- return r;
-}
-
-bool doesIdMatchExpression(const std::string& s, std::string match)
-{
- std::string id = s;
- if (id.length() == 0) return false;
-
- // Here we assume that the match expression has been
- // verified to be valid.
- bool can_match = true;
-
- // Now match bcd/hex until end of id, or '*' in match.
- while (id.length() > 0 && match.length() > 0 && match.front() != '*')
- {
- if (id.front() != match.front())
- {
- // We hit a difference, it cannot match.
- can_match = false;
- break;
- }
- id.erase(0,1);
- match.erase(0,1);
- }
-
- bool wildcard_used = false;
- if (match.length() && match.front() == '*')
- {
- wildcard_used = true;
- match.erase(0,1);
- }
-
- if (can_match)
- {
- // Ok, now the match expression should be empty.
- // If wildcard is true, then the id can still have digits,
- // otherwise it must also be empty.
- if (wildcard_used)
- {
- can_match = match.length() == 0;
- }
- else
- {
- can_match = match.length() == 0 && id.length() == 0;
- }
- }
-
- return can_match;
-}
-
-bool hasWildCard(const std::string& mes)
-{
- return mes.find('*') != std::string::npos;
-}
-
-bool AddressExpression::match(const std::string &i, uint16_t m, uchar v, uchar t)
-{
- if (!(mfct == 0xffff || mfct == m)) return false;
- if (!(version == 0xff || version == v)) return false;
- if (!(type == 0xff || type == t)) return false;
- if (!doesIdMatchExpression(i, id)) return false;
-
- return true;
-}
-
-void AddressExpression::trimToIdentity(IdentityMode im, Address &a)
-{
- switch (im)
- {
- case IdentityMode::FULL:
- id = a.id;
- mfct = a.mfct;
- version = a.version;
- type = a.type;
- required = true;
- break;
- case IdentityMode::ID_MFCT:
- id = a.id;
- mfct = a.mfct;
- version = 0xff;
- type = 0xff;
- required = true;
- break;
- case IdentityMode::ID:
- id = a.id;
- mfct = 0xffff;
- version = 0xff;
- type = 0xff;
- required = true;
- break;
- default:
- break;
- }
-}
-
-bool AddressExpression::parse(const std::string &in)
-{
- std::string s = in;
- // Example: 12345678
- // or 12345678.M=PII.T=1B.V=01
- // or 1234*
- // or 1234*.M=PII
- // or 1234*.V=01
- // or 12 // mbus primary
- // or 0 // mbus primary
- // or 250.MPII.V01.T1B // mbus primary
- // or !12345678
- // or !*.M=ABC
- // or libmbus secondary style:
- // 123456782941011B
- id = "";
- mbus_primary = false;
- mfct = 0xffff;
- type = 0xff;
- version = 0xff;
- filter_out = false;
-
- if (s.size() == 0) return false;
-
- if (s.size() > 1 && s[0] == '!')
- {
- filter_out = true;
- s = s.substr(1);
- // Double ! not allowed.
- if (s.size() > 1 && s[0] == '!') return false;
- }
- std::vector parts = splitString(s, '.');
-
- assert(parts.size() > 0);
-
- id = parts[0];
- if (!isValidMatchExpression(id, &has_wildcard))
- {
- // Not a long id, so lets check if it is p0 to p250 for primary mbus ids.
- if (id.size() < 2) return false;
- if (id[0] != 'p') return false;
- for (size_t i=1; i < id.length(); ++i)
- {
- if (!isdigit(id[i])) return false;
- }
- // All digits good.
- int v = atoi(id.c_str()+1);
- if (v < 0 || v > 250) return false;
- // It is 0-250 which means it is an mbus primary address.
- mbus_primary = true;
- }
-
- if (parts.size() == 1 && id.length() == 16)
- {
- // This is a secondary libmbus address.
- std::string mfct_hex = id.substr(8,4);
- std::string version_hex = id.substr(12,2);
- std::string type_hex = id.substr(14,2);
- id = id.substr(0,8);
-
- std::vector data;
- bool ok = hex2bin(mfct_hex.c_str(), &data);
- if (!ok) return false;
- if (data.size() != 2) return false;
- mfct = data[1] << 8 | data[0];
-
- data.clear();
- ok = hex2bin(version_hex.c_str(), &data);
- if (!ok) return false;
- if (data.size() != 1) return false;
- version = data[0];
-
- data.clear();
- ok = hex2bin(type_hex.c_str(), &data);
- if (!ok) return false;
- if (data.size() != 1) return false;
- type = data[0];
-
- return true;
- }
-
- for (size_t i=1; i data;
- bool ok = hex2bin(&parts[i][2], &data);
- if (!ok) return false;
- if (data.size() != 1) return false;
-
- if (parts[i][0] == 'V')
- {
- version = data[0];
- }
- else if (parts[i][0] == 'T')
- {
- type = data[0];
- }
- else
- {
- return false;
- }
- }
- else if (parts[i].size() == 5) // M=xyz
- {
- if (parts[i][1] != '=') return false;
- if (parts[i][0] != 'M') return false;
-
- bool ok = flagToManufacturer(&parts[i][2], &mfct);
- if (!ok) return false;
- }
- else if (parts[i].size() == 6) // M=abcd explicit hex version
- {
- if (parts[i][1] != '=') return false;
- if (parts[i][0] != 'M') return false;
-
- std::vector data;
- bool ok = hex2bin(&parts[i][2], &data);
- if (!ok) return false;
- if (data.size() != 2) return false;
-
- mfct = data[1] << 8 | data[0];
- if (!ok) return false;
- }
- else
- {
- return false;
- }
- }
-
- return true;
-}
-
-bool flagToManufacturer(const char *s, uint16_t *out_mfct)
-{
- if (s[0] == 0 || s[1] == 0 || s[2] == 0 || s[3] != 0) return false;
- if (s[0] < '@' || s[0] > 'Z' ||
- s[1] < '@' || s[1] > 'Z' ||
- s[2] < '@' || s[2] > 'Z') return false;
-
- *out_mfct = MANFCODE(s[0],s[1],s[2]);
- return true;
-}
-
-std::string AddressExpression::str()
-{
- std::string s;
-
- if (filter_out) s = "!";
- if (required) s = "R";
-
- s.append(id);
- if (mfct != 0xffff)
- {
- s += ".M="+manufacturerFlag(mfct);
- }
- if (version != 0xff)
- {
- s += ".V="+tostrprintf("%02x", version);
- }
- if (type != 0xff)
- {
- s += ".T="+tostrprintf("%02x", type);
- }
-
- return s;
-}
-
-std::string Address::str()
-{
- std::string s;
-
- s.append(id);
- if (mfct != 0xffff)
- {
- s += ".M="+manufacturerFlag(mfct);
- }
- if (version != 0xff)
- {
- s += ".V="+tostrprintf("%02x", version);
- }
- if (type != 0xff)
- {
- s += ".T="+tostrprintf("%02x", type);
- }
-
- return s;
-}
-
-std::string Address::concat(std::vector &addresses)
-{
- std::string s;
- for (Address& a: addresses)
- {
- if (s.size() > 0) s.append(",");
- s.append(a.str());
- }
- return s;
-}
-
-std::string AddressExpression::concat(std::vector &address_expressions)
-{
- std::string s;
- for (AddressExpression& a: address_expressions)
- {
- if (s.size() > 0) s.append(",");
- s.append(a.str());
- }
- return s;
-}
-
-std::string manufacturerFlag(int m_field) {
- char a = (m_field/1024)%32+64;
- char b = (m_field/32)%32+64;
- char c = (m_field)%32+64;
-
- std::string flag;
- flag += a;
- flag += b;
- flag += c;
- return flag;
-}
-
-void Address::decodeMfctFirst(const std::vector::iterator &pos)
-{
- mfct = *(pos+1) << 8 | *(pos+0);
- id = tostrprintf("%02x%02x%02x%02x", *(pos+5), *(pos+4), *(pos+3), *(pos+2));
- version = *(pos+6);
- type = *(pos+7);
-}
-
-void Address::decodeIdFirst(const std::vector::iterator &pos)
-{
- id = tostrprintf("%02x%02x%02x%02x", *(pos+3), *(pos+2), *(pos+1), *(pos+0));
- mfct = *(pos+5) << 8 | *(pos+4);
- version = *(pos+6);
- type = *(pos+7);
-}
-
-bool doesTelegramMatchExpressions(std::vector &addresses,
- std::vector& address_expressions,
- bool *used_wildcard)
-{
- bool match = false;
- bool filtered_out = false;
- bool required_found = false; // An R12345678 field was found.
- bool required_failed = true; // Init to fail, set to true if R is satistifed anywhere.
-
- for (Address &a : addresses)
- {
- if (doesAddressMatchExpressions(a,
- address_expressions,
- used_wildcard,
- &filtered_out,
- &required_found,
- &required_failed))
- {
- match = true;
- }
- // Go through all ids even though there is an early match.
- // This way we can see if theres an exact match later.
- }
- // If any expression triggered a filter out, then the whole telegram does not match.
- if (filtered_out) match = false;
- // If a required field was found and it failed....
- if (required_found && required_failed) match = false;
- return match;
-}
-
-bool doesAddressMatchExpressions(Address &address,
- std::vector& address_expressions,
- bool *used_wildcard,
- bool *filtered_out,
- bool *required_found,
- bool *required_failed)
-{
- bool found_match = false;
- bool found_negative_match = false;
- bool exact_match = false;
-
- // Goes through all possible match expressions.
- // If no expression matches, neither positive nor negative,
- // then the result is false. (ie no match)
-
- // If more than one positive match is found, and no negative,
- // then the result is true.
-
- // If more than one negative match is found, irrespective
- // if there is any positive matches or not, then the result is false.
-
- // If a positive match is found, using a wildcard not any exact match,
- // then *used_wildcard is set to true.
-
- // If an expression is required and it fails, then the match fails.
- for (AddressExpression &ae : address_expressions)
- {
- bool has_wildcard = ae.has_wildcard;
- bool is_negative_rule = ae.filter_out;
- // We currently assume that only a single expression is required, the last one!
- bool is_required = ae.required;
-
- if (is_required) *required_found = true;
-
- bool m = ae.match(address.id, address.mfct, address.version, address.type);
-
- if (is_negative_rule)
- {
- if (m) found_negative_match = true;
- }
- else
- {
- if (m)
- {
- // A match, but the required does not count.
- if (!is_required)
- {
- found_match = true;
- if (!has_wildcard)
- {
- exact_match = true;
- }
- }
- else
- {
- *required_failed = false;
- }
- }
- }
- }
- if (found_negative_match)
- {
- *filtered_out = true;
- return false;
- }
- if (found_match)
- {
- if (exact_match)
- {
- *used_wildcard = false;
- }
- else
- {
- *used_wildcard = true;
- }
- return true;
- }
- return false;
-}
-
-const char *toString(IdentityMode im)
-{
- switch (im)
- {
- case IdentityMode::ID: return "id";
- case IdentityMode::ID_MFCT: return "id-mfct";
- case IdentityMode::FULL: return "full";
- case IdentityMode::NONE: return "none";
- case IdentityMode::INVALID: return "invalid";
- }
- return "?";
-}
-
-IdentityMode toIdentityMode(const char *s)
-{
- if (!strcmp(s,"id")) return IdentityMode::ID;
- if (!strcmp(s,"id-mfct")) return IdentityMode::ID_MFCT;
- if (!strcmp(s, "full")) return IdentityMode::FULL;
- if (!strcmp(s, "none")) return IdentityMode::NONE;
- return IdentityMode::INVALID;
-}
-
-void AddressExpression::clear()
-{
- id = "";
- has_wildcard = false;
- mbus_primary = false;
- mfct = 0xffff;
- version = 0xff;
- type = 0xff;
-}
-
-void AddressExpression::appendIdentity(IdentityMode im,
- AddressExpression *identity_expression,
- std::vector &as,
- std::vector &es)
-{
- identity_expression->clear();
- if (im == IdentityMode::NONE) return;
-
- // Copy id, id-mfct, id-mfct-v-t to identity_expression from the last address.
- identity_expression->trimToIdentity(im, as.back());
-
- // Is this identity expression already in the list of address expressions?
- if (std::find(es.begin(), es.end(), *identity_expression) == es.end())
- {
- // No, then add it at the end.
- es.push_back(*identity_expression);
- }
-}
-
-bool AddressExpression::operator==(const AddressExpression&ae) const
-{
- return id == ae.id &&
- has_wildcard == ae.has_wildcard&&
- mbus_primary == ae.mbus_primary &&
- mfct == ae.mfct &&
- version == ae.version &&
- type == ae.type &&
- filter_out == ae.filter_out;
-}
diff --git a/components/wmbus_common/address.h b/components/wmbus_common/address.h
deleted file mode 100644
index 23542f04..00000000
--- a/components/wmbus_common/address.h
+++ /dev/null
@@ -1,117 +0,0 @@
-/*
- Copyright (C) 2017-2022 Fredrik Öhrström (gpl-3.0-or-later)
-
- 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 .
-*/
-
-#ifndef ADDRESS_H_
-#define ADDRESS_H_
-
-#include "util.h"
-#include
-
-/**
- IdentityMode:
-
- @ID: The default, only the id groups the meter content.
- @ID_MFCT: Used when you have two meters with the same id but different manufacturers.
- @FULL: Used when you want to fully separate meter content on id.mft.v.t
- @NONE: Do not separate any meters! This might lead to telegrams overwriting each others state.
- Use this when no state is to be kept in the wmbusmeters object.
- @INVALID: Cannot parse cmdline.
-*/
-enum class IdentityMode
-{
- ID,
- ID_MFCT,
- FULL,
- NONE,
- INVALID
-};
-
-const char *toString(IdentityMode im);
-IdentityMode toIdentityMode(const char *s);
-
-struct Address
-{
- std::string id; // p1 or 12345678 or non-compliant hex: 1234abcd
- uint16_t mfct {};
- uchar type {};
- uchar version {};
-
- void decodeMfctFirst(const std::vector::iterator &pos);
- void decodeIdFirst(const std::vector::iterator &pos);
-
- std::string str();
- static std::string concat(std::vector &addresses);
-};
-
-struct AddressExpression
-{
- // An address expression is used to select which telegrams to decode for a driver.
- // An address expression is also used to select a specific meter to poll for data.
- // Example address: 12345678
- // Or fully qualified: 12345678.M=PII.T=1b.V=01
- // which means manufacturer triplet PII, type/media=0x1b, version=0x01
- // Or wildcards in id: 12*.T=16
- // which matches all cold water meters whose ids start with 12.
- // Or negated tests: 12345678.V!=66
- // which will decode all telegrams from 12345678 except those where the version is 0x66.
- // Or every telegram which is does not start with 12 and is not from ABB:
- // !12*.M!=ABB
-
- std::string id; // p1 or 12345678 or non-compliant hex: 1234abcd
- bool has_wildcard {}; // The id contains a *
- bool mbus_primary {}; // Signals that the id is 0-250
-
- uint16_t mfct { 0xffff }; // If 0xffff then any mfct matches this address.
- uchar version { 0xff }; // If 0xff then any version matches this address.
- uchar type { 0xff }; // If 0xff then any type matches this address.
-
- bool filter_out {}; // Telegrams matching this rule should be filtered out!
- bool required {}; // If true, then this address expression must be matched!
-
- AddressExpression() {}
- AddressExpression(Address &a) : id(a.id), mfct(a.mfct), version(a.version), type(a.type) { }
- bool operator==(const AddressExpression&) const;
- void clear();
- void trimToIdentity(IdentityMode im, Address &a);
- bool parse(const std::string &s);
- bool match(const std::string &id, uint16_t mfct, uchar version, uchar type);
- std::string str();
- static std::string concat(std::vector &address_expressions);
- static void appendIdentity(IdentityMode im,
- AddressExpression *identity_expression,
- std::vector &as,
- std::vector &es);
-};
-
-/**
- isValidSequenceOfAddressExpressions:
-
- Valid sequenes look like this:
- 12345678
- 12345678,22334455,34*
- 12*.T=16,!*.M=XYZ
- !*.V=33
-*/
-bool isValidSequenceOfAddressExpressions(const std::string& s);
-std::vector splitAddressExpressions(const std::string &aes);
-bool flagToManufacturer(const char *s, uint16_t *out_mfct);
-std::string manufacturerFlag(int m_field);
-bool doesTelegramMatchExpressions(std::vector &addresses,
- std::vector& address_expressions,
- bool *used_wildcard);
-
-#endif
diff --git a/components/wmbus_common/aes.cpp b/components/wmbus_common/aes.cpp
deleted file mode 100644
index 55bb3e1a..00000000
--- a/components/wmbus_common/aes.cpp
+++ /dev/null
@@ -1,598 +0,0 @@
-// Source from https://github.com/kokke/tiny-AES128-C
-// Public Domain / CC0 / Unlicense
-// Copyright (C) 2017-2019 kokke (CC0-1.0)
-/*
-
-This is an implementation of the AES algorithm, specifically ECB and CBC mode.
-Block size can be chosen in aes.h - available choices are AES128, AES192, AES256.
-
-The implementation is verified against the test std::vectors in:
- National Institute of Standards and Technology Special Publication 800-38A 2001 ED
-
-ECB-AES128
-----------
-
- plain-text:
- 6bc1bee22e409f96e93d7e117393172a
- ae2d8a571e03ac9c9eb76fac45af8e51
- 30c81c46a35ce411e5fbc1191a0a52ef
- f69f2445df4f9b17ad2b417be66c3710
-
- key:
- 2b7e151628aed2a6abf7158809cf4f3c
-
- resulting cipher
- 3ad77bb40d7a3660a89ecaf32466ef97
- f5d3d58503b9699de785895a96fdbaaf
- 43b1cd7f598ece23881b00e3ed030688
- 7b0c785e27e8ad3f8223207104725dd4
-
-
-NOTE: String length must be evenly divisible by 16byte (str_len % 16 == 0)
- You should pad the end of the std::string with zeros if this is not the case.
- For AES192/256 the block size is proportionally larger.
-
-*/
-
-
-/*****************************************************************************/
-/* Includes: */
-/*****************************************************************************/
-#include
-#include // CBC mode, for memset
-#include "aes.h"
-
-/*****************************************************************************/
-/* Defines: */
-/*****************************************************************************/
-// The number of columns comprising a state in AES. This is a constant in AES. Value=4
-#define Nb 4
-#define BLOCKLEN 16 //Block length in bytes AES is 128b block only
-
-#if defined(AES256) && (AES256 == 1)
- #define Nk 8
- #define KEYLEN 32
- #define Nr 14
- #define keyExpSize 240
-#elif defined(AES192) && (AES192 == 1)
- #define Nk 6
- #define KEYLEN 24
- #define Nr 12
- #define keyExpSize 208
-#else
- #define Nk 4 // The number of 32 bit words in a key.
- #define KEYLEN 16 // Key length in bytes
- #define Nr 10 // The number of rounds in AES Cipher.
- #define keyExpSize 176
-#endif
-
-// jcallan@github points out that declaring Multiply as a function
-// reduces code size considerably with the Keil ARM compiler.
-// See this link for more information: https://github.com/kokke/tiny-AES128-C/pull/3
-#ifndef MULTIPLY_AS_A_FUNCTION
- #define MULTIPLY_AS_A_FUNCTION 0
-#endif
-
-
-/*****************************************************************************/
-/* Private variables: */
-/*****************************************************************************/
-// state - array holding the intermediate results during decryption.
-typedef uint8_t state_t[4][4];
-static state_t* state;
-
-// The array that stores the round keys.
-static uint8_t RoundKey[keyExpSize];
-
-// The Key input to the AES Program
-static const uint8_t* Key;
-
-#if defined(CBC) && CBC
- // Initial Vector used only for CBC mode
- static uint8_t* Iv;
-#endif
-
-// The lookup-tables are marked const so they can be placed in read-only storage instead of RAM
-// The numbers below can be computed dynamically trading ROM for RAM -
-// This can be useful in (embedded) bootloader applications, where ROM is often limited.
-static const uint8_t sbox[256] = {
- //0 1 2 3 4 5 6 7 8 9 A B C D E F
- 0x63, 0x7c, 0x77, 0x7b, 0xf2, 0x6b, 0x6f, 0xc5, 0x30, 0x01, 0x67, 0x2b, 0xfe, 0xd7, 0xab, 0x76,
- 0xca, 0x82, 0xc9, 0x7d, 0xfa, 0x59, 0x47, 0xf0, 0xad, 0xd4, 0xa2, 0xaf, 0x9c, 0xa4, 0x72, 0xc0,
- 0xb7, 0xfd, 0x93, 0x26, 0x36, 0x3f, 0xf7, 0xcc, 0x34, 0xa5, 0xe5, 0xf1, 0x71, 0xd8, 0x31, 0x15,
- 0x04, 0xc7, 0x23, 0xc3, 0x18, 0x96, 0x05, 0x9a, 0x07, 0x12, 0x80, 0xe2, 0xeb, 0x27, 0xb2, 0x75,
- 0x09, 0x83, 0x2c, 0x1a, 0x1b, 0x6e, 0x5a, 0xa0, 0x52, 0x3b, 0xd6, 0xb3, 0x29, 0xe3, 0x2f, 0x84,
- 0x53, 0xd1, 0x00, 0xed, 0x20, 0xfc, 0xb1, 0x5b, 0x6a, 0xcb, 0xbe, 0x39, 0x4a, 0x4c, 0x58, 0xcf,
- 0xd0, 0xef, 0xaa, 0xfb, 0x43, 0x4d, 0x33, 0x85, 0x45, 0xf9, 0x02, 0x7f, 0x50, 0x3c, 0x9f, 0xa8,
- 0x51, 0xa3, 0x40, 0x8f, 0x92, 0x9d, 0x38, 0xf5, 0xbc, 0xb6, 0xda, 0x21, 0x10, 0xff, 0xf3, 0xd2,
- 0xcd, 0x0c, 0x13, 0xec, 0x5f, 0x97, 0x44, 0x17, 0xc4, 0xa7, 0x7e, 0x3d, 0x64, 0x5d, 0x19, 0x73,
- 0x60, 0x81, 0x4f, 0xdc, 0x22, 0x2a, 0x90, 0x88, 0x46, 0xee, 0xb8, 0x14, 0xde, 0x5e, 0x0b, 0xdb,
- 0xe0, 0x32, 0x3a, 0x0a, 0x49, 0x06, 0x24, 0x5c, 0xc2, 0xd3, 0xac, 0x62, 0x91, 0x95, 0xe4, 0x79,
- 0xe7, 0xc8, 0x37, 0x6d, 0x8d, 0xd5, 0x4e, 0xa9, 0x6c, 0x56, 0xf4, 0xea, 0x65, 0x7a, 0xae, 0x08,
- 0xba, 0x78, 0x25, 0x2e, 0x1c, 0xa6, 0xb4, 0xc6, 0xe8, 0xdd, 0x74, 0x1f, 0x4b, 0xbd, 0x8b, 0x8a,
- 0x70, 0x3e, 0xb5, 0x66, 0x48, 0x03, 0xf6, 0x0e, 0x61, 0x35, 0x57, 0xb9, 0x86, 0xc1, 0x1d, 0x9e,
- 0xe1, 0xf8, 0x98, 0x11, 0x69, 0xd9, 0x8e, 0x94, 0x9b, 0x1e, 0x87, 0xe9, 0xce, 0x55, 0x28, 0xdf,
- 0x8c, 0xa1, 0x89, 0x0d, 0xbf, 0xe6, 0x42, 0x68, 0x41, 0x99, 0x2d, 0x0f, 0xb0, 0x54, 0xbb, 0x16 };
-
-static const uint8_t rsbox[256] = {
- 0x52, 0x09, 0x6a, 0xd5, 0x30, 0x36, 0xa5, 0x38, 0xbf, 0x40, 0xa3, 0x9e, 0x81, 0xf3, 0xd7, 0xfb,
- 0x7c, 0xe3, 0x39, 0x82, 0x9b, 0x2f, 0xff, 0x87, 0x34, 0x8e, 0x43, 0x44, 0xc4, 0xde, 0xe9, 0xcb,
- 0x54, 0x7b, 0x94, 0x32, 0xa6, 0xc2, 0x23, 0x3d, 0xee, 0x4c, 0x95, 0x0b, 0x42, 0xfa, 0xc3, 0x4e,
- 0x08, 0x2e, 0xa1, 0x66, 0x28, 0xd9, 0x24, 0xb2, 0x76, 0x5b, 0xa2, 0x49, 0x6d, 0x8b, 0xd1, 0x25,
- 0x72, 0xf8, 0xf6, 0x64, 0x86, 0x68, 0x98, 0x16, 0xd4, 0xa4, 0x5c, 0xcc, 0x5d, 0x65, 0xb6, 0x92,
- 0x6c, 0x70, 0x48, 0x50, 0xfd, 0xed, 0xb9, 0xda, 0x5e, 0x15, 0x46, 0x57, 0xa7, 0x8d, 0x9d, 0x84,
- 0x90, 0xd8, 0xab, 0x00, 0x8c, 0xbc, 0xd3, 0x0a, 0xf7, 0xe4, 0x58, 0x05, 0xb8, 0xb3, 0x45, 0x06,
- 0xd0, 0x2c, 0x1e, 0x8f, 0xca, 0x3f, 0x0f, 0x02, 0xc1, 0xaf, 0xbd, 0x03, 0x01, 0x13, 0x8a, 0x6b,
- 0x3a, 0x91, 0x11, 0x41, 0x4f, 0x67, 0xdc, 0xea, 0x97, 0xf2, 0xcf, 0xce, 0xf0, 0xb4, 0xe6, 0x73,
- 0x96, 0xac, 0x74, 0x22, 0xe7, 0xad, 0x35, 0x85, 0xe2, 0xf9, 0x37, 0xe8, 0x1c, 0x75, 0xdf, 0x6e,
- 0x47, 0xf1, 0x1a, 0x71, 0x1d, 0x29, 0xc5, 0x89, 0x6f, 0xb7, 0x62, 0x0e, 0xaa, 0x18, 0xbe, 0x1b,
- 0xfc, 0x56, 0x3e, 0x4b, 0xc6, 0xd2, 0x79, 0x20, 0x9a, 0xdb, 0xc0, 0xfe, 0x78, 0xcd, 0x5a, 0xf4,
- 0x1f, 0xdd, 0xa8, 0x33, 0x88, 0x07, 0xc7, 0x31, 0xb1, 0x12, 0x10, 0x59, 0x27, 0x80, 0xec, 0x5f,
- 0x60, 0x51, 0x7f, 0xa9, 0x19, 0xb5, 0x4a, 0x0d, 0x2d, 0xe5, 0x7a, 0x9f, 0x93, 0xc9, 0x9c, 0xef,
- 0xa0, 0xe0, 0x3b, 0x4d, 0xae, 0x2a, 0xf5, 0xb0, 0xc8, 0xeb, 0xbb, 0x3c, 0x83, 0x53, 0x99, 0x61,
- 0x17, 0x2b, 0x04, 0x7e, 0xba, 0x77, 0xd6, 0x26, 0xe1, 0x69, 0x14, 0x63, 0x55, 0x21, 0x0c, 0x7d };
-
-// The round constant word array, Rcon[i], contains the values given by
-// x to th e power (i-1) being powers of x (x is denoted as {02}) in the field GF(2^8)
-static const uint8_t Rcon[11] = {
- 0x8d, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36 };
-
-/*
- * Jordan Goulder points out in PR #12 (https://github.com/kokke/tiny-AES128-C/pull/12),
- * that you can remove most of the elements in the Rcon array, because they are unused.
- *
- * From Wikipedia's article on the Rijndael key schedule @ https://en.wikipedia.org/wiki/Rijndael_key_schedule#Rcon
- *
- * "Only the first some of these constants are actually used – up to rcon[10] for AES-128 (as 11 round keys are needed),
- * up to rcon[8] for AES-192, up to rcon[7] for AES-256. rcon[0] is not used in AES algorithm."
- *
- * ... which is why the full array below has been 'disabled' below.
- */
-#if 0
-static const uint8_t Rcon[256] = {
- 0x8d, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36, 0x6c, 0xd8, 0xab, 0x4d, 0x9a,
- 0x2f, 0x5e, 0xbc, 0x63, 0xc6, 0x97, 0x35, 0x6a, 0xd4, 0xb3, 0x7d, 0xfa, 0xef, 0xc5, 0x91, 0x39,
- 0x72, 0xe4, 0xd3, 0xbd, 0x61, 0xc2, 0x9f, 0x25, 0x4a, 0x94, 0x33, 0x66, 0xcc, 0x83, 0x1d, 0x3a,
- 0x74, 0xe8, 0xcb, 0x8d, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36, 0x6c, 0xd8,
- 0xab, 0x4d, 0x9a, 0x2f, 0x5e, 0xbc, 0x63, 0xc6, 0x97, 0x35, 0x6a, 0xd4, 0xb3, 0x7d, 0xfa, 0xef,
- 0xc5, 0x91, 0x39, 0x72, 0xe4, 0xd3, 0xbd, 0x61, 0xc2, 0x9f, 0x25, 0x4a, 0x94, 0x33, 0x66, 0xcc,
- 0x83, 0x1d, 0x3a, 0x74, 0xe8, 0xcb, 0x8d, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b,
- 0x36, 0x6c, 0xd8, 0xab, 0x4d, 0x9a, 0x2f, 0x5e, 0xbc, 0x63, 0xc6, 0x97, 0x35, 0x6a, 0xd4, 0xb3,
- 0x7d, 0xfa, 0xef, 0xc5, 0x91, 0x39, 0x72, 0xe4, 0xd3, 0xbd, 0x61, 0xc2, 0x9f, 0x25, 0x4a, 0x94,
- 0x33, 0x66, 0xcc, 0x83, 0x1d, 0x3a, 0x74, 0xe8, 0xcb, 0x8d, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20,
- 0x40, 0x80, 0x1b, 0x36, 0x6c, 0xd8, 0xab, 0x4d, 0x9a, 0x2f, 0x5e, 0xbc, 0x63, 0xc6, 0x97, 0x35,
- 0x6a, 0xd4, 0xb3, 0x7d, 0xfa, 0xef, 0xc5, 0x91, 0x39, 0x72, 0xe4, 0xd3, 0xbd, 0x61, 0xc2, 0x9f,
- 0x25, 0x4a, 0x94, 0x33, 0x66, 0xcc, 0x83, 0x1d, 0x3a, 0x74, 0xe8, 0xcb, 0x8d, 0x01, 0x02, 0x04,
- 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36, 0x6c, 0xd8, 0xab, 0x4d, 0x9a, 0x2f, 0x5e, 0xbc, 0x63,
- 0xc6, 0x97, 0x35, 0x6a, 0xd4, 0xb3, 0x7d, 0xfa, 0xef, 0xc5, 0x91, 0x39, 0x72, 0xe4, 0xd3, 0xbd,
- 0x61, 0xc2, 0x9f, 0x25, 0x4a, 0x94, 0x33, 0x66, 0xcc, 0x83, 0x1d, 0x3a, 0x74, 0xe8, 0xcb, 0x8d };
-#endif
-
-/*****************************************************************************/
-/* Private functions: */
-/*****************************************************************************/
-static uint8_t getSBoxValue(uint8_t num)
-{
- return sbox[num];
-}
-
-static uint8_t getSBoxInvert(uint8_t num)
-{
- return rsbox[num];
-}
-
-// This function produces Nb(Nr+1) round keys. The round keys are used in each round to decrypt the states.
-static void KeyExpansion(void)
-{
- uint32_t i, k;
- uint8_t tempa[4]; // Used for the column/row operations
-
- // The first round key is the key itself.
- for (i = 0; i < Nk; ++i)
- {
- RoundKey[(i * 4) + 0] = Key[(i * 4) + 0];
- RoundKey[(i * 4) + 1] = Key[(i * 4) + 1];
- RoundKey[(i * 4) + 2] = Key[(i * 4) + 2];
- RoundKey[(i * 4) + 3] = Key[(i * 4) + 3];
- }
-
- // All other round keys are found from the previous round keys.
- //i == Nk
- for (; i < Nb * (Nr + 1); ++i)
- {
- {
- tempa[0]=RoundKey[(i-1) * 4 + 0];
- tempa[1]=RoundKey[(i-1) * 4 + 1];
- tempa[2]=RoundKey[(i-1) * 4 + 2];
- tempa[3]=RoundKey[(i-1) * 4 + 3];
- }
-
- if (i % Nk == 0)
- {
- // This function shifts the 4 bytes in a word to the left once.
- // [a0,a1,a2,a3] becomes [a1,a2,a3,a0]
-
- // Function RotWord()
- {
- k = tempa[0];
- tempa[0] = tempa[1];
- tempa[1] = tempa[2];
- tempa[2] = tempa[3];
- tempa[3] = k;
- }
-
- // SubWord() is a function that takes a four-byte input word and
- // applies the S-box to each of the four bytes to produce an output word.
-
- // Function Subword()
- {
- tempa[0] = getSBoxValue(tempa[0]);
- tempa[1] = getSBoxValue(tempa[1]);
- tempa[2] = getSBoxValue(tempa[2]);
- tempa[3] = getSBoxValue(tempa[3]);
- }
-
- tempa[0] = tempa[0] ^ Rcon[i/Nk];
- }
-#if defined(AES256) && (AES256 == 1)
- if (i % Nk == 4)
- {
- // Function Subword()
- {
- tempa[0] = getSBoxValue(tempa[0]);
- tempa[1] = getSBoxValue(tempa[1]);
- tempa[2] = getSBoxValue(tempa[2]);
- tempa[3] = getSBoxValue(tempa[3]);
- }
- }
-#endif
- RoundKey[i * 4 + 0] = RoundKey[(i - Nk) * 4 + 0] ^ tempa[0];
- RoundKey[i * 4 + 1] = RoundKey[(i - Nk) * 4 + 1] ^ tempa[1];
- RoundKey[i * 4 + 2] = RoundKey[(i - Nk) * 4 + 2] ^ tempa[2];
- RoundKey[i * 4 + 3] = RoundKey[(i - Nk) * 4 + 3] ^ tempa[3];
- }
-}
-
-// This function adds the round key to state.
-// The round key is added to the state by an XOR function.
-static void AddRoundKey(uint8_t round)
-{
- uint8_t i,j;
- for (i=0;i<4;++i)
- {
- for (j = 0; j < 4; ++j)
- {
- (*state)[i][j] ^= RoundKey[round * Nb * 4 + i * Nb + j];
- }
- }
-}
-
-// The SubBytes Function Substitutes the values in the
-// state matrix with values in an S-box.
-static void SubBytes(void)
-{
- uint8_t i, j;
- for (i = 0; i < 4; ++i)
- {
- for (j = 0; j < 4; ++j)
- {
- (*state)[j][i] = getSBoxValue((*state)[j][i]);
- }
- }
-}
-
-// The ShiftRows() function shifts the rows in the state to the left.
-// Each row is shifted with different offset.
-// Offset = Row number. So the first row is not shifted.
-static void ShiftRows(void)
-{
- uint8_t temp;
-
- // Rotate first row 1 columns to left
- temp = (*state)[0][1];
- (*state)[0][1] = (*state)[1][1];
- (*state)[1][1] = (*state)[2][1];
- (*state)[2][1] = (*state)[3][1];
- (*state)[3][1] = temp;
-
- // Rotate second row 2 columns to left
- temp = (*state)[0][2];
- (*state)[0][2] = (*state)[2][2];
- (*state)[2][2] = temp;
-
- temp = (*state)[1][2];
- (*state)[1][2] = (*state)[3][2];
- (*state)[3][2] = temp;
-
- // Rotate third row 3 columns to left
- temp = (*state)[0][3];
- (*state)[0][3] = (*state)[3][3];
- (*state)[3][3] = (*state)[2][3];
- (*state)[2][3] = (*state)[1][3];
- (*state)[1][3] = temp;
-}
-
-static uint8_t xtime(uint8_t x)
-{
- return ((x<<1) ^ (((x>>7) & 1) * 0x1b));
-}
-
-// MixColumns function mixes the columns of the state matrix
-static void MixColumns(void)
-{
- uint8_t i;
- uint8_t Tmp,Tm,t;
- for (i = 0; i < 4; ++i)
- {
- t = (*state)[i][0];
- Tmp = (*state)[i][0] ^ (*state)[i][1] ^ (*state)[i][2] ^ (*state)[i][3] ;
- Tm = (*state)[i][0] ^ (*state)[i][1] ; Tm = xtime(Tm); (*state)[i][0] ^= Tm ^ Tmp ;
- Tm = (*state)[i][1] ^ (*state)[i][2] ; Tm = xtime(Tm); (*state)[i][1] ^= Tm ^ Tmp ;
- Tm = (*state)[i][2] ^ (*state)[i][3] ; Tm = xtime(Tm); (*state)[i][2] ^= Tm ^ Tmp ;
- Tm = (*state)[i][3] ^ t ; Tm = xtime(Tm); (*state)[i][3] ^= Tm ^ Tmp ;
- }
-}
-
-// Multiply is used to multiply numbers in the field GF(2^8)
-#if MULTIPLY_AS_A_FUNCTION
-static uint8_t Multiply(uint8_t x, uint8_t y)
-{
- return (((y & 1) * x) ^
- ((y>>1 & 1) * xtime(x)) ^
- ((y>>2 & 1) * xtime(xtime(x))) ^
- ((y>>3 & 1) * xtime(xtime(xtime(x)))) ^
- ((y>>4 & 1) * xtime(xtime(xtime(xtime(x))))));
- }
-#else
-#define Multiply(x, y) \
- ( ((y & 1) * x) ^ \
- ((y>>1 & 1) * xtime(x)) ^ \
- ((y>>2 & 1) * xtime(xtime(x))) ^ \
- ((y>>3 & 1) * xtime(xtime(xtime(x)))) ^ \
- ((y>>4 & 1) * xtime(xtime(xtime(xtime(x)))))) \
-
-#endif
-
-// MixColumns function mixes the columns of the state matrix.
-// The method used to multiply may be difficult to understand for the inexperienced.
-// Please use the references to gain more information.
-static void InvMixColumns(void)
-{
- int i;
- uint8_t a, b, c, d;
- for (i = 0; i < 4; ++i)
- {
- a = (*state)[i][0];
- b = (*state)[i][1];
- c = (*state)[i][2];
- d = (*state)[i][3];
-
- (*state)[i][0] = Multiply(a, 0x0e) ^ Multiply(b, 0x0b) ^ Multiply(c, 0x0d) ^ Multiply(d, 0x09);
- (*state)[i][1] = Multiply(a, 0x09) ^ Multiply(b, 0x0e) ^ Multiply(c, 0x0b) ^ Multiply(d, 0x0d);
- (*state)[i][2] = Multiply(a, 0x0d) ^ Multiply(b, 0x09) ^ Multiply(c, 0x0e) ^ Multiply(d, 0x0b);
- (*state)[i][3] = Multiply(a, 0x0b) ^ Multiply(b, 0x0d) ^ Multiply(c, 0x09) ^ Multiply(d, 0x0e);
- }
-}
-
-
-// The SubBytes Function Substitutes the values in the
-// state matrix with values in an S-box.
-static void InvSubBytes(void)
-{
- uint8_t i,j;
- for (i = 0; i < 4; ++i)
- {
- for (j = 0; j < 4; ++j)
- {
- (*state)[j][i] = getSBoxInvert((*state)[j][i]);
- }
- }
-}
-
-static void InvShiftRows(void)
-{
- uint8_t temp;
-
- // Rotate first row 1 columns to right
- temp = (*state)[3][1];
- (*state)[3][1] = (*state)[2][1];
- (*state)[2][1] = (*state)[1][1];
- (*state)[1][1] = (*state)[0][1];
- (*state)[0][1] = temp;
-
- // Rotate second row 2 columns to right
- temp = (*state)[0][2];
- (*state)[0][2] = (*state)[2][2];
- (*state)[2][2] = temp;
-
- temp = (*state)[1][2];
- (*state)[1][2] = (*state)[3][2];
- (*state)[3][2] = temp;
-
- // Rotate third row 3 columns to right
- temp = (*state)[0][3];
- (*state)[0][3] = (*state)[1][3];
- (*state)[1][3] = (*state)[2][3];
- (*state)[2][3] = (*state)[3][3];
- (*state)[3][3] = temp;
-}
-
-
-// Cipher is the main function that encrypts the PlainText.
-static void Cipher(void)
-{
- uint8_t round = 0;
-
- // Add the First round key to the state before starting the rounds.
- AddRoundKey(0);
-
- // There will be Nr rounds.
- // The first Nr-1 rounds are identical.
- // These Nr-1 rounds are executed in the loop below.
- for (round = 1; round < Nr; ++round)
- {
- SubBytes();
- ShiftRows();
- MixColumns();
- AddRoundKey(round);
- }
-
- // The last round is given below.
- // The MixColumns function is not here in the last round.
- SubBytes();
- ShiftRows();
- AddRoundKey(Nr);
-}
-
-static void InvCipher(void)
-{
- uint8_t round=0;
-
- // Add the First round key to the state before starting the rounds.
- AddRoundKey(Nr);
-
- // There will be Nr rounds.
- // The first Nr-1 rounds are identical.
- // These Nr-1 rounds are executed in the loop below.
- for (round = (Nr - 1); round > 0; --round)
- {
- InvShiftRows();
- InvSubBytes();
- AddRoundKey(round);
- InvMixColumns();
- }
-
- // The last round is given below.
- // The MixColumns function is not here in the last round.
- InvShiftRows();
- InvSubBytes();
- AddRoundKey(0);
-}
-
-
-/*****************************************************************************/
-/* Public functions: */
-/*****************************************************************************/
-#if defined(ECB) && (ECB == 1)
-
-
-void AES_ECB_encrypt(const uint8_t* input, const uint8_t* key, uint8_t* output, const uint32_t length)
-{
- // Copy input to output, and work in-memory on output
- memcpy(output, input, length);
- state = (state_t*)output;
-
- Key = key;
- KeyExpansion();
-
- // The next function call encrypts the PlainText with the Key using AES algorithm.
- Cipher();
-}
-
-void AES_ECB_decrypt(const uint8_t* input, const uint8_t* key, uint8_t *output, const uint32_t length)
-{
- // Copy input to output, and work in-memory on output
- memcpy(output, input, length);
- state = (state_t*)output;
-
- // The KeyExpansion routine must be called before encryption.
- Key = key;
- KeyExpansion();
-
- InvCipher();
-}
-
-
-#endif // #if defined(ECB) && (ECB == 1)
-
-
-
-
-
-#if defined(CBC) && (CBC == 1)
-
-
-static void XorWithIv(uint8_t* buf)
-{
- uint8_t i;
- for (i = 0; i < BLOCKLEN; ++i) //WAS for(i = 0; i < KEYLEN; ++i) but the block in AES is always 128bit so 16 bytes!
- {
- buf[i] ^= Iv[i];
- }
-}
-
-void AES_CBC_encrypt_buffer(uint8_t* output, uint8_t* input, uint32_t length, const uint8_t* key, const uint8_t* iv)
-{
- uintptr_t i;
-// uint8_t extra = length % BLOCKLEN; /* Remaining bytes in the last non-full block */
-
- // Skip the key expansion if key is passed as 0
- if (0 != key)
- {
- Key = key;
- KeyExpansion();
- }
-
- if (iv != 0)
- {
- Iv = (uint8_t*)iv;
- }
-
- for (i = 0; i < length; i += BLOCKLEN)
- {
- XorWithIv(input);
- memcpy(output, input, BLOCKLEN);
- state = (state_t*)output;
- Cipher();
- Iv = output;
- input += BLOCKLEN;
- output += BLOCKLEN;
- //printf("Step %d - %d", i/16, i);
- }
-
-/* if (extra)
- {
- memcpy(output, input, extra);
- state = (state_t*)output;
- Cipher();
- } */
-}
-
-void AES_CBC_decrypt_buffer(uint8_t* output, uint8_t* input, uint32_t length, const uint8_t* key, const uint8_t* iv)
-{
- uintptr_t i;
-// uint8_t extra = length % BLOCKLEN; /* Remaining bytes in the last non-full block */
-
- // Skip the key expansion if key is passed as 0
- if (0 != key)
- {
- Key = key;
- KeyExpansion();
- }
-
- // If iv is passed as 0, we continue to encrypt without re-setting the Iv
- if (iv != 0)
- {
- Iv = (uint8_t*)iv;
- }
-
- for (i = 0; i < length; i += BLOCKLEN)
- {
- memcpy(output, input, BLOCKLEN);
- state = (state_t*)output;
- InvCipher();
- XorWithIv(output);
- Iv = input;
- input += BLOCKLEN;
- output += BLOCKLEN;
- }
-
-/* if (extra)
- {
- memcpy(output, input, extra);
- state = (state_t*)output;
- InvCipher();
- }*/
-}
-
-#endif // #if defined(CBC) && (CBC == 1)
diff --git a/components/wmbus_common/aes.h b/components/wmbus_common/aes.h
deleted file mode 100644
index bce083e2..00000000
--- a/components/wmbus_common/aes.h
+++ /dev/null
@@ -1,44 +0,0 @@
-// Source from https://github.com/kokke/tiny-AES128-C
-// Public Domain / CC0 / Unlicense
-// Copyright (C) 2017-2019 kokke (CC0-1.0)
-#ifndef _AES_H_
-#define _AES_H_
-
-#include
-
-
-// #define the macros below to 1/0 to enable/disable the mode of operation.
-//
-// CBC enables AES encryption in CBC-mode of operation.
-// ECB enables the basic ECB 16-byte block algorithm. Both can be enabled simultaneously.
-
-// The #ifndef-guard allows it to be configured before #include'ing or at compile time.
-#ifndef CBC
- #define CBC 1
-#endif
-
-#ifndef ECB
- #define ECB 1
-#endif
-
-#define AES128 1
-//#define AES192 1
-//#define AES256 1
-
-#if defined(ECB) && (ECB == 1)
-
-void AES_ECB_encrypt(const uint8_t* input, const uint8_t* key, uint8_t *output, const uint32_t length);
-void AES_ECB_decrypt(const uint8_t* input, const uint8_t* key, uint8_t *output, const uint32_t length);
-
-#endif // #if defined(ECB) && (ECB == !)
-
-
-#if defined(CBC) && (CBC == 1)
-
-void AES_CBC_encrypt_buffer(uint8_t* output, uint8_t* input, uint32_t length, const uint8_t* key, const uint8_t* iv);
-void AES_CBC_decrypt_buffer(uint8_t* output, uint8_t* input, uint32_t length, const uint8_t* key, const uint8_t* iv);
-
-#endif // #if defined(CBC) && (CBC == 1)
-
-
-#endif //_AES_H_
diff --git a/components/wmbus_common/aescmac.cpp b/components/wmbus_common/aescmac.cpp
deleted file mode 100644
index 43f4bab8..00000000
--- a/components/wmbus_common/aescmac.cpp
+++ /dev/null
@@ -1,122 +0,0 @@
-/*
- Copyright (C) 2020 Fredrik Öhrström (gpl-3.0-or-later)
-
- 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 .
-*/
-
-#include
-#include
-#include"aes.h"
-#include"aescmac.h"
-#include"util.h"
-
-uchar vec87[16] = {
- 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
- 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x87
-};
-
-void generateSubkeys(uchar *key, uchar *K1, uchar *K2)
-{
- uchar L[16];
- uchar Z[16];
- uchar tmp[16];
-
- memset(Z, 0, 16);
-
- AES_ECB_encrypt(Z, key, L, 16);
-
- if (!(L[0] & 0x80))
- {
- shiftLeft(L, K1, 16);
- }
- else
- {
- shiftLeft(L, tmp, 16);
- xorit(tmp, vec87, K1, 16);
- }
-
- if (!(K1[0] & 0x80))
- {
- shiftLeft(K1, K2, 16);
- }
- else
- {
- shiftLeft(K1, tmp, 16);
- xorit(tmp, vec87, K2, 16);
- }
-}
-
-void pad(uchar *in, uchar *out, int len)
-{
- for (int i = 0; i < 16; i++)
- {
- if (i < len)
- {
- out[i] = in[i];
- }
- else if (i == len)
- {
- out[i] = 0x80;
- }
- else
- {
- out[i] = 0x00;
- }
- }
-}
-
-void AES_CMAC(uchar *key, uchar *input, int len, uchar *mac)
-{
- bool len_is_multiple_of_block;
- uchar X[16], Y[16];
- uchar K1[16], K2[16];
- uchar M_last[16], padded[16];
-
- generateSubkeys(key, K1, K2);
-
- int num_blocks = (len+15)/16;
-
- if (!num_blocks)
- {
- num_blocks = 1;
- len_is_multiple_of_block = false;
- }
- else
- {
- len_is_multiple_of_block = !(len%16);
- }
-
- if (len_is_multiple_of_block)
- {
- xorit(input+(16*(num_blocks-1)), K1, M_last, 16);
- }
- else
- {
- pad(input+(16*(num_blocks-1)), padded, len%16);
- xorit(padded, K2, M_last, 16);
- }
-
- memset(X, 0, 16);
-
- for (int i=0; i.
-*/
-
-#ifndef _AESCMAC_H_
-#define _AESCMAC_H_
-
-typedef unsigned char uchar;
-
-void AES_CMAC (uchar *key, uchar *input, int length, uchar *mac);
-
-#endif //_AESCMAC_H_
diff --git a/components/wmbus_common/component.cpp b/components/wmbus_common/component.cpp
index 88f0c7d5..895c43c5 100644
--- a/components/wmbus_common/component.cpp
+++ b/components/wmbus_common/component.cpp
@@ -1,33 +1,21 @@
#include "component.h"
-#include "meters.h"
+#include "esphome/components/wmbus_common/meter.h"
#include "esphome/core/defines.h"
#include "esphome/core/log.h"
-extern "C" {
-void register_wmbus_drivers();
-}
-
namespace esphome {
namespace wmbus_common {
static const char *TAG = "wmbus_common";
-std::vector sorted_drivers() {
- register_wmbus_drivers();
- auto drivers = allDrivers();
- std::sort(drivers.begin(), drivers.end(),
- [](DriverInfo *a, DriverInfo *b) { return a->name().str() < b->name().str(); });
- return drivers;
-}
-
void WMBusCommon::dump_config() {
ESP_LOGCONFIG(TAG, "wM-Bus Component:");
- ESP_LOGCONFIG(TAG, " wmbusmeters version: %s", WMBUSMETERS_TAG);
- ESP_LOGCONFIG(TAG, " Loaded drivers:");
- for (auto &driver : sorted_drivers())
- ESP_LOGCONFIG(TAG, (" - " + driver->name().str()).c_str());
+ ESP_LOGCONFIG(TAG, " wmbusmeters version: %s", WMBUSMETERS_VERSION);
+ ESP_LOGCONFIG(TAG, " Compiled drivers:");
+ for (const wmbus::DriverSpec *spec : wmbus::registered_drivers)
+ ESP_LOGCONFIG(TAG, " - %s", spec->name);
}
} // namespace wmbus_common
-} // namespace esphome
\ No newline at end of file
+} // namespace esphome
diff --git a/components/wmbus_common/crypto.cpp b/components/wmbus_common/crypto.cpp
new file mode 100644
index 00000000..abff75c3
--- /dev/null
+++ b/components/wmbus_common/crypto.cpp
@@ -0,0 +1,112 @@
+#include "crypto.h"
+
+#include "esphome/core/defines.h"
+#include "esphome/core/helpers.h"
+
+#if defined(USE_HOST)
+#include
+#else
+#include "mbedtls/aes.h"
+#endif
+
+namespace wmbus {
+
+#if defined(USE_HOST)
+namespace {
+
+// OpenSSL 3 exposes AES only through EVP; the per-call context is fine on a host.
+void evp(const EVP_CIPHER *cipher, std::span key, const uint8_t *iv, std::span in,
+ std::span out, bool encrypt) {
+ EVP_CIPHER_CTX *ctx = EVP_CIPHER_CTX_new();
+ if (ctx == nullptr)
+ return;
+
+ if (encrypt)
+ EVP_EncryptInit_ex(ctx, cipher, nullptr, key.data(), iv);
+ else
+ EVP_DecryptInit_ex(ctx, cipher, nullptr, key.data(), iv);
+ EVP_CIPHER_CTX_set_padding(ctx, 0);
+
+ int written = 0;
+ if (encrypt)
+ EVP_EncryptUpdate(ctx, out.data(), &written, in.data(), (int) in.size());
+ else
+ EVP_DecryptUpdate(ctx, out.data(), &written, in.data(), (int) in.size());
+
+ EVP_CIPHER_CTX_free(ctx);
+}
+
+} // namespace
+#endif
+
+std::array aes_ecb_encrypt(std::span key, std::span in) {
+ std::array out;
+#if defined(USE_HOST)
+ evp(EVP_aes_128_ecb(), key, nullptr, in, out, true);
+#else
+ mbedtls_aes_context ctx;
+ mbedtls_aes_init(&ctx);
+ mbedtls_aes_setkey_enc(&ctx, key.data(), 128);
+ mbedtls_aes_crypt_ecb(&ctx, MBEDTLS_AES_ENCRYPT, in.data(), out.data());
+ mbedtls_aes_free(&ctx);
+#endif
+ return out;
+}
+
+// By value: mbedtls_aes_crypt_cbc advances the iv in place.
+void aes_cbc_decrypt(std::span key, std::array iv, std::span buf) {
+ buf = buf.first(buf.size() - buf.size() % 16);
+ if (buf.empty())
+ return;
+
+#if defined(USE_HOST)
+ evp(EVP_aes_128_cbc(), key, iv.data(), buf, buf, false);
+#else
+ mbedtls_aes_context ctx;
+ mbedtls_aes_init(&ctx);
+ mbedtls_aes_setkey_dec(&ctx, key.data(), 128);
+ mbedtls_aes_crypt_cbc(&ctx, MBEDTLS_AES_DECRYPT, buf.size(), iv.data(), buf.data(), buf.data());
+ mbedtls_aes_free(&ctx);
+#endif
+}
+
+namespace {
+
+// RFC 4493's subkey step: doubling in GF(2^128).
+std::array doubled(const std::array &l) {
+ std::array out;
+ for (size_t i = 0; i < 15; ++i)
+ out[i] = (uint8_t) (l[i] << 1 | l[i + 1] >> 7);
+ out[15] = (uint8_t) (l[15] << 1 ^ (l[0] & 0x80 ? 0x87 : 0));
+ return out;
+}
+
+} // namespace
+
+std::array aes_cmac(std::span key, std::span input) {
+ const std::array k1 = doubled(aes_ecb_encrypt(key, std::array{}));
+ // Where the last block starts; an empty input has an empty one.
+ const size_t last = input.empty() ? 0 : (input.size() - 1) / 16 * 16;
+
+ std::array x{};
+ for (size_t i = 0; i < last; i += 16) {
+ xor_into(x, input.subspan(i, 16));
+ x = aes_ecb_encrypt(key, x);
+ }
+
+ const std::span tail = input.subspan(last);
+ xor_into(x, tail);
+ if (tail.size() == 16) {
+ xor_into(x, k1);
+ } else {
+ x[tail.size()] ^= 0x80;
+ xor_into(x, doubled(k1));
+ }
+ return aes_ecb_encrypt(key, x);
+}
+
+uint16_t crc16_en13757(std::span data) {
+ return esphome::crc16be(data.data(), data.size(), 0x0000, 0x3D65, false, true);
+}
+
+} // namespace wmbus
diff --git a/components/wmbus_common/crypto.h b/components/wmbus_common/crypto.h
new file mode 100644
index 00000000..bdfb1c6c
--- /dev/null
+++ b/components/wmbus_common/crypto.h
@@ -0,0 +1,26 @@
+#pragma once
+
+#include
+#include
+#include
+#include
+#include
+
+namespace wmbus {
+
+std::array aes_ecb_encrypt(std::span key, std::span in);
+
+// In place; the tail that does not fill a block is left alone.
+void aes_cbc_decrypt(std::span key, std::array iv, std::span buf);
+
+// RFC 4493.
+std::array aes_cmac(std::span key, std::span input);
+
+// Over the shorter of the two.
+inline void xor_into(std::span dest, std::span src) {
+ std::ranges::transform(dest, src, dest.begin(), std::bit_xor{});
+}
+
+uint16_t crc16_en13757(std::span data);
+
+} // namespace wmbus
diff --git a/components/wmbus_common/driver_abbb23.cpp b/components/wmbus_common/driver_abbb23.cpp
deleted file mode 100644
index dbc46425..00000000
--- a/components/wmbus_common/driver_abbb23.cpp
+++ /dev/null
@@ -1,1395 +0,0 @@
-/*
- Copyright (C) 2019-2023 Fredrik Öhrström (gpl-3.0-or-later)
-
- 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 .
-*/
-
-#include"meters_common_implementation.h"
-
-namespace
-{
- struct Driver : public virtual MeterCommonImplementation
- {
- Driver(MeterInfo &mi, DriverInfo &di);
- };
-
- static bool ok = staticRegisterDriver([](DriverInfo&di)
- {
- di.setName("abbb23");
- di.setDefaultFields("name,id,total_energy_consumption_kwh,timestamp");
- di.setMeterType(MeterType::ElectricityMeter);
- di.addLinkMode(LinkMode::T1);
- di.addMVT(MANUFACTURER_ABB, 0x02, 0x20);
- di.setConstructor([](MeterInfo& mi, DriverInfo& di){ return shared_ptr(new Driver(mi, di)); });
- });
-
- Driver::Driver(MeterInfo &mi, DriverInfo &di) : MeterCommonImplementation(mi, di)
- {
- addStringField(
- "status",
- "Status, error, warning and alarm flags.",
- DEFAULT_PRINT_PROPERTIES | PrintProperty::INCLUDE_TPL_STATUS | PrintProperty::STATUS);
-
- addNumericFieldWithExtractor(
- "total_energy_consumption",
- "Total cumulative active imported energy.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Energy,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyEnergyVIF)
- .add(VIFCombinableRaw(0))
- );
-
- addNumericFieldWithExtractor(
- "total_energy_consumption_tariff_{tariff_counter}",
- "Total cumulative active imported energy per tariff.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Energy,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyEnergyVIF)
- .set(TariffNr(1),TariffNr(4))
- .add(VIFCombinableRaw(0))
- );
-
- addNumericFieldWithExtractor(
- "total_energy_production",
- "Total cumulative active exported energy.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Energy,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyEnergyVIF)
- .set(TariffNr(0))
- .set(SubUnitNr(1))
- .add(VIFCombinableRaw(0))
- );
-
- addNumericFieldWithExtractor(
- "total_energy_production_tariff_{tariff_counter}",
- "Total cumulative active exported energy per tariff.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Energy,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyEnergyVIF)
- .set(TariffNr(1),TariffNr(4))
- .set(SubUnitNr(1))
- .add(VIFCombinableRaw(0))
- );
-
- addNumericFieldWithExtractor(
- "active_tariff",
- "Active tariff.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Dimensionless,
- VifScaling::None, DifSignedness::Signed,
- FieldMatcher::build()
- .set(DifVifKey("01FF9300")),
- Unit::NUMBER
- );
-
- addNumericFieldWithExtractor(
- "ct_numerator",
- "Current transformer ratio (numerator).",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Dimensionless,
- VifScaling::None, DifSignedness::Signed,
- FieldMatcher::build()
- .set(DifVifKey("04FFA015")),
- Unit::FACTOR
- );
-
- addNumericFieldWithExtractor(
- "vt_numerator",
- "Voltage transformer ratio (numerator).",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Dimensionless,
- VifScaling::None, DifSignedness::Signed,
- FieldMatcher::build()
- .set(DifVifKey("04FFA115")),
- Unit::FACTOR
- );
-
- addNumericFieldWithExtractor(
- "ct_denominator",
- "Current transformer ratio (denominator).",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Dimensionless,
- VifScaling::None, DifSignedness::Signed,
- FieldMatcher::build()
- .set(DifVifKey("04FFA215")),
- Unit::FACTOR
- );
-
- addNumericFieldWithExtractor(
- "vt_denominator",
- "Voltage transformer ratio (denominator).",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Dimensionless,
- VifScaling::None, DifSignedness::Signed,
- FieldMatcher::build()
- .set(DifVifKey("04FFA315")),
- Unit::FACTOR
- );
-
- addStringFieldWithExtractorAndLookup(
- "error_flags",
- "Error flags.",
- DEFAULT_PRINT_PROPERTIES | PrintProperty::INJECT_INTO_STATUS,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(DifVifKey("07FFA600")),
- Translate::Lookup()
- .add(Translate::Rule("ERROR_FLAGS", Translate::MapType::BitToString)
- .set(MaskBits(0xffffffffffffffff))
- .set(DefaultMessage("OK"))
- ));
-
- addStringFieldWithExtractorAndLookup(
- "warning_flags",
- "Warning flags.",
- DEFAULT_PRINT_PROPERTIES | PrintProperty::INJECT_INTO_STATUS,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(DifVifKey("07FFA700")),
- Translate::Lookup()
- .add(Translate::Rule("WARNING_FLAGS", Translate::MapType::BitToString)
- .set(MaskBits(0xffffffffffffffff))
- .set(DefaultMessage("OK"))
- ));
-
- addStringFieldWithExtractorAndLookup(
- "information_flags",
- "Information flags.",
- DEFAULT_PRINT_PROPERTIES,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(DifVifKey("07FFA800")),
- Translate::Lookup()
- .add(Translate::Rule("INFORMATION_FLAGS", Translate::MapType::BitToString)
- .set(MaskBits(0xffffffffffffffff))
- .set(DefaultMessage(""))
- ));
-
- addStringFieldWithExtractorAndLookup(
- "alarm_flags",
- "alarm flags.",
- DEFAULT_PRINT_PROPERTIES | PrintProperty::INJECT_INTO_STATUS,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(DifVifKey("07FFA900")),
- Translate::Lookup()
- .add(Translate::Rule("ALARM_FLAGS", Translate::MapType::BitToString)
- .set(MaskBits(0xfffffffffffffff))
- .set(DefaultMessage("OK"))
- ));
-
- addStringFieldWithExtractorAndLookup(
- "unknown_vif_FFAD",
- "Unknown byte.",
- DEFAULT_PRINT_PROPERTIES,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(DifVifKey("01FFAD00")),
- Translate::Lookup()
- .add(Translate::Rule("UNKNOWN", Translate::MapType::BitToString)
- .set(MaskBits(0xff))
- .set(DefaultMessage("OK"))
- ));
-
- addStringFieldWithExtractor(
- "firmware_version",
- "Firmware version.",
- DEFAULT_PRINT_PROPERTIES,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::FirmwareVersion)
- .add(VIFCombinableRaw(0))
- );
-
- addStringFieldWithExtractor(
- "product_no",
- "The meter device product number.",
- DEFAULT_PRINT_PROPERTIES,
- FieldMatcher::build().
- set(DifVifKey("0DFFAA00")));
-
- addNumericFieldWithExtractor(
- "power_fail",
- "Power fail counter.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Dimensionless,
- VifScaling::None, DifSignedness::Signed,
- FieldMatcher::build()
- .set(DifVifKey("04FF9800"))
- );
-
- addNumericFieldWithExtractor(
- "active_consumption",
- "Instantaneous total active imported power.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Power,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyPowerVIF)
- .add(VIFCombinableRaw(0))
- );
-
- addNumericFieldWithExtractor(
- "active_consumption_l1",
- "Instantaneous active imported power for L1 phase.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Power,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyPowerVIF)
- .add(VIFCombinableRaw(0x7f01))
- .add(VIFCombinableRaw(0))
- );
-
- addNumericFieldWithExtractor(
- "active_consumption_l2",
- "Instantaneous active imported power for L2 phase.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Power,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyPowerVIF)
- .add(VIFCombinableRaw(0x7f02))
- .add(VIFCombinableRaw(0))
- );
-
- addNumericFieldWithExtractor(
- "active_consumption_l3",
- "Instantaneous active imported power for L3 phase.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Power,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyPowerVIF)
- .add(VIFCombinableRaw(0x7f03))
- .add(VIFCombinableRaw(0))
- );
-
- addNumericFieldWithExtractor(
- "reactive_consumption",
- "Instantaneous total reactive imported power.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Power,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyPowerVIF)
- .set(SubUnitNr(2))
- .add(VIFCombinableRaw(0))
- );
-
- addNumericFieldWithExtractor(
- "reactive_consumption_l1",
- "Instantaneous reactive imported power for L1 phase.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Power,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyPowerVIF)
- .set(SubUnitNr(2))
- .add(VIFCombinableRaw(0x7f01))
- .add(VIFCombinableRaw(0))
- );
-
- addNumericFieldWithExtractor(
- "reactive_consumption_l2",
- "Instantaneous reactive imported power for L2 phase.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Power,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyPowerVIF)
- .set(SubUnitNr(2))
- .add(VIFCombinableRaw(0x7f02))
- .add(VIFCombinableRaw(0))
- );
-
- addNumericFieldWithExtractor(
- "reactive_consumption_l3",
- "Instantaneous reactive imported power for L3 phase.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Power,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyPowerVIF)
- .set(SubUnitNr(2))
- .add(VIFCombinableRaw(0x7f03))
- .add(VIFCombinableRaw(0))
- );
-
- addNumericFieldWithExtractor(
- "apparent_consumption",
- "Instantaneous total apparent imported power.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Power,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyPowerVIF)
- .set(SubUnitNr(4))
- .add(VIFCombinableRaw(0))
- );
-
- addNumericFieldWithExtractor(
- "apparent_consumption_l1",
- "Instantaneous apparent imported power for L1 phase.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Power,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyPowerVIF)
- .set(SubUnitNr(4))
- .add(VIFCombinableRaw(0x7f01))
- .add(VIFCombinableRaw(0))
- );
-
- addNumericFieldWithExtractor(
- "apparent_consumption_l2",
- "Instantaneous apparent imported power for L2 phase.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Power,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyPowerVIF)
- .set(SubUnitNr(4))
- .add(VIFCombinableRaw(0x7f02))
- .add(VIFCombinableRaw(0))
- );
-
- addNumericFieldWithExtractor(
- "apparent_consumption_l3",
- "Instantaneous apparent imported power for L3 phase.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Power,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyPowerVIF)
- .set(SubUnitNr(4))
- .add(VIFCombinableRaw(0x7f03))
- .add(VIFCombinableRaw(0))
- );
-
- addNumericFieldWithExtractor(
- "voltage_l1_n",
- "Instantaneous voltage between L1 and neutral.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Voltage,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::Voltage)
- .add(VIFCombinableRaw(0x7f01))
- .add(VIFCombinableRaw(0))
- );
-
- addNumericFieldWithExtractor(
- "voltage_l2_n",
- "Instantaneous voltage between L2 and neutral.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Voltage,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::Voltage)
- .add(VIFCombinableRaw(0x7f02))
- .add(VIFCombinableRaw(0))
- );
-
- addNumericFieldWithExtractor(
- "voltage_l3_n",
- "Instantaneous voltage between L3 and neutral.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Voltage,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::Voltage)
- .add(VIFCombinableRaw(0x7f03))
- .add(VIFCombinableRaw(0))
- );
-
- addNumericFieldWithExtractor(
- "voltage_l1_l2",
- "Instantaneous voltage between L1 and L2.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Voltage,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::Voltage)
- .add(VIFCombinableRaw(0x7f05))
- .add(VIFCombinableRaw(0))
- );
-
- addNumericFieldWithExtractor(
- "voltage_l2_l3",
- "Instantaneous voltage between L2 and L3.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Voltage,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::Voltage)
- .add(VIFCombinableRaw(0x7f06))
- .add(VIFCombinableRaw(0))
- );
-
- addNumericFieldWithExtractor(
- "voltage_l3_l1",
- "Instantaneous voltage between L3 and L1.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Voltage,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::Voltage)
- .add(VIFCombinableRaw(0x7f07))
- .add(VIFCombinableRaw(0))
- );
-
- addNumericFieldWithExtractor(
- "current_l1",
- "Instantaneous current in the L1 phase.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Amperage,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::Amperage)
- .add(VIFCombinableRaw(0x7f01))
- .add(VIFCombinableRaw(0))
- );
-
- addNumericFieldWithExtractor(
- "current_l2",
- "Instantaneous current in the L2 phase.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Amperage,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::Amperage)
- .add(VIFCombinableRaw(0x7f02))
- .add(VIFCombinableRaw(0))
- );
-
- addNumericFieldWithExtractor(
- "current_l3",
- "Instantaneous current in the L3 phase.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Amperage,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::Amperage)
- .add(VIFCombinableRaw(0x7f03))
- .add(VIFCombinableRaw(0))
- );
-
- addNumericFieldWithExtractor(
- "frequency",
- "Frequency of AC",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Frequency,
- VifScaling::None, DifSignedness::Signed,
- FieldMatcher::build()
- .set(DifVifKey("0AFFD900")),
- Unit::HZ,
- 0.01
- );
-
- addNumericFieldWithExtractor(
- "power",
- "Power factor.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Dimensionless,
- VifScaling::None, DifSignedness::Signed,
- FieldMatcher::build()
- .set(DifVifKey("02FFE000")),
- Unit::FACTOR,
- 0.001
- );
-
- addNumericFieldWithExtractor(
- "power_l1",
- "Power factor for phase L1.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Dimensionless,
- VifScaling::None, DifSignedness::Signed,
- FieldMatcher::build()
- .set(DifVifKey("02FFE0FF8100")),
- Unit::FACTOR,
- 0.001
- );
-
- addNumericFieldWithExtractor(
- "power_l2",
- "Power factor for phase L2.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Dimensionless,
- VifScaling::None, DifSignedness::Signed,
- FieldMatcher::build()
- .set(DifVifKey("02FFE0FF8200")),
- Unit::FACTOR,
- 0.001
- );
-
- addNumericFieldWithExtractor(
- "power_l3",
- "Power factor.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Dimensionless,
- VifScaling::None, DifSignedness::Signed,
- FieldMatcher::build()
- .set(DifVifKey("02FFE0FF8300")),
- Unit::FACTOR,
- 0.001
- );
-
- addNumericFieldWithExtractor(
- "power_phase_angle",
- "Total power phase angle.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Angle,
- VifScaling::None, DifSignedness::Signed,
- FieldMatcher::build()
- .set(DifVifKey("02FFD200")),
- Unit::DEGREE,
- 0.1
- );
-
- addNumericFieldWithExtractor(
- "phase_angle_power_l1",
- "Power phase angle for phase L1.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Angle,
- VifScaling::None, DifSignedness::Signed,
- FieldMatcher::build()
- .set(DifVifKey("02FFD2FF8100")),
- Unit::DEGREE,
- 0.1
- );
-
- addNumericFieldWithExtractor(
- "phase_angle_power_l2",
- "Power phase angle for phase L2.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Angle,
- VifScaling::None, DifSignedness::Signed,
- FieldMatcher::build()
- .set(DifVifKey("02FFD2FF8200")),
- Unit::DEGREE,
- 0.1
- );
-
- addNumericFieldWithExtractor(
- "phase_angle_power_l3",
- "Power phase angle for phase L3.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Angle,
- VifScaling::None, DifSignedness::Signed,
- FieldMatcher::build()
- .set(DifVifKey("02FFD2FF8300")),
- Unit::DEGREE,
- 0.1
- );
-
- addNumericFieldWithExtractor(
- "total_reactive_energy_consumption",
- "Total cumulative reactive kvarh imported energy.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Reactive_Energy,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyEnergyVIF)
- .set(SubUnitNr(2))
- .add(VIFCombinableRaw(0)),
- Unit::KVARH
- );
-
- addNumericFieldWithExtractor(
- "total_reactive_energy_consumption_tariff_{tariff_counter}",
- "Total cumulative reactive kvarh imported energy per tariff.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Energy,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyEnergyVIF)
- .set(SubUnitNr(2))
- .set(TariffNr(1),TariffNr(4))
- .add(VIFCombinableRaw(0))
- );
-
- addNumericFieldWithExtractor(
- "total_reactive_energy_production",
- "Total cumulative reactive kvarh exported energy.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Reactive_Energy,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyEnergyVIF)
- .set(SubUnitNr(3))
- .add(VIFCombinableRaw(0)),
- Unit::KVARH
- );
-
- addNumericFieldWithExtractor(
- "total_reactive_energy_production_tariff_{tariff_counter}",
- "Total cumulative reactive kvarh exported energy per tariff.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Energy,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyEnergyVIF)
- .set(SubUnitNr(3))
- .set(TariffNr(1),TariffNr(4))
- .add(VIFCombinableRaw(0))
- );
-
- addNumericFieldWithExtractor(
- "current_quadrant",
- "The quadrant in which the current is measured.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Dimensionless,
- VifScaling::None, DifSignedness::Signed,
- FieldMatcher::build()
- .set(DifVifKey("01FF9700")),
- Unit::NUMBER
- );
-
- addNumericFieldWithExtractor(
- "current_quadrant_l1",
- "The quadrant in which the current is measured for phase L1.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Dimensionless,
- VifScaling::None, DifSignedness::Signed,
- FieldMatcher::build()
- .set(DifVifKey("01FF97FF8100")),
- Unit::NUMBER
- );
-
- addNumericFieldWithExtractor(
- "current_quadrant_l2",
- "The quadrant in which the current is measured for phase L2.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Dimensionless,
- VifScaling::None, DifSignedness::Signed,
- FieldMatcher::build()
- .set(DifVifKey("01FF97FF8200")),
- Unit::NUMBER
- );
-
- addNumericFieldWithExtractor(
- "current_quadrant_l3",
- "The quadrant in which the current is measured for phase L3.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Dimensionless,
- VifScaling::None, DifSignedness::Signed,
- FieldMatcher::build()
- .set(DifVifKey("01FF97FF8300")),
- Unit::NUMBER
- );
-
- addStringFieldWithExtractorAndLookup(
- "digital_output_{subunit_counter}",
- "The state for output register 1-2.",
- DEFAULT_PRINT_PROPERTIES,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::DigitalOutput)
- .set(StorageNr(0))
- .set(TariffNr(0))
- .set(SubUnitNr(1),SubUnitNr(2))
- .add(VIFCombinableRaw(0)),
- Translate::Lookup()
- .add(Translate::Rule("OUTPUT", Translate::MapType::BitToString)
- .set(MaskBits(0xff))
- ));
-
- addStringFieldWithExtractorAndLookup(
- "digital_input_{subunit_counter-2counter}",
- "The state for input register 1-2.",
- DEFAULT_PRINT_PROPERTIES,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::DigitalInput)
- .set(SubUnitNr(3),SubUnitNr(4))
- .add(VIFCombinableRaw(0)),
- Translate::Lookup()
- .add(Translate::Rule("INPUT", Translate::MapType::BitToString)
- .set(MaskBits(0xff))
- ));
-
- addStringFieldWithExtractorAndLookup(
- "digital_historic_input_{subunit_counter-2counter}",
- "The state for input register 3-4.",
- DEFAULT_PRINT_PROPERTIES,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::DigitalInput)
- .set(StorageNr(1))
- .set(SubUnitNr(3),SubUnitNr(4))
- .add(VIFCombinableRaw(0)),
- Translate::Lookup()
- .add(Translate::Rule("INPUT", Translate::MapType::BitToString)
- .set(MaskBits(0xff))
- ));
-
- addNumericFieldWithExtractor(
- "digital_input_{subunit_counter-2counter}",
- "Number of times input 1-2 counted a 1.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Dimensionless,
- VifScaling::None, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(SubUnitNr(3),SubUnitNr(4))
- .set(VIFRange::CumulationCounter)
- .add(VIFCombinableRaw(0))
- );
-
- addNumericFieldWithExtractor(
- "resettable_energy_consumption",
- "Resettable cumulative active imported energy.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Energy,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyEnergyVIF)
- .add(VIFCombinableRaw(0x7f72))
- .add(VIFCombinableRaw(0))
- );
-
- addNumericFieldWithExtractor(
- "resettable_energy_production",
- "Resettable cumulative active exported energy.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Energy,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyEnergyVIF)
- .set(SubUnitNr(1))
- .add(VIFCombinableRaw(0x7f72))
- .add(VIFCombinableRaw(0))
- );
-
- addNumericFieldWithExtractor(
- "resettable_reactive_energy_consumption",
- "Resettable cumulative reactive imported energy.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Energy,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyEnergyVIF)
- .set(SubUnitNr(2))
- .add(VIFCombinableRaw(0x7f72))
- .add(VIFCombinableRaw(0))
- );
-
- addNumericFieldWithExtractor(
- "resettable_reactive_energy_production",
- "Resettable cumulative reactive exported energy.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Energy,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyEnergyVIF)
- .set(SubUnitNr(3))
- .add(VIFCombinableRaw(0x7f72))
- .add(VIFCombinableRaw(0))
- );
-
- addNumericFieldWithExtractor(
- "reset_energy_consumption",
- "Number of times the resettable energy imported value has been reset.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Dimensionless,
- VifScaling::None, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRaw(0x7f71))
- .add(VIFCombinableRaw(0))
- );
-
- addNumericFieldWithExtractor(
- "reset_energy_production",
- "Number of times the resettable active energy exported value has been reset.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Dimensionless,
- VifScaling::None, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRaw(0x7f71))
- .set(SubUnitNr(1))
- .add(VIFCombinableRaw(0))
- );
-
- addNumericFieldWithExtractor(
- "reset_reactive_energy_consumption",
- "Number of times the resettable reactive energy imported value has been reset.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Dimensionless,
- VifScaling::None, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRaw(0x7f71))
- .set(SubUnitNr(2))
- .add(VIFCombinableRaw(0))
- );
-
- addNumericFieldWithExtractor(
- "reset_reactive_energy_production",
- "Number of times the resettable reactive energy exported value has been reset.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Dimensionless,
- VifScaling::None, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRaw(0x7f71))
- .set(SubUnitNr(3))
- .add(VIFCombinableRaw(0))
- );
-
- addNumericFieldWithExtractor(
- "energy_co2",
- "Energy in co2.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Mass,
- VifScaling::None, DifSignedness::Signed,
- FieldMatcher::build()
- .set(DifVifKey("0EFFF9C400")),
- Unit::KG,
- 0.01
- );
-
- addNumericFieldWithExtractor(
- "co2_conversion",
- "CO2 conversion factor (kg * 10-3 /kWh).",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Dimensionless,
- VifScaling::None, DifSignedness::Signed,
- FieldMatcher::build()
- .set(DifVifKey("04FFA400")),
- Unit::FACTOR
- );
-
- addNumericFieldWithExtractor(
- "energy_currency",
- "Energy in currency.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Dimensionless,
- VifScaling::None, DifSignedness::Signed,
- FieldMatcher::build()
- .set(DifVifKey("0EFFF9C900"))
- );
-
- addNumericFieldWithExtractor(
- "currency_conversion",
- "Currency conversion factor (curr * 10-3 /kWh).",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Dimensionless,
- VifScaling::None, DifSignedness::Signed,
- FieldMatcher::build()
- .set(DifVifKey("04FFA500")),
- Unit::FACTOR
- );
-
- addNumericFieldWithExtractor(
- "total_apparent_energy_consumption",
- "Total cumulative apparent kvah imported energy.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Apparent_Energy,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyEnergyVIF)
- .set(SubUnitNr(4))
- .add(VIFCombinableRaw(0)),
- Unit::KVAH
- );
-
- addNumericFieldWithExtractor(
- "total_apparent_energy_production",
- "Total cumulative apparent kvah exported energy.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Apparent_Energy,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyEnergyVIF)
- .set(SubUnitNr(5))
- .add(VIFCombinableRaw(0)),
- Unit::KVAH
- );
-
- addNumericFieldWithExtractor(
- "total_energy_consumption_l1",
- "Total imported active energy for L1 phase.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Energy,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyEnergyVIF)
- .add(VIFCombinableRaw(0x7f01))
- .add(VIFCombinableRaw(0))
- );
-
- addNumericFieldWithExtractor(
- "total_energy_consumption_l2",
- "Total imported active energy for L2 phase.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Energy,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyEnergyVIF)
- .add(VIFCombinableRaw(0x7f02))
- .add(VIFCombinableRaw(0))
- );
-
- addNumericFieldWithExtractor(
- "total_energy_consumption_l3",
- "Total imported active energy for L3 phase.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Energy,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyEnergyVIF)
- .add(VIFCombinableRaw(0x7f03))
- .add(VIFCombinableRaw(0))
- );
-
- addNumericFieldWithExtractor(
- "total_reactive_energy_consumption_l1",
- "Total imported reactive energy for L1 phase.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Energy,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyEnergyVIF)
- .set(SubUnitNr(2))
- .add(VIFCombinableRaw(0x7f01))
- .add(VIFCombinableRaw(0))
- );
-
- addNumericFieldWithExtractor(
- "total_reactive_energy_consumption_l2",
- "Total imported reactive energy for L2 phase.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Energy,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyEnergyVIF)
- .set(SubUnitNr(2))
- .add(VIFCombinableRaw(0x7f02))
- .add(VIFCombinableRaw(0))
- );
-
- addNumericFieldWithExtractor(
- "total_reactive_energy_consumption_l3",
- "Total imported reactive energy for L3 phase.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Energy,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyEnergyVIF)
- .set(SubUnitNr(2))
- .add(VIFCombinableRaw(0x7f03))
- .add(VIFCombinableRaw(0))
- );
-
- addNumericFieldWithExtractor(
- "total_apparent_energy_consumption_l1",
- "Total imported apparent energy for L1 phase.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Energy,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyEnergyVIF)
- .set(SubUnitNr(4))
- .add(VIFCombinableRaw(0x7f01))
- .add(VIFCombinableRaw(0))
- );
-
- addNumericFieldWithExtractor(
- "total_apparent_energy_consumption_l2",
- "Total imported apparent energy for L2 phase.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Energy,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyEnergyVIF)
- .set(SubUnitNr(4))
- .add(VIFCombinableRaw(0x7f02))
- .add(VIFCombinableRaw(0))
- );
-
- addNumericFieldWithExtractor(
- "total_apparent_energy_consumption_l3",
- "Total imported apparent energy for L3 phase.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Energy,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyEnergyVIF)
- .set(SubUnitNr(4))
- .add(VIFCombinableRaw(0x7f03))
- .add(VIFCombinableRaw(0))
- );
-
- addNumericFieldWithExtractor(
- "total_energy_production_l1",
- "Total exported active energy for L1 phase.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Energy,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyEnergyVIF)
- .set(SubUnitNr(1))
- .add(VIFCombinableRaw(0x7f01))
- .add(VIFCombinableRaw(0))
- );
-
- addNumericFieldWithExtractor(
- "total_energy_production_l2",
- "Total exported active energy for L2 phase.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Energy,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyEnergyVIF)
- .set(SubUnitNr(1))
- .add(VIFCombinableRaw(0x7f02))
- .add(VIFCombinableRaw(0))
- );
-
- addNumericFieldWithExtractor(
- "total_energy_production_l3",
- "Total exported active energy for L3 phase.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Energy,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyEnergyVIF)
- .set(SubUnitNr(1))
- .add(VIFCombinableRaw(0x7f03))
- .add(VIFCombinableRaw(0))
- );
-
- addNumericFieldWithExtractor(
- "total_reactive_energy_production_l1",
- "Total exported reactive energy for L1 phase.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Energy,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyEnergyVIF)
- .set(SubUnitNr(3))
- .add(VIFCombinableRaw(0x7f01))
- .add(VIFCombinableRaw(0))
- );
-
- addNumericFieldWithExtractor(
- "total_reactive_energy_production_l2",
- "Total exported reactive energy for L2 phase.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Energy,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyEnergyVIF)
- .set(SubUnitNr(3))
- .add(VIFCombinableRaw(0x7f02))
- .add(VIFCombinableRaw(0))
- );
-
- addNumericFieldWithExtractor(
- "total_reactive_energy_production_l3",
- "Total exported reactive energy for L3 phase.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Energy,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyEnergyVIF)
- .set(SubUnitNr(3))
- .add(VIFCombinableRaw(0x7f03))
- .add(VIFCombinableRaw(0))
- );
-
- addNumericFieldWithExtractor(
- "total_apparent_energy_production_l1",
- "Total exported apparent energy for L1 phase.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Energy,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyEnergyVIF)
- .set(SubUnitNr(5))
- .add(VIFCombinableRaw(0x7f01))
- .add(VIFCombinableRaw(0))
- );
-
- addNumericFieldWithExtractor(
- "total_apparent_energy_production_l2",
- "Total exported apparent energy for L2 phase.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Energy,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyEnergyVIF)
- .set(SubUnitNr(5))
- .add(VIFCombinableRaw(0x7f02))
- .add(VIFCombinableRaw(0))
- );
-
- addNumericFieldWithExtractor(
- "total_apparent_energy_production_l3",
- "Total exported apparent energy for L3 phase.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Energy,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyEnergyVIF)
- .set(SubUnitNr(5))
- .add(VIFCombinableRaw(0x7f03))
- .add(VIFCombinableRaw(0))
- );
-
- addNumericFieldWithExtractor(
- "total_net_energy",
- "Active net energy total.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Energy,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyEnergyVIF)
- .set(SubUnitNr(6))
- .add(VIFCombinableRaw(0))
- );
-
- addNumericFieldWithExtractor(
- "total_net_energy_l1",
- "Active net energy total for phase L1.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Energy,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyEnergyVIF)
- .set(SubUnitNr(6))
- .add(VIFCombinableRaw(0x7f01))
- .add(VIFCombinableRaw(0))
- );
-
- addNumericFieldWithExtractor(
- "total_net_energy_l2",
- "Active net energy total for phase L2.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Energy,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyEnergyVIF)
- .set(SubUnitNr(6))
- .add(VIFCombinableRaw(0x7f02))
- .add(VIFCombinableRaw(0))
- );
-
- addNumericFieldWithExtractor(
- "total_net_energy_l3",
- "Active net energy total for phase L3.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Energy,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyEnergyVIF)
- .set(SubUnitNr(6))
- .add(VIFCombinableRaw(0x7f03))
- .add(VIFCombinableRaw(0))
- );
-
- addNumericFieldWithExtractor(
- "total_net_reactive_energy",
- "Active net energy total.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Energy,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyEnergyVIF)
- .set(SubUnitNr(7))
- .add(VIFCombinableRaw(0)),
- Unit::KVARH
- );
-
- addNumericFieldWithExtractor(
- "total_net_reactive_energy_l1",
- "Active net reactive energy total for phase L1.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Energy,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyEnergyVIF)
- .set(SubUnitNr(7))
- .add(VIFCombinableRaw(0x7f01))
- .add(VIFCombinableRaw(0)),
- Unit::KVARH
- );
-
- addNumericFieldWithExtractor(
- "total_net_reactive_energy_l2",
- "Active net reactive energy total for phase L2.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Energy,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyEnergyVIF)
- .set(SubUnitNr(7))
- .add(VIFCombinableRaw(0x7f02))
- .add(VIFCombinableRaw(0)),
- Unit::KVARH
- );
-
- addNumericFieldWithExtractor(
- "total_net_reactive_energy_l3",
- "Active net reactive energy total for phase L3.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Energy,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyEnergyVIF)
- .set(SubUnitNr(7))
- .add(VIFCombinableRaw(0x7f03))
- .add(VIFCombinableRaw(0)),
- Unit::KVARH
- );
-
- addNumericFieldWithExtractor(
- "total_net_apparent_energy",
- "Active net energy total.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Energy,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyEnergyVIF)
- .set(SubUnitNr(8))
- .add(VIFCombinableRaw(0)),
- Unit::KVAH
- );
-
- addNumericFieldWithExtractor(
- "total_net_apparent_energy_l1",
- "Active net apparent energy total for phase L1.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Energy,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyEnergyVIF)
- .set(SubUnitNr(8))
- .add(VIFCombinableRaw(0x7f01))
- .add(VIFCombinableRaw(0)),
- Unit::KVAH
- );
-
- addNumericFieldWithExtractor(
- "total_net_apparent_energy_l2",
- "Active net apparent energy total for phase L2.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Energy,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyEnergyVIF)
- .set(SubUnitNr(8))
- .add(VIFCombinableRaw(0x7f02))
- .add(VIFCombinableRaw(0)),
- Unit::KVAH
- );
-
- addNumericFieldWithExtractor(
- "total_net_apparent_energy_l3",
- "Active net apparent energy total for phase L3.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Energy,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyEnergyVIF)
- .set(SubUnitNr(8))
- .add(VIFCombinableRaw(0x7f03))
- .add(VIFCombinableRaw(0)),
- Unit::KVAH
- );
-
- }
-}
-
-/* / Test: ABBmeter abbb23 33221100 NOKEY
- / telegram=|844442040011223320027A3E000020_0E840017495200000004FFA0150000000004FFA1150000000004FFA2150000000004FFA3150000000007FFA600000000000000000007FFA700000000000000000007FFA800000000000000000007FFA90000000000000000000DFD8E0007302E38322E31420DFFAA000B3030312D313131203332421F|
- / {"media":"electricity","meter":"abbb23","name":"ABBmeter","id":"33221100","total_energy_consumption_kwh":5249.17,"firmware_version": "B1.28.0","product_no": "B23 111-100","timestamp":"1111-11-11T11:11:11Z"}
- / |ABBmeter;33221100;5249.17;1111-11-11 11:11.11
-*/
diff --git a/components/wmbus_common/driver_aerius.cpp b/components/wmbus_common/driver_aerius.cpp
deleted file mode 100644
index a3c7c049..00000000
--- a/components/wmbus_common/driver_aerius.cpp
+++ /dev/null
@@ -1,104 +0,0 @@
-/*
- Copyright (C) 2017-2023 Fredrik Öhrström (gpl-3.0-or-later)
-
- 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 .
-*/
-
-#include"meters_common_implementation.h"
-
-namespace
-{
- struct Driver : public virtual MeterCommonImplementation
- {
- Driver(MeterInfo &mi, DriverInfo &di);
- };
-
- static bool ok = staticRegisterDriver([](DriverInfo&di)
- {
- di.setName("aerius");
- di.setDefaultFields("name,id,total_m3,timestamp");
- di.setMeterType(MeterType::GasMeter);
- di.addLinkMode(LinkMode::T1);
- di.addMVT(MANUFACTURER_DME, 0x03, 0x30);
- di.setConstructor([](MeterInfo& mi, DriverInfo& di){ return shared_ptr(new Driver(mi, di)); });
- });
-
- Driver::Driver(MeterInfo &mi, DriverInfo &di) : MeterCommonImplementation(mi, di)
- {
- addNumericFieldWithExtractor(
- "total",
- "The total gas consumption recorded by this meter.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Volume,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::Volume)
- .add(VIFCombinable::ValueAtBaseCondC)
- );
-
- addNumericFieldWithExtractor(
- "flow",
- "The current gas flow.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Flow,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::VolumeFlow)
- );
-
- addNumericFieldWithExtractor(
- "temperature",
- "The current temperature.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Temperature,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::FlowTemperature)
- );
-
- addNumericFieldWithExtractor(
- "target",
- "Date time when previous billing period ended.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::PointInTime,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::DateTime)
- .set(StorageNr(3))
- );
-
- addNumericFieldWithExtractor(
- "target",
- "The total gas consumption recorded when the previous billing period ended.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Volume,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::Volume)
- .set(StorageNr(3))
- .add(VIFCombinable::ValueAtBaseCondC)
- );
-
- }
-}
-
-// Test: aerius_gas aerius 99657098 NOKEY
-// telegram=|2E44A5119870659930037A060020052F2F_0C933E842784060A3B00000A5A5901C4016D3B37DF2CCC01933E24032606|
-// {"_":"telegram","media":"gas","meter":"aerius","name":"aerius_gas","id":"99657098","total_m3":6842.784,"flow_m3h":0,"temperature_c":15.9,"target_datetime":"2022-12-31 23:59","target_m3":6260.324,"timestamp":"1111-11-11T11:11:11Z"}
-// |aerius_gas;99657098;6842.784;1111-11-11 11:11.11
diff --git a/components/wmbus_common/driver_amiplus.cpp b/components/wmbus_common/driver_amiplus.cpp
deleted file mode 100644
index 0f5013c4..00000000
--- a/components/wmbus_common/driver_amiplus.cpp
+++ /dev/null
@@ -1,251 +0,0 @@
-/*
- Copyright (C) 2019-2025 Fredrik Öhrström (gpl-3.0-or-later)
-
- 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 .
-*/
-
-#include"meters_common_implementation.h"
-
-namespace
-{
- struct Driver : public virtual MeterCommonImplementation
- {
- Driver(MeterInfo &mi, DriverInfo &di);
- };
-
- static bool ok = staticRegisterDriver([](DriverInfo&di)
- {
- di.setName("amiplus");
- di.setDefaultFields("name,id,total_energy_consumption_kwh,current_power_consumption_kw,total_energy_production_kwh,current_power_production_kw,voltage_at_phase_1_v,voltage_at_phase_2_v,voltage_at_phase_3_v,total_energy_consumption_tariff_1_kwh,total_energy_consumption_tariff_2_kwh,total_energy_consumption_tariff_3_kwh,total_energy_production_tariff_1_kwh,total_energy_production_tariff_2_kwh,total_energy_production_tariff_3_kwh,timestamp");
- di.setMeterType(MeterType::ElectricityMeter);
- di.addLinkMode(LinkMode::T1);
- di.addMVT(MANUFACTURER_APA, 0x02, 0x02);
- di.addMVT(MANUFACTURER_DEV, 0x37, 0x02);
- di.addMVT(MANUFACTURER_DEV, 0x02, 0x00);
- di.addMVT(MANUFACTURER_DEV, 0x02, 0x01);
- di.addMVT(MANUFACTURER_NES, 0x02, 0x03);
- // Apator Otus 1/3 seems to use both, depending on a frame.
- // Frames with APA are successfully decoded by this driver
- // Frames with APT are not - and their content is unknown - perhaps it broadcasts two data formats?
- di.addMVT(MANUFACTURER_APA, 0x02, 0x01);
- //di.addMVT(MANUFACTURER_APT, 0x02, 0x01);
- di.setConstructor([](MeterInfo& mi, DriverInfo& di){ return shared_ptr(new Driver(mi, di)); });
- });
-
- Driver::Driver(MeterInfo &mi, DriverInfo &di) : MeterCommonImplementation(mi, di)
- {
- addNumericFieldWithExtractor(
- "total_energy_consumption",
- "The total energy consumption recorded by this meter.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Energy,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyEnergyVIF)
- );
-
- addNumericFieldWithExtractor(
- "current_power_consumption",
- "Current power consumption.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Power,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::PowerW)
- );
-
- addNumericFieldWithExtractor(
- "total_energy_production",
- "The total energy production recorded by this meter.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Energy,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(DifVifKey("0E833C"))
- );
-
- addNumericFieldWithExtractor(
- "current_power_production",
- "Current power production.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Power,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(DifVifKey("0BAB3C"))
- );
-
- addNumericFieldWithExtractor(
- "voltage_at_phase_1",
- "Voltage at phase L1.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Voltage,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::Voltage)
- .add(VIFCombinable::AtPhase1)
- );
-
- addNumericFieldWithExtractor(
- "voltage_at_phase_2",
- "Voltage at phase L2.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Voltage,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::Voltage)
- .add(VIFCombinable::AtPhase2)
- );
-
- addNumericFieldWithExtractor(
- "voltage_at_phase_3",
- "Voltage at phase L3.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Voltage,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::Voltage)
- .add(VIFCombinable::AtPhase3)
- );
-
- addStringFieldWithExtractor(
- "device_date_time",
- "Device date time.",
- DEFAULT_PRINT_PROPERTIES,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::DateTime)
- );
-
- addNumericFieldWithExtractor(
- "total_energy_consumption_tariff_1",
- "The total energy consumption recorded by this meter on tariff 1.",
- DEFAULT_PRINT_PROPERTIES, // ,
- Quantity::Energy,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyEnergyVIF)
- .set(TariffNr(1))
- );
-
- addNumericFieldWithExtractor(
- "total_energy_consumption_tariff_2",
- "The total energy consumption recorded by this meter on tariff 2.",
- DEFAULT_PRINT_PROPERTIES, // ,
- Quantity::Energy,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyEnergyVIF)
- .set(TariffNr(2))
- );
-
- addNumericFieldWithExtractor(
- "total_energy_consumption_tariff_3",
- "The total energy consumption recorded by this meter on tariff 3.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Energy,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyEnergyVIF)
- .set(TariffNr(3))
- );
-
- addNumericFieldWithExtractor(
- "total_energy_production_tariff_1",
- "The total energy production recorded by this meter on tariff 1.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Energy,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(DifVifKey("8E10833C"))
- );
-
- addNumericFieldWithExtractor(
- "total_energy_production_tariff_2",
- "The total energy production recorded by this meter on tariff 2.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Energy,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(DifVifKey("8E20833C"))
- );
-
- addNumericFieldWithExtractor(
- "total_energy_production_tariff_3",
- "The total energy production recorded by this meter on tariff 3.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Energy,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(DifVifKey("8E30833C"))
- );
-
- addNumericFieldWithExtractor(
- "max_power_consumption",
- "The maximum demand indicator (maximum 15-min average power consumption recorded this month).",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Power,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Maximum)
- .set(VIFRange::AnyPowerVIF)
- );
-
- }
-}
-
-// Test: MyElectricity1 amiplus 10101010 NOKEY
-// telegram=|4E4401061010101002027A00004005_2F2F0E035040691500000B2B300300066D00790C7423400C78371204860BABC8FC100000000E833C8074000000000BAB3C0000000AFDC9FC0136022F2F2F2F2F|
-// {"_":"telegram","media":"electricity","meter":"amiplus","name":"MyElectricity1","id":"10101010","total_energy_consumption_kwh":15694.05,"current_power_consumption_kw":0.33,"total_energy_production_kwh":7.48,"current_power_production_kw":0,"voltage_at_phase_1_v":236,"device_date_time":"2019-03-20 12:57:00","timestamp":"1111-11-11T11:11:11Z"}
-// |MyElectricity1;10101010;15694.05;0.33;7.48;0;236;null;null;null;null;null;null;null;null;1111-11-11 11:11.11
-
-// Test: MyElectricity2 amiplus 00254358 NOKEY
-// Comment: amiplus/apator electricity meter with three phase voltages
-// telegram=|5E44B6105843250000027A2A005005_2F2F0C7835221400066D404708AC2A400E032022650900000E833C0000000000001B2B9647000B2B5510000BAB3C0000000AFDC9FC0135020AFDC9FC0245020AFDC9FC0339020BABC8FC100000002F2F|
-// {"_":"telegram","media":"electricity","meter":"amiplus","name":"MyElectricity2","id":"00254358","total_energy_consumption_kwh":9652.22,"current_power_consumption_kw":1.055,"total_energy_production_kwh":0,"current_power_production_kw":0,"voltage_at_phase_1_v":235,"voltage_at_phase_2_v":245,"voltage_at_phase_3_v":239,"max_power_consumption_kw":4.796,"device_date_time":"2021-10-12 08:07:00","timestamp":"1111-11-11T11:11:11Z"}
-// |MyElectricity2;00254358;9652.22;1.055;0;0;235;245;239;null;null;null;null;null;null;1111-11-11 11:11.11
-
-// Test: MyElectricity3 amiplus 86064864 NOKEY
-// Comment: amiplus/apator electricity meter with three phase voltages and 2 tariffs.
-// telegram=|804401066448068602027A000070052F2F_066D1E5C11DA21400C78644806868E10036110012500008E20038106531800008E10833C9949000000008E20833C8606000000001B2B5228020B2B3217000BAB3C0000000AFDC9FC0131020AFDC9FC0225020AFDC9FC0331020BABC8FC100000002F2F2F2F2F2F2F2F2F2F2F2F2FDE47|
-// {"_":"telegram","media":"electricity","meter":"amiplus","name":"MyElectricity3","id":"86064864","current_power_consumption_kw":1.732,"current_power_production_kw":0,"voltage_at_phase_1_v":231,"voltage_at_phase_2_v":225,"voltage_at_phase_3_v":231,"device_date_time":"2022-01-26 17:28:30","total_energy_consumption_tariff_1_kwh":25011.061,"total_energy_consumption_tariff_2_kwh":18530.681,"total_energy_production_tariff_1_kwh":4.999,"total_energy_production_tariff_2_kwh":0.686,"max_power_consumption_kw":22.852,"timestamp":"1111-11-11T11:11:11Z"}
-// |MyElectricity3;86064864;null;1.732;null;0;231;225;231;25011.061;18530.681;null;4.999;0.686;null;1111-11-11 11:11.11
-
-// Test: MyElectricity4 amiplus 55090884 NOKEY
-// Comment: amiplus/apator electricity meter with single phase voltage - Otus 1
-// telegram=|7E4401068408095501027A7C1070052F2F_066DDE5E150D39800C78840809550AFDC9FC0139028E30833C0000000000008E20833C0000000000008E10833C4301000000000BABC8FC100000008E10035336420200008E20030000000000008E30030000000000000B2B9502000BAB3C0000002F2F2F2F2F2F2F2F2F2F2F2F2F|
-// {"_":"telegram","media":"electricity","meter":"amiplus","name":"MyElectricity4","id":"55090884","current_power_consumption_kw":0.295,"current_power_production_kw":0,"total_energy_consumption_tariff_1_kwh":2423.653,"total_energy_consumption_tariff_2_kwh":0,"total_energy_consumption_tariff_3_kwh":0,"total_energy_production_tariff_1_kwh":0.143,"total_energy_production_tariff_2_kwh":0,"total_energy_production_tariff_3_kwh":0,"voltage_at_phase_1_v":239,"device_date_time":"2024-09-13 21:30:30","timestamp":"1111-11-11T11:11:11Z"}
-// |MyElectricity4;55090884;null;0.295;null;0;239;null;null;2423.653;0;0;0.143;0;0;1111-11-11 11:11.11
-
-// Test: MyElectricity4 amiplus 00320787 NOKEY
-// telegram=|3e44b6108707320001027a380030052f2f0C7830253390066D6872141239400E031891690000000E833C9265010000000B2B2602000BAB3C0000002F2F2F2F|
-// {"_":"telegram","current_power_consumption_kw": 0.226,"current_power_production_kw": 0,"device_date_time": "2024-09-18 20:50:40","id": "00320787","media": "electricity","meter": "amiplus","name": "MyElectricity4","timestamp": "1111-11-11T11:11:11Z","total_energy_consumption_kwh": 699.118,"total_energy_production_kwh": 16.592}
-// |MyElectricity4;00320787;699.118;0.226;16.592;0;null;null;null;null;null;null;null;null;null;1111-11-11 11:11.11
-
-// Test: MyElectricity5 amiplus 56914504 NOKEY
-// telegram=|9e4401060445915601027a3d0390052f2f066dc076091935800c78044591560e032088300000008e10032088300000008e20030000000000008e30030000000000008e8010030000000000000e833c2702000000008e10833c2702000000008e20833c0000000000008e30833c0000000000008e8010833c0000000000000afdc8fc0136240afdc8fc0262240afdc8fc0389222f2f2f2f2f2f2f2f2f2f2f2f|
-// {"_":"telegram","media":"electricity","meter":"amiplus","name":"MyElectricity5","id":"56914504","total_energy_consumption_kwh":308.82,"total_energy_consumption_tariff_1_kwh":308.82,"total_energy_consumption_tariff_2_kwh":0,"total_energy_consumption_tariff_3_kwh":0,"total_energy_production_kwh":0.227,"total_energy_production_tariff_1_kwh":0.227,"total_energy_production_tariff_2_kwh":0,"total_energy_production_tariff_3_kwh":0,"voltage_at_phase_1_v":243.6,"voltage_at_phase_2_v":246.2,"voltage_at_phase_3_v":228.9,"device_date_time":"2024-05-25 09:54:00","timestamp":"1111-11-11T11:11:11Z"}
-// |MyElectricity5;56914504;308.82;null;0.227;null;243.6;246.2;228.9;308.82;0;0;0.227;0;0;1111-11-11 11:11.11
-
-// Test: MyElectricity6 amiplus 00086426 NOKEY
-// telegram=|8E44B3382664080003027A090080052F2F_066D37090E2232050C78266408000AFDC9FC0142020AFDC9FC0240020AFDC9FC0338028E30833C0000000000008E20833C0000000000008E10833C0000000000000BABC8FC100000008E10030750030000008E20035379060000008E30030000000000000B2B1307000BAB3C0000002F2F2F2F2F2F2F2F2F2F2F2F2F2F2F2F|
-// {"_": "telegram","current_power_consumption_kw": 0.713,"current_power_production_kw": 0,"device_date_time": "2025-02-02 14:09:55","id": "00086426","media": "electricity","meter": "amiplus","name": "MyElectricity6","timestamp": "1111-11-11T11:11:11Z","total_energy_consumption_tariff_1_kwh": 35.007,"total_energy_consumption_tariff_2_kwh": 67.953,"total_energy_consumption_tariff_3_kwh": 0,"total_energy_production_tariff_1_kwh": 0,"total_energy_production_tariff_2_kwh": 0,"total_energy_production_tariff_3_kwh": 0,"voltage_at_phase_1_v": 242,"voltage_at_phase_2_v": 240,"voltage_at_phase_3_v": 238}
-// |MyElectricity6;00086426;null;0.713;null;0;242;240;238;35.007;67.953;0;0;0;0;1111-11-11 11:11.11
diff --git a/components/wmbus_common/driver_apator08.cpp b/components/wmbus_common/driver_apator08.cpp
deleted file mode 100644
index 511203d2..00000000
--- a/components/wmbus_common/driver_apator08.cpp
+++ /dev/null
@@ -1,93 +0,0 @@
-/*
- Copyright (C) 2020-2022 Fredrik Öhrström (gpl-3.0-or-later)
-
- 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 .
-*/
-
-#include"meters_common_implementation.h"
-
-namespace
-{
- struct Driver : public virtual MeterCommonImplementation
- {
- Driver(MeterInfo &mi, DriverInfo &di);
-
- private:
-
- void processContent(Telegram *t);
- };
-
- static bool ok = staticRegisterDriver([](DriverInfo&di)
- {
- di.setName("apator08");
- di.setDefaultFields("name,id,total_m3,timestamp");
- di.setMeterType(MeterType::WaterMeter);
- di.addLinkMode(LinkMode::T1);
- di.addMVT(MANUFACTURER_APT, 0x03, 0x03);
- di.addMVT(MANUFACTURER_APT, 0x0F, 0x0F);
- di.usesProcessContent();
- di.setConstructor([](MeterInfo& mi, DriverInfo& di){ return shared_ptr(new Driver(mi, di)); });
- });
-
- Driver::Driver(MeterInfo &mi, DriverInfo &di) : MeterCommonImplementation(mi, di)
- {
- addNumericField(
- "total",
- Quantity::Volume,
- DEFAULT_PRINT_PROPERTIES,
- "The total water consumption recorded by this meter.");
- }
-
- void Driver::processContent(Telegram *t)
- {
- // The telegram says gas (0x03) but it is a water meter.... so fix this.
- t->dll_type = 0x07;
-
- std::vector content;
- t->extractPayload(&content);
-
- if (content.size() < 4) return;
-
- std::map> vendor_values;
-
- std::string total;
- strprintf(&total, "%02x%02x%02x%02x", content[0], content[1], content[2], content[3]);
-
- vendor_values["0413"] = { 25, DVEntry(25, DifVifKey("0413"), MeasurementType::Instantaneous, 0x13, {}, {}, 0, 0, 0, total) };
- int offset;
- std::string key;
- if(findKey(MeasurementType::Instantaneous, VIFRange::Volume, 0, 0, &key, &vendor_values))
- {
- double total_water_consumption_m3 {};
- extractDVdouble(&vendor_values, key, &offset, &total_water_consumption_m3);
- // Now divide with 3! Is this the same for all apator08 meters? Time will tell.
- total_water_consumption_m3 /= 3.0;
-
- total = "*** 10|"+total+" total consumption (%f m3)";
- t->addSpecialExplanation(offset, 4, KindOfData::CONTENT, Understanding::FULL, total.c_str(), total_water_consumption_m3);
-
- setNumericValue("total", Unit::M3, total_water_consumption_m3);
- }
- }
-}
-
-// Test: Vatten apator08 004444dd NOKEY
-// telegram=|73441486DD4444000303A0B9E527004C4034B31CED0106FF01D093270065F022009661230054D02300EC49240018B424005F012500936D2500FFD525000E3D26001EAC26000B2027000300000000371D0B2000000000000024000000000000280000000000002C0033150C010D2F000000000000|
-// {"_":"telegram","media":"water","meter":"apator08","name":"Vatten","id":"004444dd","total_m3":871.571,"timestamp":"1111-11-11T11:11:11Z"}
-// |Vatten;004444dd;871.571;1111-11-11 11:11.11
-
-// Test: test_apator082 apator08 00149c06 NOKEY
-// telegram=|_1C441486069C14000F0FA042F214000040030000000005FF0472BF1400|
-// {"_":"telegram","media":"water","meter":"apator08","name":"test_apator082","id":"00149c06","total_m3":457.579333,"timestamp":"1111-11-11T11:11:11Z"}
-// |test_apator082;00149c06;457.579333;1111-11-11 11:11.11
diff --git a/components/wmbus_common/driver_apator162.cpp b/components/wmbus_common/driver_apator162.cpp
deleted file mode 100644
index 7b041878..00000000
--- a/components/wmbus_common/driver_apator162.cpp
+++ /dev/null
@@ -1,342 +0,0 @@
-/*
- Copyright (C) 2017-2022 Fredrik Öhrström (gpl-3.0-or-later)
-
- 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 .
-*/
-
-#include"meters_common_implementation.h"
-
-namespace
-{
- struct Driver : public virtual MeterCommonImplementation
- {
- Driver(MeterInfo &mi, DriverInfo &di);
-
- void processContent(Telegram *t);
- void processExtras(std::string miExtras);
- int registerSize(int c);
- };
-
- static bool ok = staticRegisterDriver([](DriverInfo&di)
- {
- di.setName("apator162");
- di.setDefaultFields("name,id,total_m3,timestamp");
- di.setMeterType(MeterType::WaterMeter);
- di.addLinkMode(LinkMode::T1);
- di.addLinkMode(LinkMode::C1);
- di.addMVT(MANUFACTURER_APA, 0x06, 0x05);
- di.addMVT(MANUFACTURER_APA, 0x07, 0x05);
- di.addMVT(0x8614 /*APT?*/, 0x07, 0x05); // Older version of telegram that is not understood!
- di.usesProcessContent();
- di.setConstructor([](MeterInfo& mi, DriverInfo& di){ return shared_ptr(new Driver(mi, di)); });
- });
-
- Driver::Driver(MeterInfo &mi, DriverInfo &di) : MeterCommonImplementation(mi, di)
- {
- processExtras(mi.extras);
-
- addNumericField(
- "total",
- Quantity::Volume,
- DEFAULT_PRINT_PROPERTIES,
- "The total water consumption recorded by this meter.");
- }
-
- void Driver::processContent(Telegram *t)
- {
- // Unfortunately, the at-wmbus-16-2 is mostly a proprietary protocol
- // simply wrapped inside a wmbus telegram.
-
- if (t->tpl_ci == 0xb6)
- {
- // Oups really old style telegram that we cannot decode.
- t->discard = true;
- return;
- }
-
- // Anyway, it seems like telegram is broken up into registers.
- // Each register is identified with a single byte after which the content follows.
- // For example, the total volume is marked by 0x10 followed by 4 bytes.
-
- std::vector content;
- t->extractPayload(&content);
-
- std::map> vendor_values;
-
- // The first 8 bytes are error flags and a date time.
- // E.g. 0F005B5996000000 therefore we skip the first 8 bytes.
- //
- // 0F - Spcial function / packet
- // next 4B : Date - In default frame
- // next 3B : Faults - In default frame example: please see description of 0x01 register
-
- size_t i=8;
- while (i < content.size())
- {
- int c = content[i];
- int size = registerSize(c);
- if (c == 0xff) break; // An FF signals end of telegram padded to encryption boundary,
- // FFFFFFF623A where 4 last are perhaps crc or counter?
- i++;
- if (size == -1 || i+size > content.size())
- {
- std::vector frame;
- t->extractFrame(&frame);
- std::string hex = bin2hex(frame);
-
- if (t->beingAnalyzed() == false)
- {
- if (size == -1)
- {
- warning("(apator162) telegram contains a register (%02x) with unknown size.\n"
- "Please open an issue at https://github.com/wmbusmeters/wmbusmeters/\n"
- "and report this telegram: %s\n", c, hex.c_str());
- }
- else
- {
- warning("(apator162) telegram decoding fails since last register (%02x size %d) does not\n"
- "align with telegram size %zu > %zu.\n"
- "Please open an issue at https://github.com/wmbusmeters/wmbusmeters/\n"
- "and report this telegram: %s\n", c, size, i+size, content.size(), hex.c_str());
- }
- }
- break;
- }
- if (c == 0x10 && size == 4 && i+size < content.size())
- {
- // We found the register representing the total
- std::string total;
- strprintf(&total, "%02x%02x%02x%02x", content[i+0], content[i+1], content[i+2], content[i+3]);
- int offset = i-1+t->header_size;
- vendor_values["0413"] = {offset, DVEntry(offset, DifVifKey("0413"), MeasurementType::Instantaneous, 0x13, {}, {}, 0, 0, 0, total) };
- double total_water_consumption_m3 {};
- extractDVdouble(&vendor_values, "0413", &offset, &total_water_consumption_m3);
- total = "*** 10-"+total+" total consumption (%f m3)";
- t->addSpecialExplanation(offset, 4, KindOfData::CONTENT, Understanding::FULL, total.c_str(), total_water_consumption_m3);
-
- setNumericValue("total", Unit::M3, total_water_consumption_m3);
- }
- else
- {
- std::string msg = "*** ";
- msg += bin2hex(content, i-1, 1)+"-"+bin2hex(content, i, size);
- t->addSpecialExplanation(i-1+t->header_size, size, KindOfData::CONTENT, Understanding::NONE, msg.c_str());
- }
- i += size;
- }
- }
-
- int Driver::registerSize(int c)
- {
- switch (c)
- {
- // case 0x0f: return 3; // Payload often starts with 0x0f,
- // which also means dif = manufacturer data follows.
- // After 0x0F there is always:
- // next 4B : Date - In default frame
- // next 3B : Faults - In default frame
-
- case 0x00: return 4; // Date
- case 0x01: return 3; // Faults - In default frame f.ex. 0F 09 4D A1 97 18 02 00 -> 18 02 00 -> 00 02 18 -> 0x0218
-
- case 0xA1:
- case 0x10: return 4; // Total volume - In default frame
-
- case 0x11: return 2; // Flow
-
- case 0x40: return 6; // Detectors
- case 0x41: return 2; // Voltage
- case 0x42: return 4; // Energy
- case 0x43: return 2; // Life days - In default frame f.ex. 43 6E 0A -> 2670 days from first run
-
- case 0x44: return 3;
-
- case 0x71: return 1+2*4; // ?
- case 0x72: return 1+3*4; // ?
- case 0x73: return 1+4*4; // Historical data
- case 0x74: return 1+5*4; // ? guess
- case 0x75: return 1+6*4; // Historical data
- case 0x76: return 1+7*4; // ? guess
- case 0x77: return 1+8*4; // ? guess
- case 0x78: return 1+9*4; // ? guess
- case 0x79: return 1+10*4; // ? guess
- case 0x7A: return 1+11*4; // ? guess
- case 0x7B: return 1+12*4; // Historical data
-
- case 0x80:
- case 0x81:
- case 0x82:
- case 0x83:
- case 0x84:
- case 0x86:
- case 0x87: return 10; // Events
-
- case 0x85:
- case 0x88:
- case 0x8F: return 11; // Events
-
- case 0x8A: return 9; // Events
-
- case 0x8B:
- case 0x8C: return 6; // Events
-
- case 0x8E: return 7; // Events
-
- case 0xA0: return 4;
-
- case 0xA2: return 1;
-
- case 0xA3: return 7;
-
- case 0xA4: return 4;
-
- case 0xA5:
- case 0xA9:
- case 0xAF: return 1;
-
- case 0xA6: return 3;
-
- case 0xA7:
- case 0xA8:
- case 0xAA:
- case 0xAB:
- case 0xAC:
- case 0xAD: return 2;
-
- case 0xB0: return 5;
- case 0xB1: return 8;
- case 0xB2: return 16;
- case 0xB3: return 8;
- case 0xB4: return 2;
- case 0xB5: return 16;
-
- // Unknown
- case 0xB6: return 3;
- case 0xB7: return 3;
- case 0xB8: return 3;
- case 0xB9: return 3;
- case 0xBA: return 3;
- case 0xBB: return 3;
- case 0xBC: return 3;
- case 0xBD: return 3;
- case 0xBE: return 3;
- case 0xBF: return 3;
-
- case 0xC0: return 3;
- case 0xC1: return 3;
- case 0xC2: return 3;
- case 0xC3: return 3;
- case 0xC4: return 3;
- case 0xC5: return 3;
- case 0xC6: return 3;
- case 0xC7: return 3;
-
- case 0xD0: return 3;
- case 0xD3: return 3;
-
- case 0xF0: return 4;
- }
- return -1;
- }
-
- void Driver::processExtras(std::string miExtras)
- {
- std::map extras;
- bool ok = parseExtras(miExtras, &extras);
- if (!ok)
- {
- error("(apator162) invalid extra parameters (%s)\n", miExtras.c_str());
- }
- }
-
-}
-
-// Test: Wasser apator162 20202020 NOKEY
-// telegram=|6E4401062020202005077A9A006085|2F2F0F0A734393CC0000435B0183001A54E06F630291342510|030F00007B013E0B00003E0B00003E0B00003E0B00003E0B00003E0B00003E0B0000650000003D0000003D0000003D00000000000000A0910CB003FFFFFFFFFFFFFFFFFFFFA62B|
-// {"_":"telegram","media":"water","meter":"apator162","name":"Wasser","id":"20202020","total_m3":3.843,"timestamp":"1111-11-11T11:11:11Z"}
-// |Wasser;20202020;3.843;1111-11-11 11:11.11
-
-// Test: MyTapWatera apator162 21202020 NOKEY
-// telegram=|4E4401062020202105077A13004085|2F2F0F6D4C389300020043840210|351F040075012C0B040048D603003E630300CD2C03001EF402000ACE0200A098A39603FFFFFFFFFFFFFFFFFFFFFFFFFF1977|
-// {"_":"telegram","media":"water","meter":"apator162","name":"MyTapWatera","id":"21202020","total_m3":270.133,"timestamp":"1111-11-11T11:11:11Z"}
-// |MyTapWatera;21202020;270.133;1111-11-11 11:11.11
-
-// Test: MyTapWaterb apator162 22202020 NOKEY
-// telegram=|4E4401062020202205077A4B004085|2F2F0FE566B99390000087C0B24B732679FF75350010|FCFB00004155594265086A0043B4017301DFF600006AE70000BFD5000051BC0000A0F56C2602FFFF1B1B|
-// {"_":"telegram","media":"water","meter":"apator162","name":"MyTapWaterb","id":"22202020","total_m3":64.508,"timestamp":"1111-11-11T11:11:11Z"}
-// |MyTapWaterb;22202020;64.508;1111-11-11 11:11.11
-
-// Test: MyTapWaterc apator162 23202020 NOKEY
-// telegram=|4E4401062020202305077A9D004085|2F2F0F81902C9300000010|B82F010041555942BD2882004319027301BC2601005C180100CB0A0100DFF60000A0F56C2602FFFFFFFFFFFFFFFFFFFFFFFFFF5B7C|
-// {"_":"telegram","media":"water","meter":"apator162","name":"MyTapWaterc","id":"23202020","total_m3":77.752,"timestamp":"1111-11-11T11:11:11Z"}
-// |MyTapWaterc;23202020;77.752;1111-11-11 11:11.11
-
-// Test: MyTapWaterd apator162 24202020 NOKEY
-// telegram=|4E4401062020202405077A6C0040852F2F|0F73B3E19410000084E15381E553810101000010|FA41010041555942BF4E8A00433B027301AD380100BC2601005C180100CB0A0100A0F56C2602FFFFD0D7|
-// {"_":"telegram","media":"water","meter":"apator162","name":"MyTapWaterd","id":"24202020","total_m3":82.426,"timestamp":"1111-11-11T11:11:11Z"}
-// |MyTapWaterd;24202020;82.426;1111-11-11 11:11.11
-
-// Test: MyTapWatere apator162 25202020 NOKEY
-// telegram=|4E4401062020202505077AEF0040852F2F|0F|071122|94|100200|43|6103|84|8B745953486C09100000|10|81920200|75|01F1800200E5640200534A02003431020080150200D9000200|A0|DC939703|FFFFA434|
-// {"_":"telegram","media":"water","meter":"apator162","name":"MyTapWatere","id":"25202020","total_m3":168.577,"timestamp":"1111-11-11T11:11:11Z"}
-// |MyTapWatere;25202020;168.577;1111-11-11 11:11.11
-
-// Test: MyTapWatere apator162 26202020 NOKEY
-// telegram=|6E4401062020202605077AAC0060852F2F|0F|0C4442|94|1A0000|43|B502|83|000A549B4159029C290F|10|AB440000|7B|012C440000BE3E00008838000072340000493000009B2C00001D2C0000822B00007428000010250000B7200000261C0000|A0|A4D9A103|FFFFFFFFFFFFFFFFFFFF7823|
-// {"_":"telegram","media":"water","meter":"apator162","name":"MyTapWatere","id":"26202020","total_m3":17.579,"timestamp":"1111-11-11T11:11:11Z"}
-// |MyTapWatere;26202020;17.579;1111-11-11 11:11.11
-
-// telegram=|6E4401062020202605077AAD0060852F2F|0F|0E4442|94|1A0000|43|B502|84|4265594C655901010000|10|AB440000|7B|012C440000BE3E00008838000072340000493000009B2C00001D2C0000822B00007428000010250000B7200000261C0000|A0|A4D9A103|FFFFFFFFFFFFFFFFFFFF6C1B|
-// {"_":"telegram","media":"water","meter":"apator162","name":"MyTapWatere","id":"26202020","total_m3":17.579,"timestamp":"1111-11-11T11:11:11Z"}
-// |MyTapWatere;26202020;17.579;1111-11-11 11:11.11
-
-// telegram=|6E4401062020202605077AAE0060852F2F|0F|0F4442|94|1A0000|43|B502|81|D87F57D87F5701010000|10|AB440000|7B|012C440000BE3E00008838000072340000493000009B2C00001D2C0000822B00007428000010250000B7200000261C0000|A0|A4D9A103|FFFFFFFFFFFFFFFFFFFF5F22|
-// {"_":"telegram","media":"water","meter":"apator162","name":"MyTapWatere","id":"26202020","total_m3":17.579,"timestamp":"1111-11-11T11:11:11Z"}
-// |MyTapWatere;26202020;17.579;1111-11-11 11:11.11
-
-// Test: MyTapWaterf apator162 03410514 NOKEY
-// telegram=|3E4401061405410305077A190030852F2F|0F|86B4B8|95|290200|40|C6C1|B4|F0F3F3|41|5559|42|FA701000|F0|01010000|10|BC780000|FFFFFFFFFFFFFFFFFFFFFF2483|
-// {"_":"telegram","media":"water","meter":"apator162","name":"MyTapWaterf","id":"03410514","total_m3":30.908,"timestamp":"1111-11-11T11:11:11Z"}
-// |MyTapWaterf;03410514;30.908;1111-11-11 11:11.11
-
-// Test: MyTapWaterg apator162 27202020 NOKEY
-// telegram=|6E4401062020202705077A3D0060852F2F|0F|151794|94|0A0200|43|0403|81|D87F57D87F5701010000|10|783E0000|7B01223C00009137000098320000392D000010290000F02600004C2400003422000004220000CB21000017200000C51C0000|A0|9AD9A103|FFFFFFFFFFFFFFFFFFFF367E|
-// {"_":"telegram","media":"water","meter":"apator162","name":"MyTapWaterg","id":"27202020","total_m3":15.992,"timestamp":"1111-11-11T11:11:11Z"}
-// |MyTapWaterg;27202020;15.992;1111-11-11 11:11.11
-
-
-// Test: NewAndOld apator162 00148686 NOKEY
-// Comment: New apator162 telegram which can be decoded.
-// telegram=4E4401068686140005077A350040852F2F_0F005B599600000010AA55000041545A42850BD800437D037301C5500000564B00009E4600006A410000A01778EC03FFFFFFFFFFFFFFFFFFFFFFFFFFE393
-// {"_":"telegram","media":"water","meter":"apator162","name":"NewAndOld","id":"00148686","total_m3":21.93,"timestamp":"1111-11-11T11:11:11Z"}
-// |NewAndOld;00148686;21.93;1111-11-11 11:11.11
-
-// Comment: Old style apator162 telegram which cannot be decoded.
-// telegram=5A441486868614000507B6_0AFFFFF5450106F41BAD717A35004085C90AC6D97E3294827563E70F4CF00655FC796A76B87AD1D4A69D16F5EDD1084318F46559E43D2C60D2B1CE581D0CAC1BBC73A376B9D71F0D71C6C904B04DC30E
-// This telegram should not trigger any shell or other output!
-
-// telegram=4E4401068686140005077A350040852F2F_0F005B599600000010AA66000041545A42850BD800437D037301C5500000564B00009E4600006A410000A01778EC03FFFFFFFFFFFFFFFFFFFFFFFFFFE393
-// {"_":"telegram","media":"water","meter":"apator162","name":"NewAndOld","id":"00148686","total_m3":26.282,"timestamp":"1111-11-11T11:11:11Z"}
-// |NewAndOld;00148686;26.282;1111-11-11 11:11.11
-
-
-// Test: has80 apator162 04040404 NOKEY
-// telegram=|704401060404040405077A0E0060852F2F_0F766DFB96010000430600808F67DB8F67DB01010000102F1F01007B01000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000A05F5C1804FFFFFFFFFFFFFFFFFFFF26BCD649|
-// {"_":"telegram","media":"water","meter":"apator162","name":"has80","id":"04040404","total_m3":73.519,"timestamp":"1111-11-11T11:11:11Z"}
-// |has80;04040404;73.519;1111-11-11 11:11.11
-
-// Test: has79 apator162 04960212 NOKEY
-// telegram=|5E4401061202960405077A790050852F2F0F78A599983B0200435000805771165771160103000010653A00007919321E0000620000006200000000000000000000000000000000000000000000000000000000000000A0422C6004FFFF2FBF|
-// {"_": "telegram","id": "04960212","media": "water","meter": "apator162","name": "has79","timestamp": "1111-11-11T11:11:11Z","total_m3": 14.949}
-// |has79;04960212;14.949;1111-11-11 11:11.11
diff --git a/components/wmbus_common/driver_apator172.cpp b/components/wmbus_common/driver_apator172.cpp
deleted file mode 100644
index b7139c40..00000000
--- a/components/wmbus_common/driver_apator172.cpp
+++ /dev/null
@@ -1,88 +0,0 @@
-/*
- Copyright (C) 2022 Fredrik Öhrström (gpl-3.0-or-later)
-
- 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 .
-*/
-
-#include"meters_common_implementation.h"
-
-#include
-
-namespace
-{
- struct Driver : public virtual MeterCommonImplementation
- {
- Driver(MeterInfo &mi, DriverInfo &di);
-
- void processContent(Telegram *t);
- };
-
- static bool ok = staticRegisterDriver([](DriverInfo&di)
- {
- di.setName("apator172");
- di.setDefaultFields("name,id,total_m3,timestamp");
- di.setMeterType(MeterType::WaterMeter);
- di.addMVT(0x8614 /*APT?*/, 0x11, 0x04);
- di.usesProcessContent();
- di.setConstructor([](MeterInfo& mi, DriverInfo& di){ return shared_ptr(new Driver(mi, di)); });
- });
-
- Driver::Driver(MeterInfo &mi, DriverInfo &di) : MeterCommonImplementation(mi, di)
- {
- addNumericField(
- "total",
- Quantity::Volume,
- DEFAULT_PRINT_PROPERTIES,
- "The total water consumption recorded by this meter.");
- }
-
- void Driver::processContent(Telegram *t)
- {
- std::vector content;
- t->extractPayload(&content);
-
- // Overwrite the non-standard 0x11 with 0x07 which means water.
- t->dll_type = 0x07;
-
- std::map> vendor_values;
-
- size_t i=0;
- if (i+4 < content.size())
- {
- // We found the register representing the total
- std::string total;
- strprintf(&total, "%02x%02x%02x%02x", content[i+0], content[i+1], content[i+2], content[i+3]);
- int offset = i-1+t->header_size;
- vendor_values["0413"] = {offset, DVEntry(offset, DifVifKey("0413"), MeasurementType::Instantaneous, 0x13, {}, {}, 0, 0, 0, total) };
- double tmp = 0;
- extractDVdouble(&vendor_values, "0413", &offset, &tmp);
- // Single tick seems to be 1/3 of a m3. Divide by 3 and keep a single decimal.
- // This will report consumption as 100.0 100.3 100.7 101.0 etc.
- double total_water_consumption_m3 = 0.1*std::round(10000.0*tmp/3.0);
- total = "*** "+total+" total consumption (%f m3)";
- t->addSpecialExplanation(offset, 4, KindOfData::CONTENT, Understanding::FULL, total.c_str(), total_water_consumption_m3);
-
- setNumericValue("total", Unit::M3, total_water_consumption_m3);
- }
- }
-}
-
-// Test: Vattur apator172 0014a807 NOKEY
-// telegram=|1C44148607A814000411A0_1D5400000840030000000005FF05D83D0000|
-// {"_":"telegram","media":"water","meter":"apator172","name":"Vattur","id":"0014a807","total_m3":7177.7,"timestamp":"1111-11-11T11:11:11Z"}
-// |Vattur;0014a807;7177.7;1111-11-11 11:11.11
-
-// telegram=|1C44148607A814000411A0_215400000840030000000005FF05D83D0000|
-// {"_":"telegram","media":"water","meter":"apator172","name":"Vattur","id":"0014a807","total_m3":7179,"timestamp":"1111-11-11T11:11:11Z"}
-// |Vattur;0014a807;7179;1111-11-11 11:11.11
diff --git a/components/wmbus_common/driver_apatoreitn.cpp b/components/wmbus_common/driver_apatoreitn.cpp
deleted file mode 100644
index 485bef90..00000000
--- a/components/wmbus_common/driver_apatoreitn.cpp
+++ /dev/null
@@ -1,194 +0,0 @@
-/*
- Copyright (C) 2022-2023 Fredrik Öhrström (gpl-3.0-or-later)
- Copyright (C) 2022 Kajetan Krykwiński (gpl-3.0-or-later)
-
- 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 .
-*/
-
-#include"meters_common_implementation.h"
-
-namespace
-{
- struct Driver : public virtual MeterCommonImplementation
- {
- Driver(MeterInfo &mi, DriverInfo &di);
-
- void processContent(Telegram *t);
- std::string dateToString(uchar date_lo, uchar date_hi);
- };
-
- static bool ok = staticRegisterDriver([](DriverInfo&di)
- {
- // Note: this supports only E.ITN 30.51 at the moment.
- // E.ITN 30.60 should be similar, as it is covered via the same datasheet
- // http://www.apator.com/uploads/files/Produkty/Podzielnik_kosztow_ogrzewania/i-pl-021-2016-e-itn-30-51-30-6.pdf
- di.setName("apatoreitn");
- di.setDefaultFields("name,id,current_hca,previous_hca,current_date,season_start_date,esb_date,temp_room_avg_c,temp_room_prev_avg_c,timestamp");
- di.setMeterType(MeterType::HeatCostAllocationMeter);
- di.addMVT(MANUFACTURER_APA, 0x08, 0x04);
- di.addMVT(MANUFACTURER_APT, 0x08, 0x04);
- di.usesProcessContent();
- di.setConstructor([](MeterInfo& mi, DriverInfo& di){ return shared_ptr(new Driver(mi, di)); });
- });
-
- Driver::Driver(MeterInfo &mi, DriverInfo &di) : MeterCommonImplementation(mi, di)
- {
- addNumericField("current",
- Quantity::HCA,
- DEFAULT_PRINT_PROPERTIES,
- "Energy consumption so far in this billing period.");
-
- addNumericField("previous",
- Quantity::HCA,
- DEFAULT_PRINT_PROPERTIES,
- "Energy consumption in previous billing period.");
-
- addStringField("current_date",
- "Current date, as reported by meter.",
- DEFAULT_PRINT_PROPERTIES);
-
- addStringField("season_start_date",
- "Season start date.",
- DEFAULT_PRINT_PROPERTIES);
-
- addStringField("esb_date",
- "Electronic seal protection break date.",
- DEFAULT_PRINT_PROPERTIES);
-
- addNumericField("temp_room_avg",
- Quantity::Temperature,
- DEFAULT_PRINT_PROPERTIES,
- "Average room temperature in current season.");
-
- addNumericField("temp_room_prev_avg",
- Quantity::Temperature,
- DEFAULT_PRINT_PROPERTIES,
- "Average room temperature in previous season.");
- }
-
- void Driver::processContent(Telegram *t)
- {
- std::vector content;
- t->extractPayload(&content);
-
- if (t->tpl_ci == 0xB6) {
- // tpl-ci-field B6, there's a header to skip
- // first byte contains following header length
- uint header_len = content[0] + 1;
-
- //drop header data from content
- content.erase(content.begin(), content.begin() + header_len);
- }
-
- if (t->tpl_ci == 0xA0) {
- // In my opinion this is already part of the telegram data
- // so lets add it back to content.
- // Telegram either starts with B0A0A1...
- // or directly with A0A1...
- content.insert(content.begin(),t->tpl_ci);
- }
-
- if (content.size() != 16)
- {
- // Payload most likely is broken
- debugPayload("(apatoreitn) content size wrong!", content);
- return;
- }
-
- // Season start date + install date + some flag?
- //
- // Note: may be wrong, requires confirmation as all meters I see in range
- // report start date 1.05, installed in 2016 and field is A0A1h
- // Note: NOT byte swapped. Accidentally? works via dateToString conversion.
- uchar season_start_date_lo = content[1];
- uchar season_start_date_hi = content[0];
- std::string season_start_date = dateToString(season_start_date_lo, season_start_date_hi);
- setStringValue("season_start_date", season_start_date, NULL);
-
- // Previous season total allocation
- uchar prev_lo = content[4];
- uchar prev_hi = content[5];
- double previous_hca = (256.0*prev_hi+prev_lo);
- setNumericValue("previous", Unit::HCA, previous_hca);
-
- // Electronic seal break date
- uchar esb_date_lo = content[6];
- uchar esb_date_hi = content[7];
- std::string esb_date = dateToString(esb_date_lo, esb_date_hi);
- setStringValue("esb_date", esb_date, NULL);
-
- // Current season allocation
- uchar curr_lo = content[8];
- uchar curr_hi = content[9];
- double current_hca = (256.0*curr_hi+curr_lo);
- setNumericValue("current", Unit::HCA, current_hca);
-
- // Current date reported by meter
- uchar date_curr_lo = content[10];
- uchar date_curr_hi = content[11];
- std::string current_date = dateToString(date_curr_lo, date_curr_hi);
- setStringValue("current_date", current_date, NULL);
-
- // Previous season average temperature
- double temp_room_prev_avg_frac = content[12];
- double temp_room_prev_avg_deg = content[13];
- double temp_room_prev_avg = temp_room_prev_avg_deg + temp_room_prev_avg_frac/256.0;
- setNumericValue("temp_room_prev_avg", Unit::C, temp_room_prev_avg);
-
- // Current season average temperature
- double temp_room_avg_frac = content[14];
- double temp_room_avg_deg = content[15];
- double temp_room_avg = temp_room_avg_deg + temp_room_avg_frac/256.0;
- setNumericValue("temp_room_avg", Unit::C, temp_room_avg);
- }
-
- std::string Driver::dateToString(uchar date_lo, uchar date_hi) {
- // Data format (MSB -> LSB):
- // 2 bits of unknown data (or part of a year, but left over for season date
- // hack, and it doesn't matter until 2064 anyway...)
- // 5 bits of year
- // 4 bits of month
- // 5 bits of day
- uint date_curr = (256*date_hi + date_lo);
- if(date_curr == 0)
- {
- // date is null, what should we do?
- return "";
- }
-
- uint day = date_curr & 0x1F;
- uint month = (date_curr >> 5) & 0x0F;
- uint year = ((date_curr >> 9) & 0x1F) + 2000;
-
- char buf[30];
- // is there a better way to construct a date from this?
- std::snprintf(buf, sizeof(buf), "%d-%02d-%02dT02:00:00Z", year, month, day);
- return buf;
- }
-}
-
-// Test: HCA1 apatoreitn 37373731 NOKEY
-// telegram=|19440186313737370408A0A1000059001C270100322DE413B415|
-// {"_":"telegram","media":"heat cost allocation","meter":"apatoreitn","name":"HCA1","id":"37373731","current_hca":1,"previous_hca":89,"current_date":"2022-09-18T02:00:00Z","season_start_date":"2016-05-01T02:00:00Z","esb_date":"2019-08-28T02:00:00Z","temp_room_avg_c":21.703125,"temp_room_prev_avg_c":19.890625,"timestamp":"1111-11-11T11:11:11Z"}
-// |HCA1;37373731;1;89;2022-09-18T02:00:00Z;2016-05-01T02:00:00Z;2019-08-28T02:00:00Z;21.703125;19.890625;1111-11-11 11:11.11
-
-// Test: HCA2 apatoreitn 37373732 NOKEY
-// telegram=|25441486323737370408B60AFFFFF5450186F41B9D58A0A100007809000000001F2D6416C819|
-// {"_":"telegram","media":"heat cost allocation","meter":"apatoreitn","name":"HCA2","id":"37373732","current_hca":0,"previous_hca":2424,"current_date":"2022-08-31T02:00:00Z","season_start_date":"2016-05-01T02:00:00Z","esb_date":"","temp_room_avg_c":25.78125,"temp_room_prev_avg_c":22.390625,"timestamp":"1111-11-11T11:11:11Z"}
-// |HCA2;37373732;0;2424;2022-08-31T02:00:00Z;2016-05-01T02:00:00Z;;25.78125;22.390625;1111-11-11 11:11.11
-
-// Test: HCA3 apatoreitn 37373733 NOKEY
-// telegram=|29441486333737370408B60EFFFFF1460186EC1B934EE91BA57BA0A1000059009C250100322DE413B415|
-// {"_":"telegram","media":"heat cost allocation","meter":"apatoreitn","name":"HCA3","id":"37373733","current_hca":1,"previous_hca":89,"current_date":"2022-09-18T02:00:00Z","season_start_date":"2016-05-01T02:00:00Z","esb_date":"2018-12-28T02:00:00Z","temp_room_avg_c":21.703125,"temp_room_prev_avg_c":19.890625,"timestamp":"1111-11-11T11:11:11Z"}
-// |HCA3;37373733;1;89;2022-09-18T02:00:00Z;2016-05-01T02:00:00Z;2018-12-28T02:00:00Z;21.703125;19.890625;1111-11-11 11:11.11
diff --git a/components/wmbus_common/driver_apatorna1.cpp b/components/wmbus_common/driver_apatorna1.cpp
deleted file mode 100644
index 3e105be7..00000000
--- a/components/wmbus_common/driver_apatorna1.cpp
+++ /dev/null
@@ -1,88 +0,0 @@
-/*
- Copyright (C) 2017-2022 Fredrik Öhrström (gpl-3.0-or-later)
-
- 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 .
-*/
-
-#include"meters_common_implementation.h"
-#include "wmbus_utils.h"
-#include
-
-namespace
-{
- struct Driver : public virtual MeterCommonImplementation
- {
- Driver(MeterInfo &mi, DriverInfo &di);
-
- void processContent(Telegram *t);
- };
-
- static bool ok = staticRegisterDriver([](DriverInfo&di)
- {
- di.setName("apatorna1");
- di.setDefaultFields("name,id,total_m3,timestamp");
- di.setMeterType(MeterType::WaterMeter);
- di.addMVT(MANUFACTURER_APA, 0x07, 0x14);
- di.usesProcessContent();
- di.setConstructor([](MeterInfo& mi, DriverInfo& di){ return shared_ptr(new Driver(mi, di)); });
- });
-
- Driver::Driver(MeterInfo &mi, DriverInfo &di) : MeterCommonImplementation(mi, di)
- {
- addNumericField(
- "total",
- Quantity::Volume,
- DEFAULT_PRINT_PROPERTIES,
- "The total water consumption recorded by this meter.");
- }
-
- void Driver::processContent(Telegram *t)
- {
- std::vector content;
- t->extractPayload(&content);
-
- if (content.size() < 4) return;
-
- std::vector frame(content.begin() + 2, content.begin() + 18);
- std::vector::iterator pos = frame.begin();
-
- // TODO: read specified key from input
- std::vector aes_key(16, 0);
-
- int num_encrypted_bytes = 0;
- int num_not_encrypted_at_end = 0;
-
- t->tpl_acc = content[0]; //0xBB;
-
- decrypt_TPL_AES_CBC_IV(t, frame, pos, aes_key, &num_encrypted_bytes, &num_not_encrypted_at_end);
-
- const int multiplier = pow(10, (frame.at(1) & 0b00110000) >> 4);
-
- const int reading = static_cast(frame.at(4)) << 20 |
- static_cast(frame.at(3)) << 12 |
- static_cast(frame.at(2)) << 4 |
- (static_cast(frame.at(1)) & 0b00001111);
-
- const double volume = static_cast(reading) * multiplier / 1000;
-
- debug("Volume: %.3f \n", volume);
-
- setNumericValue("total", Unit::M3, volume);
- }
-}
-
-// Test: ApNa1 apatorna1 04913581 00000000000000000000000000000000
-// telegram=|1C440106813591041407A0B000266A705474DDB80D9A0EB9AE2EF29D96|
-// {"_":"telegram","media":"water","meter":"apatorna1","name":"ApNa1","id":"04913581","total_m3":345.312,"timestamp":"1111-11-11T11:11:11Z"}
-// |ApNa1;04913581;345.312;1111-11-11 11:11.11
diff --git a/components/wmbus_common/driver_auto.cpp b/components/wmbus_common/driver_auto.cpp
deleted file mode 100644
index 235a2c79..00000000
--- a/components/wmbus_common/driver_auto.cpp
+++ /dev/null
@@ -1,42 +0,0 @@
-/*
- Copyright (C) 2021-2022 Fredrik Öhrström (gpl-3.0-or-later)
-
- 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 .
-*/
-
-#include"meters_common_implementation.h"
-
-struct MeterAuto : public virtual MeterCommonImplementation
-{
- MeterAuto(MeterInfo &mi, DriverInfo &di);
-};
-
-bool ok = staticRegisterDriver([](DriverInfo&di)
-{
- di.setName("auto");
- di.setDefaultFields("name,id,timestamp");
- di.setMeterType(MeterType::AutoMeter);
- di.setConstructor([](MeterInfo& mi, DriverInfo& di){ return shared_ptr(new MeterAuto(mi, di)); });
-});
-
-MeterAuto::MeterAuto(MeterInfo &mi, DriverInfo &di) : MeterCommonImplementation(mi, di)
-{
-}
-
-shared_ptr createAuto(MeterInfo &mi)
-{
- DriverInfo di;
- di.setName("auto");
- return shared_ptr(new MeterAuto(mi, di));
-}
diff --git a/components/wmbus_common/driver_aventieshca.cpp b/components/wmbus_common/driver_aventieshca.cpp
deleted file mode 100644
index 9d455f4f..00000000
--- a/components/wmbus_common/driver_aventieshca.cpp
+++ /dev/null
@@ -1,133 +0,0 @@
-/*
- Copyright (C) 2019-2022 Fredrik Öhrström (gpl-3.0-or-later)
-
- 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 .
-*/
-
-#include"meters_common_implementation.h"
-
-namespace
-{
- struct Driver : public virtual MeterCommonImplementation
- {
- Driver(MeterInfo &mi, DriverInfo &di);
- };
-
- static bool ok = staticRegisterDriver([](DriverInfo&di)
- {
- di.setName("aventieshca");
- di.setDefaultFields("name,id,current_consumption_hca,error_flags,timestamp");
- di.setMeterType(MeterType::HeatCostAllocationMeter);
- di.addLinkMode(LinkMode::T1);
- di.addMVT(MANUFACTURER_AAA, 0x08, 0x55);
- di.setConstructor([](MeterInfo& mi, DriverInfo& di){ return shared_ptr(new Driver(mi, di)); });
- });
-
- Driver::Driver(MeterInfo &mi, DriverInfo &di) : MeterCommonImplementation(mi, di)
- {
- addStringFieldWithExtractorAndLookup(
- "status",
- "Meter status from error flags and tpl status field.",
- PrintProperty::STATUS | PrintProperty::INCLUDE_TPL_STATUS,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::ErrorFlags),
- Translate::Lookup(
- {
- {
- {
- "ERROR_FLAGS",
- Translate::MapType::BitToString,
- AlwaysTrigger, MaskBits(0xffff),
- "OK",
- {
- { 0x01, "MEASUREMENT", TestBit::Set },
- { 0x02, "SABOTAGE" },
- { 0x04, "BATTERY" },
- { 0x08, "CS" },
- { 0x10, "HF" },
- { 0x20, "RESET" }
- }
- },
- },
- }));
-
- addStringFieldWithExtractorAndLookup(
- "error_flags",
- "Deprecated.",
- DEFAULT_PRINT_PROPERTIES | PrintProperty::DEPRECATED,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::ErrorFlags),
- Translate::Lookup(
- {
- {
- {
- "ERROR_FLAGS",
- Translate::MapType::BitToString,
- AlwaysTrigger, MaskBits(0xffff),
- "",
- {
- { 0x01, "MEASUREMENT", TestBit::Set },
- { 0x02, "SABOTAGE" },
- { 0x04, "BATTERY" },
- { 0x08, "CS" },
- { 0x10, "HF" },
- { 0x20, "RESET" }
- }
- },
- },
- }));
-
- addNumericFieldWithExtractor(
- "current_consumption",
- "The current heat cost allocation.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::HCA,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::HeatCostAllocation)
- );
-
- addNumericFieldWithExtractor(
- "consumption_at_set_date",
- "Heat cost allocation at the most recent billing period date.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::HCA,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::HeatCostAllocation)
- .set(StorageNr(1))
- );
-
- addNumericFieldWithExtractor(
- "consumption_at_set_date_{storage_counter}",
- "The heat cost allocation at set date #.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::HCA,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::HeatCostAllocation)
- .set(StorageNr(2),StorageNr(17))
- );
- }
-}
-
-// Test: HCA aventieshca 60900126 NOKEY
-// telegram=|76442104260190605508722601906021045508060060052F2F#0B6E660100426EA60082016EA600C2016E9E0082026E7E00C2026E5B0082036E4200C2036E770182046E5B01C2046E4C0182056E4701C2056E3E0182066E3B01C2066E3B0182076E3B01C2076E3B0182086E1301C2086E9C0002FD170000|
-// {"_":"telegram","media":"heat cost allocation","meter":"aventieshca","name":"HCA","id":"60900126","status":"OK","current_consumption_hca":166,"consumption_at_set_date_hca":166,"consumption_at_set_date_2_hca":166,"consumption_at_set_date_3_hca":158,"consumption_at_set_date_4_hca":126,"consumption_at_set_date_5_hca":91,"consumption_at_set_date_6_hca":66,"consumption_at_set_date_7_hca":375,"consumption_at_set_date_8_hca":347,"consumption_at_set_date_9_hca":332,"consumption_at_set_date_10_hca":327,"consumption_at_set_date_11_hca":318,"consumption_at_set_date_12_hca":315,"consumption_at_set_date_13_hca":315,"consumption_at_set_date_14_hca":315,"consumption_at_set_date_15_hca":315,"consumption_at_set_date_16_hca":275,"consumption_at_set_date_17_hca":156,"error_flags":"","timestamp":"1111-11-11T11:11:11Z"}
-// |HCA;60900126;166;;1111-11-11 11:11.11
diff --git a/components/wmbus_common/driver_aventieswm.cpp b/components/wmbus_common/driver_aventieswm.cpp
deleted file mode 100644
index 67460249..00000000
--- a/components/wmbus_common/driver_aventieswm.cpp
+++ /dev/null
@@ -1,129 +0,0 @@
-/*
- Copyright (C) 2020-2022 Fredrik Öhrström (gpl-3.0-or-later)
-
- 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 .
-*/
-
-#include"meters_common_implementation.h"
-
-namespace
-{
- struct Driver : public virtual MeterCommonImplementation
- {
- Driver(MeterInfo &mi, DriverInfo &di);
- };
-
- static bool ok = staticRegisterDriver([](DriverInfo&di)
- {
- di.setName("aventieswm");
- di.setDefaultFields("name,id,total_m3,error_flags,timestamp");
- di.setMeterType(MeterType::WaterMeter);
- di.addLinkMode(LinkMode::T1);
- di.addMVT(MANUFACTURER_AAA, 0x07, 0x25);
- di.setConstructor([](MeterInfo& mi, DriverInfo& di){ return shared_ptr(new Driver(mi, di)); });
- });
-
- Driver::Driver(MeterInfo &mi, DriverInfo &di) :
- MeterCommonImplementation(mi, di)
- {
- addStringFieldWithExtractorAndLookup(
- "status",
- "Meter status from error flags and tpl status field.",
- PrintProperty::STATUS | PrintProperty::INCLUDE_TPL_STATUS,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::ErrorFlags),
- Translate::Lookup(
- {
- {
- {
- "ERROR_FLAGS",
- Translate::MapType::BitToString,
- AlwaysTrigger, MaskBits(0xffff),
- "OK",
- {
- { 0x01, "MEASUREMENT", TestBit::Set },
- { 0x02, "SABOTAGE" },
- { 0x04, "BATTERY" },
- { 0x08, "CS" },
- { 0x10, "HF" },
- { 0x20, "RESET" }
- }
- },
- },
- }));
-
- addNumericFieldWithExtractor(
- "total",
- "The total water consumption recorded by this meter.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Volume,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::Volume)
- );
-
- addNumericFieldWithExtractor(
- "consumption_at_set_date_{storage_counter}",
- "Water consumption at the # billing period date.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Volume,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::Volume)
- .set(StorageNr(1),StorageNr(14))
- );
-
-
- addStringFieldWithExtractorAndLookup(
- "error_flags",
- "Deprecated.",
- DEFAULT_PRINT_PROPERTIES | PrintProperty::DEPRECATED,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::ErrorFlags),
- Translate::Lookup(
- {
- {
- {
- "ERROR_FLAGS",
- Translate::MapType::BitToString,
- AlwaysTrigger, MaskBits(0xffff),
- "",
- {
- { 0x01, "MEASUREMENT", TestBit::Set },
- { 0x02, "SABOTAGE" },
- { 0x04, "BATTERY" },
- { 0x08, "CS" },
- { 0x10, "HF" },
- { 0x20, "RESET" }
- }
- },
- },
- }));
- }
-}
-
-// Test: Votten aventieswm 61070071 A004EB23329A477F1DD2D7820B56EB3D
-// telegram=76442104710007612507727100076121042507B5006005E2E95A3C2A1279A5415E6732679B43369FD5FDDDD783EEEBB48236D34E7C94AF0A18A5FDA5F7D64111EB42D4D891622139F2952F9D12A20088DFA4CF8123871123EE1F6C1DCEA414879DDB4E05E508F1826D7EFBA6964DF804C9261EA23BBF03
-// {"_":"telegram","media":"water","meter":"aventieswm","name":"Votten","id":"61070071","total_m3":466.472,"consumption_at_set_date_1_m3":465.96,"consumption_at_set_date_2_m3":458.88,"consumption_at_set_date_3_m3":449.65,"consumption_at_set_date_4_m3":442.35,"consumption_at_set_date_5_m3":431.07,"consumption_at_set_date_6_m3":423.98,"consumption_at_set_date_7_m3":415.23,"consumption_at_set_date_8_m3":409.03,"consumption_at_set_date_9_m3":400.79,"consumption_at_set_date_10_m3":393.2,"consumption_at_set_date_11_m3":388.63,"consumption_at_set_date_12_m3":379.26,"consumption_at_set_date_13_m3":371.26,"consumption_at_set_date_14_m3":357.84,"status":"OK","error_flags":"","timestamp":"1111-11-11T11:11:11Z"}
-// |Votten;61070071;466.472;;1111-11-11 11:11.11
-
-
-// Test: Vatten aventieswm 61070072 NOKEY
-// telegram=76442104720007612507727200076121042507B50060052F2F0413281E0700431404B60083011440B300C30114A5AF00830214CBAC00C3021463A8008303149EA500C3031433A200830414C79F00C304148F9C00830514989900C30514CF9700830614269400C30614069100830714C88B0002FD171111
-// {"_":"telegram","media":"water","meter":"aventieswm","name":"Vatten","id":"61070072","total_m3":466.472,"consumption_at_set_date_1_m3":465.96,"consumption_at_set_date_2_m3":458.88,"consumption_at_set_date_3_m3":449.65,"consumption_at_set_date_4_m3":442.35,"consumption_at_set_date_5_m3":431.07,"consumption_at_set_date_6_m3":423.98,"consumption_at_set_date_7_m3":415.23,"consumption_at_set_date_8_m3":409.03,"consumption_at_set_date_9_m3":400.79,"consumption_at_set_date_10_m3":393.2,"consumption_at_set_date_11_m3":388.63,"consumption_at_set_date_12_m3":379.26,"consumption_at_set_date_13_m3":371.26,"consumption_at_set_date_14_m3":357.84,"status":"ERROR_FLAGS_1100 HF MEASUREMENT","error_flags":"ERROR_FLAGS_1100 HF MEASUREMENT","timestamp":"1111-11-11T11:11:11Z"}
-// |Vatten;61070072;466.472;ERROR_FLAGS_1100 HF MEASUREMENT;1111-11-11 11:11.11
diff --git a/components/wmbus_common/driver_bfw240radio.cpp b/components/wmbus_common/driver_bfw240radio.cpp
deleted file mode 100644
index 7fb7ef16..00000000
--- a/components/wmbus_common/driver_bfw240radio.cpp
+++ /dev/null
@@ -1,170 +0,0 @@
-/*
- Copyright (C) 2021-2022 Fredrik Öhrström (gpl-3.0-or-later)
-
- 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 .
-*/
-
-#include"meters_common_implementation.h"
-
-namespace
-{
- struct Driver : public virtual MeterCommonImplementation
- {
- Driver(MeterInfo &mi, DriverInfo &di);
-
- void processContent(Telegram *t);
- };
-
- static bool ok = staticRegisterDriver([](DriverInfo&di)
- {
- di.setName("bfw240radio");
- di.setDefaultFields("name,id,current_hca,prev_hca,timestamp");
- di.addLinkMode(LinkMode::T1);
- di.setMeterType(MeterType::HeatCostAllocationMeter);
- di.addMVT(MANUFACTURER_BFW,0x08, 0x02);
- di.forceMfctIndex(2); // First two bytes are 2f2f after that its completely mfct specific.
- di.usesProcessContent();
- di.setConstructor([](MeterInfo& mi, DriverInfo& di){ return shared_ptr(new Driver(mi, di)); });
- });
-
- Driver::Driver(MeterInfo &mi, DriverInfo &di) : MeterCommonImplementation(mi, di)
- {
- addNumericField("current",
- Quantity::HCA,
- DEFAULT_PRINT_PROPERTIES,
- "Energy consumption so far in this billing period.");
-
- addNumericField("prev",
- Quantity::HCA,
- DEFAULT_PRINT_PROPERTIES,
- "Energy consumption at end of previous billing period.");
-
- for (int i=0; i<18; ++i)
- {
- std::string info = tostrprintf("prev_%02d", i+1);
- std::string about = tostrprintf("Energy consumption %d months ago.", i+1);
-
- addNumericField(info,
- Quantity::HCA,
- DEFAULT_PRINT_PROPERTIES,
- about);
- }
-
- addStringField("device_date",
- "Device date when telegram was sent.",
- DEFAULT_PRINT_PROPERTIES);
- }
-
-
- int getHistoric(int n, std::vector &content)
- {
- assert(n >= 0 && n < 18);
- assert(content.size() >= 40);
-
- int offset = (n*12)/8;
- int remainder = (n*12)%8;
-
- uchar lo, hi;
-
- if (remainder == 0)
- {
- lo = content[36-offset];
- hi = 0x0f & content[36-1-offset];
- }
- else
- {
- assert(remainder == 4);
- lo = content[36-1-offset];
- hi = (0xf0 & content[36-offset]) >> 4;
- }
- return hi*256 + lo;
- }
-
- /*
- date of telegram reception--------------------------------------------------------------------------------|
- |
- 18 historic monthly values (newest to the right, byte-reordering for 2.,4.,6.,...-oldest month)----| |
- | |
- ???------------------------| | |
- | | |
- current consumption---| | | |
- | | | |
- prev. cons.---vvvv vvvv vvvv vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv vvvvvv
- feb: 2F2F6F1F 0144 0100 1470 000 000 000 000 000 000 000 000 000 000 000 000 000 000 370 09B 441 0AC 260221
- mar: 2F2F481F 0144 0100 1470 000 000 000 000 000 000 000 000 000 000 000 000 000 037 9B0 144 AC0 100 040321
- apr: 2F2F871F 0144 013C 1470 000 000 000 000 000 000 000 000 000 000 000 000 370 09B 441 0AC 001 13C 030421
- */
- void Driver::processContent(Telegram *t)
- {
- std::vector content;
- t->extractPayload(&content);
-
- if (content.size() < 40) return;
-
- double current_hca = content[6]*256 + content[7];
- setNumericValue("current", Unit::HCA, current_hca);
-
- std::string msg = tostrprintf("*** %02X%02X \"current_hca\":%g", content[6], content[7], current_hca);
- t->addSpecialExplanation(6+t->header_size, 2, KindOfData::CONTENT, Understanding::FULL, msg.c_str());
-
- double prev_hca = content[4]*256 + content[5];
- setNumericValue("prev", Unit::HCA, prev_hca);
-
- msg = tostrprintf("*** %02X%02X \"prev_hca\":%g", content[4], content[5], prev_hca);
- t->addSpecialExplanation(4+t->header_size, 2, KindOfData::CONTENT, Understanding::FULL, msg.c_str());
-
- std::string device_date = tostrprintf("20%02x-%02x-%02x", content[39], content[39-1], content[39-2]);
- setStringValue("device_date", device_date, NULL);
-
- msg = tostrprintf("*** %02X%02X%02X \"device_date\":\"%s\"", content[39-2], content[39-1], content[39],
- device_date.c_str());
- t->addSpecialExplanation(39-2+t->header_size, 3, KindOfData::CONTENT, Understanding::FULL, msg.c_str());
-
-
- for (int i=0; i<18; ++i)
- {
- std::string info = tostrprintf("prev_%02d", i+1);
- double historic_hca = getHistoric(i, content);
- setNumericValue(info, Unit::HCA, historic_hca);
- }
- }
-}
-
-// Test: bfw bfw240radio 00707788 NOKEY
-// telegram=|3644D7088877700002087ADBC000002F2F9E1F03C10388152A00000000000000000000000000000204000404000EE2020AC1321D280221|
-// {"_":"telegram","media":"heat cost allocation","meter":"bfw240radio","name":"bfw","id":"00707788","current_hca":904,"prev_hca":961,"prev_01_hca":541,"prev_02_hca":961,"prev_03_hca":522,"prev_04_hca":226,"prev_05_hca":14,"prev_06_hca":4,"prev_07_hca":4,"prev_08_hca":4,"prev_09_hca":2,"prev_10_hca":0,"prev_11_hca":0,"prev_12_hca":0,"prev_13_hca":0,"prev_14_hca":0,"prev_15_hca":0,"prev_16_hca":0,"prev_17_hca":0,"prev_18_hca":0,"device_date":"2021-02-28","timestamp":"1111-11-11T11:11:11Z"}
-// |bfw;00707788;904;961;1111-11-11 11:11.11
-
-// telegram=|3644D7088877700002087A8BC000002F2F011F03C1038D152A0000000000000000000000000200040400040E00E20A23C11D238D010321|
-// {"_":"telegram","media":"heat cost allocation","meter":"bfw240radio","name":"bfw","id":"00707788","current_hca":909,"prev_hca":961,"prev_01_hca":909,"prev_02_hca":541,"prev_03_hca":961,"prev_04_hca":522,"prev_05_hca":226,"prev_06_hca":14,"prev_07_hca":4,"prev_08_hca":4,"prev_09_hca":4,"prev_10_hca":2,"prev_11_hca":0,"prev_12_hca":0,"prev_13_hca":0,"prev_14_hca":0,"prev_15_hca":0,"prev_16_hca":0,"prev_17_hca":0,"prev_18_hca":0,"device_date":"2021-03-01","timestamp":"1111-11-11T11:11:11Z"}
-// |bfw;00707788;909;961;1111-11-11 11:11.11
-
-// Test: bfww bfw240radio 00707076 NOKEY
-// telegram=|3644D7087670700002087A9CC000002F2F6E1F000000000B36000000000000000000000000000000000000000000000000000000260221|
-// {"_":"telegram","media":"heat cost allocation","meter":"bfw240radio","name":"bfww","id":"00707076","current_hca":0,"prev_hca":0,"prev_01_hca":0,"prev_02_hca":0,"prev_03_hca":0,"prev_04_hca":0,"prev_05_hca":0,"prev_06_hca":0,"prev_07_hca":0,"prev_08_hca":0,"prev_09_hca":0,"prev_10_hca":0,"prev_11_hca":0,"prev_12_hca":0,"prev_13_hca":0,"prev_14_hca":0,"prev_15_hca":0,"prev_16_hca":0,"prev_17_hca":0,"prev_18_hca":0,"device_date":"2021-02-26","timestamp":"1111-11-11T11:11:11Z"}
-// |bfww;00707076;0;0;1111-11-11 11:11.11
-
-// telegram=|3644D7087670700002087A27C000002F2F011F000000000B36000000000000000000000000000000000000000000000000000000010321|
-// {"_":"telegram","media":"heat cost allocation","meter":"bfw240radio","name":"bfww","id":"00707076","current_hca":0,"prev_hca":0,"prev_01_hca":0,"prev_02_hca":0,"prev_03_hca":0,"prev_04_hca":0,"prev_05_hca":0,"prev_06_hca":0,"prev_07_hca":0,"prev_08_hca":0,"prev_09_hca":0,"prev_10_hca":0,"prev_11_hca":0,"prev_12_hca":0,"prev_13_hca":0,"prev_14_hca":0,"prev_15_hca":0,"prev_16_hca":0,"prev_17_hca":0,"prev_18_hca":0,"device_date":"2021-03-01","timestamp":"1111-11-11T11:11:11Z"}
-// |bfww;00707076;0;0;1111-11-11 11:11.11
-
-
-// Test: bfwww bfw240radio 00707447 NOKEY
-// telegram=|3644D7084774700002087A80C000002F2F6F1F01440100147000000000000000000000000000000000000000000037009B4410AC260221|
-// {"_":"telegram","media":"heat cost allocation","meter":"bfw240radio","name":"bfwww","id":"00707447","current_hca":256,"prev_hca":324,"prev_01_hca":172,"prev_02_hca":324,"prev_03_hca":155,"prev_04_hca":55,"prev_05_hca":0,"prev_06_hca":0,"prev_07_hca":0,"prev_08_hca":0,"prev_09_hca":0,"prev_10_hca":0,"prev_11_hca":0,"prev_12_hca":0,"prev_13_hca":0,"prev_14_hca":0,"prev_15_hca":0,"prev_16_hca":0,"prev_17_hca":0,"prev_18_hca":0,"device_date":"2021-02-26","timestamp":"1111-11-11T11:11:11Z"}
-// |bfwww;00707447;256;324;1111-11-11 11:11.11
-
-// telegram=|3644D7084774700002087AE1C000002F2F481F0144010014700000000000000000000000000000000000000000379B0144AC0100040321|
-// {"_":"telegram","media":"heat cost allocation","meter":"bfw240radio","name":"bfwww","id":"00707447","current_hca":256,"prev_hca":324,"prev_01_hca":256,"prev_02_hca":172,"prev_03_hca":324,"prev_04_hca":155,"prev_05_hca":55,"prev_06_hca":0,"prev_07_hca":0,"prev_08_hca":0,"prev_09_hca":0,"prev_10_hca":0,"prev_11_hca":0,"prev_12_hca":0,"prev_13_hca":0,"prev_14_hca":0,"prev_15_hca":0,"prev_16_hca":0,"prev_17_hca":0,"prev_18_hca":0,"device_date":"2021-03-04","timestamp":"1111-11-11T11:11:11Z"}
-// |bfwww;00707447;256;324;1111-11-11 11:11.11
diff --git a/components/wmbus_common/driver_c5isf.cpp b/components/wmbus_common/driver_c5isf.cpp
deleted file mode 100644
index a39aafb7..00000000
--- a/components/wmbus_common/driver_c5isf.cpp
+++ /dev/null
@@ -1,321 +0,0 @@
-/*
- Copyright (C) 2022 Fredrik Öhrström (gpl-3.0-or-later)
- 2022 Alexander Streit (gpl-3.0-or-later)
-
- 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 .
-*/
-
-#include"meters_common_implementation.h"
-
-namespace
-{
- struct Driver : public virtual MeterCommonImplementation
- {
- Driver(MeterInfo &mi, DriverInfo &di);
-
- // Three types of telegrams (T1A1 T1A2 T1B) they all share total_energy_kwh and total_volume_m3.
- // The T1A1 and T1B also contains a status.
-
- // T1A1 and T1A2 also contains the previous month dates.
- // We assume that they are identical for both types of telegrams
- // so we use the same storage here.
- // prev_month_date[14]
-
- // T1A1 contains 14 back months of energy consumption.
- // total_energy_prev_month_kwh[14]
-
- // T1A2 contains 14 back months of volume consumption.
- // total_volume_prev_month_m3[14]
-
- // T1B contains:
- // due_energy_kwh
- // due_date
- // volume_flow_m3h
- // power_kw
- // total_energy_last_month_kwh
- // last_month_date_;
- // max_power_last_month_kw
- // flow_temperature_c
- // return_temperature_c
- };
-
- static bool ok = staticRegisterDriver([](DriverInfo&di)
- {
- di.setName("c5isf");
- di.setDefaultFields("name,id,total_energy_consumption_kwh,total_volume_m3,status,timestamp");
-
- di.setMeterType(MeterType::HeatMeter);
- di.addLinkMode(LinkMode::T1);
- di.addMVT(MANUFACTURER_ZRI, 0x0d, 0x88); // Telegram type T1A1
- di.addMVT(MANUFACTURER_ZRI, 0x07, 0x88); // Telegram type T1A2
- di.addMVT(MANUFACTURER_ZRI, 0x04, 0x88); // Telegram type T1B
- di.setConstructor([](MeterInfo& mi, DriverInfo& di){ return shared_ptr(new Driver(mi, di)); });
- });
-
- Driver::Driver(MeterInfo &mi, DriverInfo &di) : MeterCommonImplementation(mi, di)
- {
- // Fields common for T1A1, T1A2, T1B...........
-
- addNumericFieldWithExtractor(
- "total_energy_consumption",
- "The total heat energy consumption recorded by this meter.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Energy,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyEnergyVIF)
- );
-
- addNumericFieldWithExtractor(
- "total_cooling_consumption",
- "The total cooling energy consumption recorded by this meter.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Energy,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyEnergyVIF)
- .set(TariffNr(1))
- );
-
- addNumericFieldWithExtractor(
- "total_volume",
- "The total heating media volume recorded by this meter.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Volume,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::Volume)
- );
-
- // Status field common for T1A1 and T1B
-
- addStringFieldWithExtractorAndLookup(
- "status",
- "Status and error flags.",
- PrintProperty::STATUS | PrintProperty::INCLUDE_TPL_STATUS,
- FieldMatcher::build()
- .set(VIFRange::ErrorFlags),
- {
- {
- {
- "ERROR_FLAGS",
- Translate::MapType::DecimalsToString,
- AlwaysTrigger, MaskBits(9999),
- "OK",
- {
- { 2000, "VERIFICATION_EXPIRED" }, // Status initial verification expired
- { 1000, "BATTERY_EXPIRED" }, // END Status end of the battery
- { 800, "WIRELESS_ERROR" }, // Wireless interface
- { 100, "HARDWARE_ERROR3" }, // Hardware error
- { 50, "VALUE_OVERLOAD" }, // Measured value outside overload range
- { 40, "AIR_INSIDE" }, // Air inside the medium Vent system (**)
- { 30, "REVERSE_FLOW" }, // Reverse water flow detected
- { 20, "DRY" }, // No water in the measuring tube
- { 10, "ERROR_MEASURING" }, // Error in the measuring system
- { 9, "HARDWARE_ERROR2" }, // Hardware error Exchange device
- { 8, "HARDWARE_ERROR1" }, // Hardware error Exchange device
- { 7, "LOW_BATTERY" }, // Battery voltage Exchange device
- { 6, "SUPPLY_SENSOR_INTERRUPTED" }, // Interruption supply sensor Check sensors
- { 5, "SHORT_CIRCUIT_SUPPLY_SENSOR" }, // Short circuit supply sensor Check sensors
- { 4, "RETURN_SENSOR_INTERRUPTED" }, // Interruption return sensor
- { 3, "SHORT_CIRCUIT_RETURN_SENSOR" }, // Short circuit return sensor / Check sensors
- { 2, "TEMP_ABOVE_RANGE" }, // Temperature above of measuring range
- { 1, "TEMP_BELOW_RANGE" }, // Temperature below of measuring range
- }
- },
- },
- });
-
- // Dates are common to T1A1 and T1A2 ///////////////////////////////////////////////////////
-
- for (int i=0; i<14; ++i)
- {
- addStringFieldWithExtractor(
- tostrprintf("prev_%d_month", i+1),
- "The due date.",
- DEFAULT_PRINT_PROPERTIES,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(StorageNr(32+i))
- .set(VIFRange::Date)
- );
- }
-
- // Telegram type T1A1 ///////////////////////////////////////////////////////
-
- for (int i=0; i<14; ++i)
- {
- addNumericFieldWithExtractor(
- tostrprintf("prev_%d_month", i+1),
- "The total heat energy consumption recorded at end of previous month.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Energy,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(StorageNr(32+i))
- .set(VIFRange::AnyEnergyVIF)
- );
- }
-
- // Telegram type T1A2 ///////////////////////////////////////////////////////
-
- for (int i=0; i<14; ++i)
- {
- addNumericFieldWithExtractor(
- tostrprintf("prev_%d_month", i+1),
- tostrprintf("Previous month %d last date.", i+1),
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Volume,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(StorageNr(32+i))
- .set(VIFRange::Volume)
- );
- }
-
- // Telegram type T1B ///////////////////////////////////////////////////////
-
- addNumericFieldWithExtractor(
- "due_energy_consumption",
- "The total heat energy consumption at the due date.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Energy,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(StorageNr(8))
- .set(VIFRange::AnyEnergyVIF)
- );
-
- addStringFieldWithExtractor(
- "due_date",
- "The due date.",
- DEFAULT_PRINT_PROPERTIES,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(StorageNr(8))
- .set(VIFRange::Date)
- );
-
- addNumericFieldWithExtractor(
- "volume_flow",
- "The current heat media volume flow.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Flow,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::VolumeFlow)
- );
-
- addNumericFieldWithExtractor(
- "power",
- "The current power consumption.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Power,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::PowerW)
- );
-
- addNumericFieldWithExtractor(
- "total_energy_consumption_last_month",
- "The total heat energy consumption recorded at end of last month.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Energy,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(StorageNr(32))
- .set(VIFRange::AnyEnergyVIF)
- );
-
- addStringFieldWithExtractor(
- "last_month_date",
- "The due date.",
- DEFAULT_PRINT_PROPERTIES,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::DateTime)
- );
-
- addNumericFieldWithExtractor(
- "max_power_last_month",
- "Maximum power consumption last month.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Power,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Maximum)
- .set(StorageNr(32))
- .set(VIFRange::PowerW)
- .add(VIFCombinable::PerMonth)
- );
-
- addNumericFieldWithExtractor(
- "flow_temperature",
- "The current forward heat media temperature.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Temperature,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::FlowTemperature)
- );
-
- addNumericFieldWithExtractor(
- "return_temperature",
- "The current return heat media temperature.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Temperature,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::ReturnTemperature)
- );
- }
-}
-
-// Test: Heat c5isf 55445555 NOKEY
-
-// telegram=|E544496A55554455880D7A320200002F2F_04060000000004130000000002FD17240084800106000000008280016C2124C480010600000080C280016CFFFF84810106000000808281016CFFFFC481010600000080C281016CFFFF84820106000000808282016CFFFFC482010600000080C282016CFFFF84830106000000808283016CFFFFC483010600000080C283016CFFFF84840106000000808284016CFFFFC484010600000080C284016CFFFF84850106000000808285016CFFFFC485010600000080C285016CFFFF84860106000000808286016CFFFFC486010600000080C286016CFFFF|
-// {"_":"telegram","media":"heat/cooling load","meter":"c5isf","name":"Heat","id":"55445555","total_energy_consumption_kwh":0,"total_volume_m3":0,"status":"ERROR REVERSE_FLOW SUPPLY_SENSOR_INTERRUPTED","prev_1_month":"2017-04-01","prev_2_month":"2127-15-31","prev_3_month":"2127-15-31","prev_4_month":"2127-15-31","prev_5_month":"2127-15-31","prev_6_month":"2127-15-31","prev_7_month":"2127-15-31","prev_8_month":"2127-15-31","prev_9_month":"2127-15-31","prev_10_month":"2127-15-31","prev_11_month":"2127-15-31","prev_12_month":"2127-15-31","prev_13_month":"2127-15-31","prev_14_month":"2127-15-31","prev_1_month_kwh":0,"prev_2_month_kwh":-2147483648,"prev_3_month_kwh":-2147483648,"prev_4_month_kwh":-2147483648,"prev_5_month_kwh":-2147483648,"prev_6_month_kwh":-2147483648,"prev_7_month_kwh":-2147483648,"prev_8_month_kwh":-2147483648,"prev_9_month_kwh":-2147483648,"prev_10_month_kwh":-2147483648,"prev_11_month_kwh":-2147483648,"prev_12_month_kwh":-2147483648,"prev_13_month_kwh":-2147483648,"prev_14_month_kwh":-2147483648,"total_energy_consumption_last_month_kwh":0,"timestamp":"1111-11-11T11:11:11Z"}
-// |Heat;55445555;0;0;ERROR REVERSE_FLOW SUPPLY_SENSOR_INTERRUPTED;1111-11-11 11:11.11
-
-// Type T1A2 telegram:
-// telegram=|DA44496A5555445588077A320200002F2F_04140000000084800114000000008280016C2124C480011400000080C280016CFFFF84810114000000808281016CFFFFC481011400000080C281016CFFFF84820114000000808282016CFFFFC482011400000080C282016CFFFF84830114000000808283016CFFFFC483011400000080C283016CFFFF84840114000000808284016CFFFFC484011400000080C284016CFFFF84850114000000808285016CFFFFC485011400000080C285016CFFFF84860114000000808286016CFFFFC486011400000080C286016CFFFF|
-// {"_":"telegram","id": "55445555","media": "water","meter": "c5isf","name": "Heat","prev_10_month": "2127-15-31","prev_10_month_kwh":-2147483648,"prev_10_month_m3":-21474836.48,"prev_11_month": "2127-15-31","prev_11_month_kwh":-2147483648,"prev_11_month_m3":-21474836.48,"prev_12_month": "2127-15-31","prev_12_month_kwh":-2147483648,"prev_12_month_m3":-21474836.48,"prev_13_month": "2127-15-31","prev_13_month_kwh":-2147483648,"prev_13_month_m3":-21474836.48,"prev_14_month": "2127-15-31","prev_14_month_kwh":-2147483648,"prev_14_month_m3":-21474836.48,"prev_1_month": "2017-04-01","prev_1_month_kwh": 0,"prev_1_month_m3": 0,"prev_2_month": "2127-15-31","prev_2_month_kwh":-2147483648,"prev_2_month_m3":-21474836.48,"prev_3_month": "2127-15-31","prev_3_month_kwh":-2147483648,"prev_3_month_m3":-21474836.48,"prev_4_month": "2127-15-31","prev_4_month_kwh":-2147483648,"prev_4_month_m3":-21474836.48,"prev_5_month": "2127-15-31","prev_5_month_kwh":-2147483648,"prev_5_month_m3":-21474836.48,"prev_6_month": "2127-15-31","prev_6_month_kwh":-2147483648,"prev_6_month_m3":-21474836.48,"prev_7_month": "2127-15-31","prev_7_month_kwh":-2147483648,"prev_7_month_m3":-21474836.48,"prev_8_month": "2127-15-31","prev_8_month_kwh":-2147483648,"prev_8_month_m3":-21474836.48,"prev_9_month": "2127-15-31","prev_9_month_kwh":-2147483648,"prev_9_month_m3":-21474836.48,"status": "ERROR","timestamp": "1111-11-11T11:11:11Z","total_energy_consumption_kwh": 0,"total_energy_consumption_last_month_kwh": 0,"total_volume_m3": 0}
-// |Heat;55445555;0;0;ERROR;1111-11-11 11:11.11
-
-// Type T1B telegram:
-// telegram=|5E44496A5555445588047A0A0050052F2F_04061A0000000413C20800008404060000000082046CC121043BA4000000042D1900000002591216025DE21002FD17000084800106000000008280016CC121948001AE25000000002F2F2F2F2F2F|
-// {"_":"telegram","due_date": "2022-01-01","due_energy_consumption_kwh": 0,"flow_temperature_c": 56.5,"id": "55445555","max_power_last_month_kw": 0,"media": "heat","meter": "c5isf","name": "Heat","power_kw": 2.5,"prev_10_month": "2127-15-31","prev_10_month_kwh":-2147483648,"prev_10_month_m3":-21474836.48,"prev_11_month": "2127-15-31","prev_11_month_kwh":-2147483648,"prev_11_month_m3":-21474836.48,"prev_12_month": "2127-15-31","prev_12_month_kwh":-2147483648,"prev_12_month_m3":-21474836.48,"prev_13_month": "2127-15-31","prev_13_month_kwh":-2147483648,"prev_13_month_m3":-21474836.48,"prev_14_month": "2127-15-31","prev_14_month_kwh":-2147483648,"prev_14_month_m3":-21474836.48,"prev_1_month": "2022-01-01","prev_1_month_kwh": 0,"prev_1_month_m3": 0,"prev_2_month": "2127-15-31","prev_2_month_kwh":-2147483648,"prev_2_month_m3":-21474836.48,"prev_3_month": "2127-15-31","prev_3_month_kwh":-2147483648,"prev_3_month_m3":-21474836.48,"prev_4_month": "2127-15-31","prev_4_month_kwh":-2147483648,"prev_4_month_m3":-21474836.48,"prev_5_month": "2127-15-31","prev_5_month_kwh":-2147483648,"prev_5_month_m3":-21474836.48,"prev_6_month": "2127-15-31","prev_6_month_kwh":-2147483648,"prev_6_month_m3":-21474836.48,"prev_7_month": "2127-15-31","prev_7_month_kwh":-2147483648,"prev_7_month_m3":-21474836.48,"prev_8_month": "2127-15-31","prev_8_month_kwh":-2147483648,"prev_8_month_m3":-21474836.48,"prev_9_month": "2127-15-31","prev_9_month_kwh":-2147483648,"prev_9_month_m3":-21474836.48,"return_temperature_c": 43.22,"status": "OK","timestamp": "1111-11-11T11:11:11Z","total_energy_consumption_kwh": 26,"total_energy_consumption_last_month_kwh": 0,"total_volume_m3": 2.242,"volume_flow_m3h": 0.164}
-// |Heat;55445555;26;2.242;OK;1111-11-11 11:11.11
-
-// Test: Heat c5isf 32002044 NOKEY
-// Test telegram with max_power_last_month_kwh which is non-zero
-// telegram=|5E44496A4420003288047AFC0050052F2F_0406D00E00000413B28A05008404060000000082046CC121043B00000000042D000000000259E719025D051402FD17000084800106C00C00008280016CC125948001AE25090000002F2F2F2F2F2F|
-// {"_":"telegram","media":"heat","meter":"c5isf","name":"Heat","id":"32002044","total_energy_consumption_kwh":3792,"total_volume_m3":363.186,"status":"OK","prev_1_month":"2022-05-01","prev_1_month_kwh":3264,"due_energy_consumption_kwh":0,"due_date":"2022-01-01","volume_flow_m3h":0,"power_kw":0,"total_energy_consumption_last_month_kwh":3264,"max_power_last_month_kw":9,"flow_temperature_c":66.31,"return_temperature_c":51.25,"timestamp":"1111-11-11T11:11:11Z"}
-// |Heat;32002044;3792;363.186;OK;1111-11-11 11:11.11
-
-// Test: HeatCool c5isf 32022703 NOKEY
-// telegram=|8E44496A03270232880D7AED0080052F2F0406050200008410069A1700000413BC6F2E00840406E701000082046C2131841406C20B000082146C2131043B00000000042D0000000002595D09025D240802FD17000084800106050200008280016C213C948001AE2501000000849001069A1700008290016C213C849001AE25000000002F2F2F2F2F2F2F2F2F2F2F2F|
-// {"_": "telegram","due_date": "2025-01-01","due_energy_consumption_kwh": 487,"flow_temperature_c": 23.97,"id": "32022703","max_power_last_month_kw": 1,"media": "heat/cooling load","meter": "c5isf","name": "HeatCool","power_kw": 0,"prev_1_month": "2025-12-01","prev_1_month_kwh": 517,"return_temperature_c": 20.84,"status": "OK","timestamp": "1111-11-11T11:11:11Z","total_cooling_consumption_kwh": 6042,"total_energy_consumption_kwh": 517,"total_energy_consumption_last_month_kwh": 517,"total_volume_m3": 3043.26,"volume_flow_m3h": 0}
-// |HeatCool;32022703;517;3043.26;OK;1111-11-11 11:11.11
diff --git a/components/wmbus_common/driver_cma12w.cpp b/components/wmbus_common/driver_cma12w.cpp
deleted file mode 100644
index 4684f693..00000000
--- a/components/wmbus_common/driver_cma12w.cpp
+++ /dev/null
@@ -1,90 +0,0 @@
-/*
- Copyright (C) 2020-2022 Fredrik Öhrström (gpl-3.0-or-later)
-
- 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 .
-*/
-
-#include"meters_common_implementation.h"
-
-namespace
-{
- struct Driver : public virtual MeterCommonImplementation
- {
- Driver(MeterInfo &mi, DriverInfo &di);
- };
-
- static bool ok = staticRegisterDriver([](DriverInfo&di)
- {
- di.setName("cma12w");
- di.setDefaultFields("name,id,current_temperature_c,timestamp");
- di.setMeterType(MeterType::TempHygroMeter);
- di.addLinkMode(LinkMode::C1);
- di.addLinkMode(LinkMode::T1);
- di.addMVT(MANUFACTURER_ELV, 0x1b, 0x20);
-
- di.setConstructor([](MeterInfo& mi, DriverInfo& di){ return shared_ptr(new Driver(mi, di)); });
-
- });
-
- Driver::Driver(MeterInfo &mi, DriverInfo &di) : MeterCommonImplementation(mi, di)
- {
- addOptionalLibraryFields("software_version");
-
- addStringField(
- "status",
- "Meter status from tpl status field.",
- PrintProperty::STATUS | PrintProperty::INCLUDE_TPL_STATUS);
-
- addNumericFieldWithExtractor(
- "current_temperature",
- "The current temperature.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Temperature,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::ExternalTemperature)
- );
-
- addNumericFieldWithExtractor(
- "average_temperature_1h",
- "The average temperature over the last hour.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Temperature,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::ExternalTemperature)
- .set(StorageNr(1))
- );
-
- addStringFieldWithExtractorAndLookup(
- "battery",
- "Battery status.",
- DEFAULT_PRINT_PROPERTIES,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::DigitalInput),
- Translate::Lookup()
- .add(Translate::Rule("BATTERY", Translate::MapType::BitToString)
- .set(MaskBits(0xffff)))
- );
- }
-}
-
-
-// Test: Tempo cma12w 66666666 NOKEY
-// telegram=|2744961566666666201B7AF9000020_2F2F02651E094265180902FD1B30030DFD0F05302E302E340F|
-// {"_":"telegram","media":"room sensor","meter":"cma12w","name":"Tempo","id":"66666666","software_version":"4.0.0","status":"OK","current_temperature_c":23.34,"average_temperature_1h_c":23.28,"battery":"BATTERY_330","timestamp":"1111-11-11T11:11:11Z"}
-// |Tempo;66666666;23.34;1111-11-11 11:11.11
diff --git a/components/wmbus_common/driver_compact5.cpp b/components/wmbus_common/driver_compact5.cpp
deleted file mode 100644
index 098adbe8..00000000
--- a/components/wmbus_common/driver_compact5.cpp
+++ /dev/null
@@ -1,132 +0,0 @@
-/*
- Copyright (C) 2020-2022 Fredrik Öhrström (gpl-3.0-or-later)
-
- 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 .
-*/
-
-#include"meters_common_implementation.h"
-
-namespace
-{
- struct Driver : public virtual MeterCommonImplementation
- {
- Driver(MeterInfo &mi, DriverInfo &di);
-
- void processContent(Telegram *t);
- };
-
- static bool ok = staticRegisterDriver([](DriverInfo&di)
- {
- di.setName("compact5");
- di.setDefaultFields("name,id,total_kwh,current_kwh,previous_kwh,timestamp");
- di.setMeterType(MeterType::WaterMeter);
- di.addLinkMode(LinkMode::T1);
- di.addLinkMode(LinkMode::C1);
- di.addMVT(MANUFACTURER_TCH, 0x04, 0x45);
- di.addMVT(MANUFACTURER_TCH, 0xc3, 0x45);
- di.addMVT(MANUFACTURER_TCH, 0x43, 0x22);
- di.addMVT(MANUFACTURER_TCH, 0x43, 0x45);
- di.addMVT(MANUFACTURER_TCH, 0x43, 0x39);
- di.usesProcessContent();
- di.setConstructor([](MeterInfo& mi, DriverInfo& di){ return shared_ptr(new Driver(mi, di)); });
- });
-
- Driver::Driver(MeterInfo &mi, DriverInfo &di) : MeterCommonImplementation(mi, di)
- {
- addNumericField("total",
- Quantity::Energy,
- DEFAULT_PRINT_PROPERTIES,
- "The total energy consumption recorded by this meter.");
-
- addNumericField("current",
- Quantity::Energy,
- DEFAULT_PRINT_PROPERTIES,
- "Energy consumption so far in this billing period.");
-
- addNumericField("previous",
- Quantity::Energy,
- DEFAULT_PRINT_PROPERTIES,
- "Energy consumption in previous billing period.");
- }
-
- void Driver::processContent(Telegram *t)
- {
- // Unfortunately, the Techem Compact V is mostly a proprieatary protocol
- // simple wrapped inside a wmbus telegram since the ci-field is 0xa2.
- // Which means that the entire payload is manufacturer specific.
-
- std::map> vendor_values;
- std::vector content;
-
- t->extractPayload(&content);
-
- if (content.size() < 9) return;
-
- uchar prev_lo = content[3];
- uchar prev_hi = content[4];
- uchar prev_hihi = content[5];
- double prev = (65536.0*prev_hihi+256.0*prev_hi+prev_lo);
-
- std::string prevs;
- strprintf(&prevs, "%02x%02x%02x", prev_lo, prev_hi, prev_hihi);
- int offset = t->parsed.size()+3;
- vendor_values["0215"] = { offset, DVEntry(offset, DifVifKey("0215"), MeasurementType::Instantaneous, 0x15, {}, {}, 0, 0, 0, prevs) };
- Explanation pe(offset, 3, prevs, KindOfData::CONTENT, Understanding::FULL);
- t->explanations.push_back(pe);
- t->addMoreExplanation(offset, " energy used in previous billing period (%f KWH)", prev);
-
- uchar curr_lo = content[7];
- uchar curr_hi = content[8];
- uchar curr_hihi = content[9];
- double curr = (65536.0*curr_hihi+256.0*curr_hi+curr_lo);
-
- std::string currs;
- strprintf(&currs, "%02x%02x%02x", curr_lo, curr_hi, curr_hihi);
- offset = t->parsed.size()+7;
- vendor_values["0215"] = { offset, DVEntry(offset, DifVifKey("0215"), MeasurementType::Instantaneous, 0x15, {}, {}, 0, 0, 0, currs) };
- Explanation ce(offset, 3, currs, KindOfData::CONTENT, Understanding::FULL);
- t->explanations.push_back(ce);
- t->addMoreExplanation(offset, " energy used in current billing period (%f KWH)", curr);
-
- double total_energy_kwh = prev+curr;
- setNumericValue("total", Unit::KWH, total_energy_kwh);
-
- double curr_energy_kwh = curr;
- setNumericValue("current", Unit::KWH, curr_energy_kwh);
-
- double prev_energy_kwh = prev;
- setNumericValue("previous", Unit::KWH, prev_energy_kwh);
- }
-}
-
-// Test: Heating compact5 62626262 NOKEY
-// telegram=|36446850626262624543A1_009F2777010060780000000A000000000000000000000000000000000000000000000000A0400000B4010000|
-// {"_":"telegram","media":"heat","meter":"compact5","name":"Heating","id":"62626262","total_kwh":495,"current_kwh":120,"previous_kwh":375,"timestamp":"1111-11-11T11:11:11Z"}
-// |Heating;62626262;495;120;375;1111-11-11 11:11.11
-
-// Test: Heating2 compact5 66336633 NOKEY
-// telegram=|37446850336633663943a2_10672c866100181c01000480794435d50000000000000000000000000000000000000000000000000000000000|
-// {"_":"telegram","media":"heat","meter":"compact5","name":"Heating2","id":"66336633","total_kwh":25250,"current_kwh":284,"previous_kwh":24966,"timestamp":"1111-11-11T11:11:11Z"}
-// |Heating2;66336633;25250;284;24966;1111-11-11 11:11.11
-
-// A telegram from a Vario 3 type 3.2.1 meter.
-// Test: vario compact5 67552079 NOKEY
-// telegram=|37446850792055673943A2_109F2F13C500608F1D00008066E8A69B26988D335F6411450C564C5145145CA0F1DA35B9DD37A1936BBF3D31D8|
-// {"_": "telegram","current_kwh": 7567,"id": "67552079","media": "heat","meter": "compact5","name": "vario","previous_kwh": 50451,"timestamp": "1111-11-11T11:11:11Z","total_kwh": 58018}
-// |vario;67552079;58018;7567;50451;1111-11-11 11:11.11
-
-// Test: tt compact5 41711422 NOKEY
-// telegram=|36446850221471414543a100ff32406002c8cd0000019f81071f68e001072c0c9106642075B5a6ac6ad229e993376aa7d10e1e5c60c1050000000056fed3|
-// {"_": "telegram","current_kwh": 205,"id": "41711422","media": "heat","meter": "compact5","name": "tt","previous_kwh": 155712,"timestamp": "1111-11-11T11:11:11Z","total_kwh": 155917}
-// |tt;41711422;155917;205;155712;1111-11-11 11:11.11
diff --git a/components/wmbus_common/driver_dme_07.cpp b/components/wmbus_common/driver_dme_07.cpp
deleted file mode 100644
index 98142366..00000000
--- a/components/wmbus_common/driver_dme_07.cpp
+++ /dev/null
@@ -1,77 +0,0 @@
-/*
- Copyright (C) 2021-2022 Fredrik Öhrström (gpl-3.0-or-later)
-
- 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 .
-*/
-
-#include"meters_common_implementation.h"
-
-namespace
-{
- struct Driver : public virtual MeterCommonImplementation {
- Driver(MeterInfo &mi, DriverInfo &di);
- };
-
- static bool ok = staticRegisterDriver([](DriverInfo&di)
- {
- di.setName("dme_07");
- di.setDefaultFields("name,id,total_m3,status,timestamp");
- di.setMeterType(MeterType::WaterMeter);
- di.addLinkMode(LinkMode::T1);
- di.addMVT(MANUFACTURER_DME, 0x07, 0x7b);
-
- di.setConstructor([](MeterInfo& mi, DriverInfo& di){ return shared_ptr(new Driver(mi, di)); });
- });
-
- Driver::Driver(MeterInfo &mi, DriverInfo &di) : MeterCommonImplementation(mi, di)
- {
- addStringFieldWithExtractorAndLookup(
- "status",
- "Status of meter.",
- DEFAULT_PRINT_PROPERTIES | PrintProperty::STATUS,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::ErrorFlags),
- {
- {
- {
- "ERROR_FLAGS",
- Translate::MapType::BitToString,
- AlwaysTrigger, MaskBits(0xffff),
- "OK",
- {
- }
- },
- },
- });
-
- addNumericFieldWithExtractor(
- "total",
- "The total water consumption recorded by this meter.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Volume,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::Volume)
- );
-
-
- }
-}
-
-// Test: DigiWasser dme_07 93929190 NOKEY
-// telegram=|1E44A511909192937B077A9F0010052F2F_04130347030002FD1700002F2F2F|
-// {"_":"telegram","media":"water","meter":"dme_07","name":"DigiWasser","id":"93929190","status":"OK","total_m3":214.787,"timestamp":"1111-11-11T11:11:11Z"}
-// |DigiWasser;93929190;214.787;OK;1111-11-11 11:11.11
diff --git a/components/wmbus_common/driver_ebzwmbe.cpp b/components/wmbus_common/driver_ebzwmbe.cpp
deleted file mode 100644
index e2f05fbf..00000000
--- a/components/wmbus_common/driver_ebzwmbe.cpp
+++ /dev/null
@@ -1,122 +0,0 @@
-/*
- Copyright (C) 2020-2022 Fredrik Öhrström (gpl-3.0-or-later)
-
- 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 .
-*/
-
-#include"meters_common_implementation.h"
-
-namespace
-{
- struct Driver : public virtual MeterCommonImplementation
- {
- Driver(MeterInfo &mi, DriverInfo &di);
- };
-
- static bool ok = staticRegisterDriver([](DriverInfo&di)
- {
- di.setName("ebzwmbe");
- di.setDefaultFields("name,id,total_energy_consumption_kwh,current_power_consumption_kw,current_power_consumption_phase1_kw,current_power_consumption_phase2_kw,current_power_consumption_phase3_kw,timestamp");
- di.setMeterType(MeterType::ElectricityMeter);
- di.addLinkMode(LinkMode::T1);
- di.addMVT(MANUFACTURER_EBZ, 0x02, 0x01);
- di.setConstructor([](MeterInfo& mi, DriverInfo& di){ return shared_ptr(new Driver(mi, di)); });
- });
-
- Driver::Driver(MeterInfo &mi, DriverInfo &di) : MeterCommonImplementation(mi, di)
- {
- addNumericFieldWithExtractor(
- "total_energy_consumption",
- "The total energy consumption recorded by this meter.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Energy,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyEnergyVIF)
- );
-
- addNumericFieldWithExtractor(
- "current_power_consumption_phase1",
- "Current power consumption at phase 1.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Power,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(DifVifKey("04A9FF01"))
- );
-
- addNumericFieldWithExtractor(
- "current_power_consumption_phase2",
- "Current power consumption at phase 2.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Power,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(DifVifKey("04A9FF02"))
- );
-
- addNumericFieldWithExtractor(
- "current_power_consumption_phase3",
- "Current power consumption at phase 3.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Power,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(DifVifKey("04A9FF03"))
- );
-
- addStringFieldWithExtractor(
- "customer",
- "Customer name.",
- DEFAULT_PRINT_PROPERTIES,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::Customer)
- );
-
-/*
- addNumericFieldWithExtractor(
- "current_power_consumption",
- "Calculated sum of power consumption of all phases.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Power,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyPowerVIF)
- );
-*/
-
- addNumericFieldWithCalculator(
- "current_power_consumption",
- "Calculated sum of power consumption of all phases.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Power,
- "current_power_consumption_phase1_kw + current_power_consumption_phase2_kw + current_power_consumption_phase3_kw"
- );
-
- }
-}
-
-// Test: Elen1 ebzwmbe 22992299 NOKEY
-// telegram=|5B445A149922992202378C20F6900F002C25BC9E0000BF48954821BC508D72992299225A140102F6003007102F2F040330F92A0004A9FF01FF24000004A9FF026A29000004A9FF03460600000DFD11063132333435362F2F2F2F2F2F|
-// {"_":"telegram","media":"electricity","meter":"ebzwmbe","name":"Elen1","id":"22992299","total_energy_consumption_kwh":2816.304,"current_power_consumption_phase1_kw":0.09471,"current_power_consumption_phase2_kw":0.10602,"current_power_consumption_phase3_kw":0.01606,"customer":"654321","current_power_consumption_kw":0.21679,"timestamp":"1111-11-11T11:11:11Z"}
-// |Elen1;22992299;2816.304;0.21679;0.09471;0.10602;0.01606;1111-11-11 11:11.11
-
-// Test: MyEl ebzwmbe 01135263 NOKEY
-// Comment: This is eBZ DD3 meter with a wMB-E01 wmbus radio converter module.
-// telegram=|4B445A146450000002378C20F3900F002C25F38B00006CBB07CEB215901172635213015A140102F3002007102F2F#04039D3E03000429E82201000DFD110E333632353331313031305A424531|
-// {"_":"telegram","media":"electricity","meter":"ebzwmbe","name":"MyEl","id":"01135263","total_energy_consumption_kwh":212.637,"customer":"1EBZ0101135263","current_power_consumption_kw":null,"timestamp":"1111-11-11T11:11:11Z"}
-// |MyEl;01135263;212.637;null;null;null;null;1111-11-11 11:11.11
diff --git a/components/wmbus_common/driver_ehzp.cpp b/components/wmbus_common/driver_ehzp.cpp
deleted file mode 100644
index 1bd55992..00000000
--- a/components/wmbus_common/driver_ehzp.cpp
+++ /dev/null
@@ -1,85 +0,0 @@
-/*
- Copyright (C) 2020-2022 Fredrik Öhrström (gpl-3.0-or-later)
-
- 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 .
-*/
-#include"meters_common_implementation.h"
-
-namespace
-{
- struct Driver : public virtual MeterCommonImplementation
- {
- Driver(MeterInfo &mi, DriverInfo &di);
- };
-
- static bool ok = staticRegisterDriver([](DriverInfo&di)
- {
- di.setName("ehzp");
- di.setDefaultFields("name,id,total_energy_consumption_kwh,current_power_consumption_kw,total_energy_production_kwh,timestamp");
- di.setMeterType(MeterType::ElectricityMeter);
- di.addLinkMode(LinkMode::T1);
- di.addMVT(MANUFACTURER_EMH, 0x02, 0x02);
- di.setConstructor([](MeterInfo& mi, DriverInfo& di){ return shared_ptr(new Driver(mi, di)); });
- });
-
- Driver::Driver(MeterInfo &mi, DriverInfo &di) : MeterCommonImplementation(mi, di)
- {
- addStringField(
- "status",
- "Meter status. Includes both meter error field and tpl status field.",
- PrintProperty::STATUS | PrintProperty::INCLUDE_TPL_STATUS);
-
- addOptionalLibraryFields("on_time_h");
-
- addNumericFieldWithExtractor(
- "total_energy_consumption",
- "The total energy consumption recorded by this meter.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Energy,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyEnergyVIF)
- );
-
- addNumericFieldWithExtractor(
- "current_power_consumption",
- "Current power consumption.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Power,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyPowerVIF)
- );
-
- addNumericFieldWithExtractor(
- "total_energy_production",
- "The total energy production recorded by this meter.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Energy,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyEnergyVIF)
- .add(VIFCombinable::BackwardFlow)
- );
-
- }
-}
-
-// Test: Elen3 ehzp 55995599 NOKEY
-// telegram=|5344A8159955995502028C201D900F002C250C390000ED176BBBB1591ADB7A1D003007102F2F_0700583B74020000000007803CBCD70200000000000728B070200000000000042092A406002F2F2F2F2F2F2F2F2F|
-// {"_":"telegram","media":"electricity","meter":"ehzp","name":"Elen3","id":"55995599","status":"OK","on_time_h":120.929444,"total_energy_consumption_kwh":41.1718,"current_power_consumption_kw":2.126,"total_energy_production_kwh":0.1863,"timestamp":"1111-11-11T11:11:11Z"}
-// |Elen3;55995599;41.1718;2.126;0.1863;1111-11-11 11:11.11
diff --git a/components/wmbus_common/driver_ei6500.cpp b/components/wmbus_common/driver_ei6500.cpp
deleted file mode 100644
index ead1de99..00000000
--- a/components/wmbus_common/driver_ei6500.cpp
+++ /dev/null
@@ -1,344 +0,0 @@
-/*
- Copyright (C) 2020-2022 Fredrik Öhrström (gpl-3.0-or-later)
-
- 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 .
-*/
-
-#include"meters_common_implementation.h"
-
-namespace
-{
- struct Driver : public virtual MeterCommonImplementation
- {
- Driver(MeterInfo &mi, DriverInfo &di);
- };
-
- static bool ok = staticRegisterDriver([](DriverInfo&di)
- {
- di.setName("ei6500");
- di.setDefaultFields("name,id,status,last_alarm_date,alarm_counter,timestamp");
- di.setMeterType(MeterType::SmokeDetector);
- di.addLinkMode(LinkMode::T1);
- di.addMVT(MANUFACTURER_EIE, 0x1a, 0x0c);
- di.setConstructor([](MeterInfo& mi, DriverInfo& di){ return shared_ptr(new Driver(mi, di)); });
- });
-
- Driver::Driver(MeterInfo &mi, DriverInfo &di) : MeterCommonImplementation(mi, di)
- {
- setMfctTPLStatusBits(
- Translate::Lookup()
- .add(Translate::Rule("TPL_STS", Translate::MapType::BitToString)
- .set(MaskBits(0xe0))
- .set(DefaultMessage("OK"))
- .add(Translate::Map(0x04 ,"RTC_INVALID", TestBit::Set))));
-
- addStringFieldWithExtractorAndLookup(
- "status",
- "Meter error flags. IMPORTANT! Smoke alarm is NOT reported here! You MUST check last alarm date and counter!",
- PrintProperty::STATUS | PrintProperty::INCLUDE_TPL_STATUS,
- FieldMatcher::build()
- .set(VIFRange::ErrorFlags),
- Translate::Lookup(
- {
- {
- {
- "ERROR_FLAGS",
- Translate::MapType::BitToString,
- AlwaysTrigger, MaskBits(0xffff),
- "OK",
- {
- /* When used for m-bus?
- bit 7-7 Not used, zero by default
- bit 8 Application error, unknown field C
- bit 9 Application error, unknown field CI
- bit 10 Application error, unknown record
- bit 11 Application error, access right
- bit 12 Application error, record size
- bit 13 Application error, record value
- bit 14 Application error, bad password
- bit 15 Not used, zero by defaul
- */
- }
- },
- },
- }));
-
- addStringFieldWithExtractor(
- "last_alarm_date",
- "Date when the smoke alarm last triggered.",
- DEFAULT_PRINT_PROPERTIES,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(SubUnitNr(1))
- .set(TariffNr(1))
- .set(VIFRange::Date)
- );
-
- addNumericFieldWithExtractor(
- "alarm",
- "Number of times the smoke alarm has triggered.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Dimensionless,
- VifScaling::None, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(SubUnitNr(1))
- .set(TariffNr(1))
- .set(VIFRange::CumulationCounter)
- );
-
- addStringFieldWithExtractor(
- "software_version",
- "Meter software version number.",
- DEFAULT_PRINT_PROPERTIES,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::SoftwareVersion)
- );
-
- addStringFieldWithExtractor(
- "message_datetime",
- "Device date time.",
- DEFAULT_PRINT_PROPERTIES,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::DateTime)
- );
-
- addNumericFieldWithExtractor(
- "duration_removed",
- "Time the smoke alarm has been removed.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Time,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(SubUnitNr(1))
- .set(TariffNr(2))
- .set(VIFRange::DurationOfTariff)
- );
-
- addStringFieldWithExtractor(
- "last_remove_date",
- "Date when the smoke alarm was last removed.",
- DEFAULT_PRINT_PROPERTIES,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(SubUnitNr(1))
- .set(TariffNr(2))
- .set(VIFRange::Date)
- );
-
- addNumericFieldWithExtractor(
- "removed",
- "Number of times the smoke alarm has been removed.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Dimensionless,
- VifScaling::None, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(SubUnitNr(1))
- .set(TariffNr(2))
- .set(VIFRange::CumulationCounter)
- );
-
- addStringFieldWithExtractor(
- "test_button_last_date",
- "Date when test button was last pressed.",
- DEFAULT_PRINT_PROPERTIES,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(SubUnitNr(1))
- .set(TariffNr(3))
- .set(VIFRange::Date)
- );
-
- addNumericFieldWithExtractor(
- "test_button",
- "Number of times the test button has been pressed.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Dimensionless,
- VifScaling::None, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(SubUnitNr(1))
- .set(TariffNr(3))
- .set(VIFRange::CumulationCounter)
- );
-
- addStringFieldWithExtractor(
- "installation_date",
- "Date when the smoke alarm was installed.",
- DEFAULT_PRINT_PROPERTIES,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(TariffNr(2))
- .set(VIFRange::Date)
- );
-
- addStringFieldWithExtractor(
- "last_sound_check_date",
- "Date when the smoke alarm last checked the piezo speaker.",
- DEFAULT_PRINT_PROPERTIES,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(StorageNr(1))
- .set(VIFRange::Date)
- );
-
- addStringFieldWithExtractorAndLookup(
- "dust_level",
- "Dust level 0 (best) to 15 (worst).",
- DEFAULT_PRINT_PROPERTIES,
- FieldMatcher::build()
- .set(DifVifKey("8440FF2C")),
- Translate::Lookup(
- {
- {
- {
- "DUST",
- Translate::MapType::IndexToString,
- AlwaysTrigger, MaskBits(0x1f),
- "",
- {
- }
- },
- },
- }));
-
- addStringFieldWithExtractorAndLookup(
- "battery_level",
- "Battery voltage level.",
- DEFAULT_PRINT_PROPERTIES,
- FieldMatcher::build()
- .set(DifVifKey("8440FF2C")),
- Translate::Lookup(
- {
- {
- {
- "BATTERY_VOLTAGE",
- Translate::MapType::IndexToString,
- AlwaysTrigger, MaskBits(0x0f00),
- "",
- {
- { 0x0000, "2.25V" },
- { 0x0100, "2.30V" },
- { 0x0200, "2.35V" },
- { 0x0300, "2.40V" },
-
- { 0x0400, "2.45V" },
- { 0x0500, "2.50V" },
- { 0x0600, "2.55V" },
- { 0x0700, "2.60V" },
-
- { 0x0800, "2.65V" },
- { 0x0900, "2.70V" },
- { 0x0a00, "2.75V" },
- { 0x0b00, "2.80V" },
-
- { 0x0c00, "2.85V" },
- { 0x0d00, "2.90V" },
- { 0x0e00, "2.95V" },
- { 0x0f00, "3.00V" }
- }
- },
- },
- }));
-
- addStringFieldWithExtractorAndLookup(
- "obstacle_distance",
- "The distance to a detected obstacle.",
- DEFAULT_PRINT_PROPERTIES,
- FieldMatcher::build()
- .set(DifVifKey("8440FF2C")),
- Translate::Lookup(
- {
- {
- {
- "OBSTACLE_DISTANCE",
- Translate::MapType::IndexToString,
- AlwaysTrigger, MaskBits(0x700000),
- "",
- {
- { 0x000000, "SEODS_NOT_COMPLETED" },
- { 0x100000, "" }, // No obstacle detected
- { 0x200000, "45_TO_60_CM" },
- { 0x300000, "38_TO_53_CM" },
-
- { 0x400000, "33_TO_48_CM" },
- { 0x500000, "28_TO_40_CM" },
- { 0x600000, "20_TO_33_CM" },
- { 0x700000, "0_TO_25_CM" },
- }
- },
- },
- }));
-
-
- addStringFieldWithExtractorAndLookup(
- "head_status",
- "Status of smoke detector sensors, merged into the status field.",
- PrintProperty::INJECT_INTO_STATUS | PrintProperty::HIDE,
- FieldMatcher::build()
- .set(DifVifKey("8440FF2C")),
- Translate::Lookup(
- {
- {
- {
- "HEAD_STATUS",
- Translate::MapType::BitToString,
- AlwaysTrigger, MaskBits(0xff8ff0e0),
- "OK",
- {
- /* 0x00000000-0x000000f dust level*/
- { 0x00000020, "SOUNDER_FAULT" },
- { 0x00000040, "TAMPER_WHILE_REMOVED" },
- { 0x00000080, "EOL_REACHED" },
- /* 0x00000100-0x000f00 battery evel*/
- { 0x00001000, "LOW_BATTERY_FAULT" },
- { 0x00002000, "ALARM_SENSOR_FAULT" },
- { 0x00004000, "OBSTACLE_DETECTOR_FAULT" },
- { 0x00008000, "EOL_WITHIN_12_MONTH" },
- { 0x00010000, "SEODS_NOT_YET_COMPLETED", TestBit::NotSet },
- { 0x00020000, "ENV_CHANGED_SINCE_INSTALLATION" },
- { 0x00040000, "COMM_TO_HEAD_FAULT" },
- { 0x00080000, "INTERFERENCE_PREVENTING_OBSTACLE_DETECTION" },
- /* 0x00100000-0x0700000 distance */
- /* 0x00800000 reserved */
- { 0x01000000, "OBSTACLE_DETECTED" },
- { 0x02000000, "SMOKE_DETECTOR_FULLY_COVERED" }
- }
- },
- },
- }));
-
- }
-}
-
-// Test: Smokey ei6500 01097274 NOKEY
-// telegram=|58442515747209010C1A7A8B0000000BFD0F070101046D2A06D82502FD17000082206CD825426CD0238440FF2C000F11008250FD61000082506C01018260FD6100008360FD3100000082606C01018270FD61000082706C0101|
-// {"_":"telegram","media":"smoke detector","meter":"ei6500","name":"Smokey","id":"01097274","status":"OK","last_alarm_date":"2000-01-01","alarm_counter":0,"software_version":"010107","message_datetime":"2022-05-24 06:42","duration_removed_h":0,"last_remove_date":"2000-01-01","removed_counter":0,"test_button_last_date":"2000-01-01","test_button_counter":0,"installation_date":"2022-05-24","last_sound_check_date":"2022-03-16","dust_level":"DUST_0","battery_level":"3.00V","obstacle_distance":"","timestamp":"1111-11-11T11:11:11Z"}
-// |Smokey;01097274;OK;2000-01-01;0;1111-11-11 11:11.11
-
-// telegram=|58442515747209010C1A7A8D0000000BFD0F070101046D2E06D82502FD17000082206CD825426CD0238440FF2C000F11008250FD61000082506C01018260FD6100008360FD3100000082606C01018270FD61020082706CD825|
-// {"_":"telegram","media":"smoke detector","meter":"ei6500","name":"Smokey","id":"01097274","status":"OK","last_alarm_date":"2000-01-01","alarm_counter":0,"software_version":"010107","message_datetime":"2022-05-24 06:46","duration_removed_h":0,"last_remove_date":"2000-01-01","removed_counter":0,"test_button_last_date":"2022-05-24","test_button_counter":2,"installation_date":"2022-05-24","last_sound_check_date":"2022-03-16","dust_level":"DUST_0","battery_level":"3.00V","obstacle_distance":"","timestamp":"1111-11-11T11:11:11Z"}
-// |Smokey;01097274;OK;2000-01-01;0;1111-11-11 11:11.11
-
-// telegram=|58442515747209010C1A7A900000000BFD0F070101046D3406D82502FD17000082206CD825426CD0238440FF2C020F11008250FD61010082506CD8258260FD6100008360FD3100000082606C01018270FD61020082706CD825|
-// {"_":"telegram","media":"smoke detector","meter":"ei6500","name":"Smokey","id":"01097274","status":"OK","last_alarm_date":"2022-05-24","alarm_counter":1,"software_version":"010107","message_datetime":"2022-05-24 06:52","duration_removed_h":0,"last_remove_date":"2000-01-01","removed_counter":0,"test_button_last_date":"2022-05-24","test_button_counter":2,"installation_date":"2022-05-24","last_sound_check_date":"2022-03-16","dust_level":"DUST_2","battery_level":"3.00V","obstacle_distance":"","timestamp":"1111-11-11T11:11:11Z"}
-// |Smokey;01097274;OK;2022-05-24;1;1111-11-11 11:11.11
-
-// telegram=|58442515747209010C1A7A940000000BFD0F070101046D0007D82502FD17000082206CD825426CD0238440FF2C420F11008250FD61010082506CD8258260FD6101008360FD3101000082606CD8258270FD61020082706CD825|
-// {"_":"telegram","media":"smoke detector","meter":"ei6500","name":"Smokey","id":"01097274","status":"TAMPER_WHILE_REMOVED","last_alarm_date":"2022-05-24","alarm_counter":1,"software_version":"010107","message_datetime":"2022-05-24 07:00","duration_removed_h":0.016667,"last_remove_date":"2022-05-24","removed_counter":1,"test_button_last_date":"2022-05-24","test_button_counter":2,"installation_date":"2022-05-24","last_sound_check_date":"2022-03-16","dust_level":"DUST_2","battery_level":"3.00V","obstacle_distance":"","timestamp":"1111-11-11T11:11:11Z"}
-// |Smokey;01097274;TAMPER_WHILE_REMOVED;2022-05-24;1;1111-11-11 11:11.11
diff --git a/components/wmbus_common/driver_elf.cpp b/components/wmbus_common/driver_elf.cpp
deleted file mode 100644
index 8a690d88..00000000
--- a/components/wmbus_common/driver_elf.cpp
+++ /dev/null
@@ -1,182 +0,0 @@
-/*
- Copyright (C) 2021-2022 Fredrik Öhrström (gpl-3.0-or-later)
-
- 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 .
-*/
-
-#include"meters_common_implementation.h"
-
-namespace
-{
- struct Driver : public virtual MeterCommonImplementation
- {
- Driver(MeterInfo &mi, DriverInfo &di);
- };
-
- static bool ok = staticRegisterDriver([](DriverInfo&di)
- {
- di.setName("elf");
- di.setDefaultFields("name,id,total_energy_consumption_kwh,current_power_consumption_kw,total_volume_m3,flow_temperature_c,return_temperature_c,external_temperature_c,status,timestamp");
- di.setMeterType(MeterType::HeatMeter);
- di.addMVT(MANUFACTURER_APA, 0x04, 0x40);
- di.setConstructor([](MeterInfo& mi, DriverInfo& di){ return shared_ptr(new Driver(mi, di)); });
- });
-
- Driver::Driver(MeterInfo &mi, DriverInfo &di) : MeterCommonImplementation(mi, di)
- {
- addStringFieldWithExtractorAndLookup(
- "status",
- "Meter status from manufacturer status and tpl status field.",
- PrintProperty::STATUS | PrintProperty::INCLUDE_TPL_STATUS,
- FieldMatcher::build()
- .set(DifVifKey("047F")),
- {
- {
- {
- "ERROR_FLAGS",
- Translate::MapType::BitToString,
- AlwaysTrigger, MaskBits(0xffffffff),
- "OK",
- {
- }
- },
- },
- });
-
-
- addStringFieldWithExtractor(
- "meter_date",
- "The meter date.",
- DEFAULT_PRINT_PROPERTIES,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::Date)
- );
-
- addNumericFieldWithExtractor(
- "total_energy_consumption",
- "The total energy consumption recorded by this meter.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Energy,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyEnergyVIF)
- );
-
- addNumericFieldWithExtractor(
- "current_power_consumption",
- "Current power consumption.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Power,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::PowerW)
- );
-
- addNumericFieldWithExtractor(
- "total_volume",
- "Total volume of heat media.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Volume,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::Volume)
- );
-
- addNumericFieldWithExtractor(
- "total_energy_consumption_at_date",
- "The total energy consumption recorded at the target date.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Energy,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyEnergyVIF)
- .set(StorageNr(1))
- );
-
- addNumericFieldWithExtractor(
- "flow_temperature",
- "The flow temperature.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Temperature,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::FlowTemperature)
- );
-
- addNumericFieldWithExtractor(
- "return_temperature",
- "The return temperature.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Temperature,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::ReturnTemperature)
- );
-
- addNumericFieldWithExtractor(
- "external_temperature",
- "The external temperature.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Temperature,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::ExternalTemperature)
- );
-
- addNumericFieldWithExtractor(
- "operating_time",
- "How long the meter has been collecting data.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Time,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::OperatingTime)
- );
-
- addStringFieldWithExtractor(
- "version",
- "version.",
- DEFAULT_PRINT_PROPERTIES,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::ModelVersion)
- );
-
- addNumericFieldWithExtractor(
- "battery",
- "Battery voltage.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Voltage,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::Voltage)
- );
-
- }
-}
-
-// Test: Hetta elf 01885619 NOKEY
-// telegram=|51440186010905001837721956880101064004DA000020026CA9220E017799241103000C13641320000A2D00000A5A90060A5E800544050E77000001FD0C010A6564370AFD4731030A274907047F00000002|
-// {"_":"telegram","media":"heat","meter":"elf","name":"Hetta","id":"01885619","status":"ERROR_FLAGS_2000000","meter_date":"2021-02-09","total_energy_consumption_kwh":3112.49977,"current_power_consumption_kw":0,"total_volume_m3":201.364,"total_energy_consumption_at_date_kwh":3047.8,"flow_temperature_c":69,"return_temperature_c":58,"external_temperature_c":37.64,"operating_time_h":17976,"version":"01","battery_v":3.31,"timestamp":"1111-11-11T11:11:11Z"}
-// |Hetta;01885619;3112.49977;0;201.364;69;58;37.64;ERROR_FLAGS_2000000;1111-11-11 11:11.11
diff --git a/components/wmbus_common/driver_em24.cpp b/components/wmbus_common/driver_em24.cpp
deleted file mode 100644
index e1c67a21..00000000
--- a/components/wmbus_common/driver_em24.cpp
+++ /dev/null
@@ -1,267 +0,0 @@
-/*
- Copyright (C) 2017-2022 Fredrik Öhrström (gpl-3.0-or-later)
-
- 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 .
-*/
-
-#include"meters_common_implementation.h"
-
-namespace
-{
- struct Driver : public virtual MeterCommonImplementation
- {
- Driver(MeterInfo &mi, DriverInfo &di);
- };
-
- static bool ok = staticRegisterDriver([](DriverInfo&di)
- {
- di.setName("em24");
- di.setDefaultFields(
- "name,id,"
- "total_energy_consumption_kwh,total_energy_production_kwh,"
- "total_reactive_energy_consumption_kvarh,total_reactive_energy_production_kvarh,"
- "total_apparent_energy_consumption_kvah,total_apparent_energy_production_kvah,"
- "timestamp");
- di.setMeterType(MeterType::ElectricityMeter);
- di.addLinkMode(LinkMode::C1);
- di.addMVT(MANUFACTURER_KAM, 0x02, 0x33);
- di.addMVT(MANUFACTURER_GAV, 0x02, 0x00);
- di.setConstructor([](MeterInfo& mi, DriverInfo& di){ return shared_ptr(new Driver(mi, di)); });
- });
-
- Driver::Driver(MeterInfo &mi, DriverInfo &di) : MeterCommonImplementation(mi, di)
- {
- addStringFieldWithExtractorAndLookup(
- "status",
- "Status of meter.",
- DEFAULT_PRINT_PROPERTIES | PrintProperty::STATUS,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::ErrorFlags),
- {
- {
- {
- "ERROR_FLAGS",
- Translate::MapType::BitToString,
- AlwaysTrigger, MaskBits(0xff),
- "OK",
- {
- { 0x01, "V_1_OVERFLOW" },
- { 0x02, "V_2_OVERFLOW" },
- { 0x04, "V_2_OVERFLOW" },
- { 0x08, "I_1_OVERFLOW" },
- { 0x10, "I_2_OVERFLOW" },
- { 0x20, "I_3_OVERFLOW" },
- { 0x40, "FREQUENCY" },
- }
- },
- },
- });
-
- addStringFieldWithExtractorAndLookup(
- "error",
- "Any errors currently being reported, this field is deprecated and replaced by the status field.",
- DEFAULT_PRINT_PROPERTIES | PrintProperty::DEPRECATED,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::ErrorFlags),
- {
- {
- {
- "ERROR_FLAGS",
- Translate::MapType::BitToString,
- AlwaysTrigger, MaskBits(0xff),
- "",
- {
- { 0x01, "V_1_OVERFLOW" },
- { 0x02, "V_2_OVERFLOW" },
- { 0x04, "V_2_OVERFLOW" },
- { 0x08, "I_1_OVERFLOW" },
- { 0x10, "I_2_OVERFLOW" },
- { 0x20, "I_3_OVERFLOW" },
- { 0x40, "FREQUENCY" },
- }
- },
- },
- });
-
- addNumericFieldWithExtractor(
- "total_energy_consumption",
- "The total energy consumption recorded by this meter.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Energy,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyEnergyVIF)
- );
-
- addNumericFieldWithExtractor(
- "power",
- "Power measured by this meter at the moment.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Power,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyPowerVIF)
- );
-
- addNumericFieldWithExtractor(
- "total_energy_production",
- "The total energy backward (production) recorded by this meter.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Energy,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyEnergyVIF)
- .add(VIFCombinable::BackwardFlow)
- );
-
- addNumericFieldWithExtractor(
- "total_reactive_energy_consumption",
- "The reactive total energy consumption recorded by this meter.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Reactive_Energy,
- VifScaling::None, DifSignedness::Signed,
- FieldMatcher::build()
- .set(DifVifKey("04FB8275"))
- );
-
- addNumericFieldWithExtractor(
- "total_reactive_energy_production",
- "The total reactive energy backward (production) recorded by this meter.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Reactive_Energy,
- VifScaling::None, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(DifVifKey("04FB82F53C"))
- );
-
- addNumericFieldWithCalculator(
- "total_apparent_energy_consumption",
- "Calculated: the total apparent energy consumption.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Apparent_Energy,
- "sqrt("
- " (total_energy_consumption_kwh * total_energy_consumption_kwh) "
- " +"
- " (total_reactive_energy_consumption_kvarh * total_reactive_energy_consumption_kvarh)"
- " )");
-
- addNumericFieldWithCalculator(
- "total_apparent_energy_production",
- "Calculated: the total apparent energy production.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Apparent_Energy,
- R"STR(
- sqrt( (total_energy_production_kwh * total_energy_production_kwh) +
- (total_reactive_energy_production_kvarh * total_reactive_energy_production_kvarh) )
- )STR");
-
- addNumericFieldWithExtractor(
- "amperage_at_phase_1",
- "Amperage at phase L1.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Amperage,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(DifVifKey("04FDD9FC01"))
- );
-
- addNumericFieldWithExtractor(
- "amperage_at_phase_2",
- "Amperage at phase L2.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Amperage,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(DifVifKey("04FDD9FC02"))
- );
-
- addNumericFieldWithExtractor(
- "amperage_at_phase_3",
- "Amperage at phase L3.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Amperage,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(DifVifKey("04FDD9FC03"))
- );
-
- addNumericFieldWithExtractor(
- "voltage_at_phase_1",
- "Voltage at phase L1.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Voltage,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(DifVifKey("04FDC8FC01"))
- );
-
- addNumericFieldWithExtractor(
- "voltage_at_phase_2",
- "Voltage at phase L2.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Voltage,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(DifVifKey("04FDC8FC02"))
- );
-
- addNumericFieldWithExtractor(
- "voltage_at_phase_3",
- "Voltage at phase L3.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Voltage,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(DifVifKey("04FDC8FC03"))
- );
-
- addNumericFieldWithExtractor(
- "raw_frequency",
- "Frequency in 0.1 Hz",
- DEFAULT_PRINT_PROPERTIES | PrintProperty::HIDE,
- Quantity::Frequency,
- VifScaling::None, DifSignedness::Signed,
- FieldMatcher::build()
- .set(DifVifKey("02FB2E"))
- );
-
- addNumericFieldWithCalculator(
- "frequency",
- "Frequency of AC.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Frequency,
- "raw_frequency_hz / 10 counter");
- }
-}
-
-// Test: Elen em24 66666666 NOKEY
-// telegram=|35442D2C6666666633028D2070806A0520B4D378_0405F208000004FB82753F00000004853C0000000004FB82F53CCA01000001FD1722|
-// {"_":"telegram","media":"electricity","meter":"em24","name":"Elen","id":"66666666","status":"I_3_OVERFLOW V_2_OVERFLOW","error":"I_3_OVERFLOW V_2_OVERFLOW","total_energy_consumption_kwh":229,"total_energy_production_kwh":0,"total_reactive_energy_consumption_kvarh":63,"total_reactive_energy_production_kvarh":458,"total_apparent_energy_consumption_kvah":237.507895,"total_apparent_energy_production_kvah":458,"frequency_hz":null,"timestamp":"1111-11-11T11:11:11Z"}
-// |Elen;66666666;229;0;63;458;237.507895;458;1111-11-11 11:11.11
-
-// Test: Elen2 em24 02020202 NOKEY
-// telegram=|4144361C0202020200028C209A7A9A0030252F2F_04050100000004FB82750000000004FB82F53C00000000042A0000000001FD17002F2F2F2F2F2F2F2F2F2F2F2F2F|
-// {"_":"telegram","error": "","id": "02020202","media": "electricity","meter": "em24","name": "Elen2","power_kw": 0,"status": "OK","timestamp": "1111-11-11T11:11:11Z","total_apparent_energy_consumption_kvah": 0.1,"total_apparent_energy_production_kvah": null,"total_energy_consumption_kwh": 0.1,"total_reactive_energy_consumption_kvarh": 0,"total_reactive_energy_production_kvarh": 0,"frequency_hz":null}
-// |Elen2;02020202;0.1;null;0;0;0.1;null;1111-11-11 11:11.11
-
-
-// telegram=|8144361C0202020200028C20357A351070252F2F_04050100000004FB82750000000004FB82F53C00000000042A0000000004FB140000000004FB943C0000000004FDD9FC010000000004FDD9FC020000000004FDD9FC030000000004FDC8FC011709000004FDC8FC02EC04000004FDC8FC03EC04000002FB2EF40101FD17002F2F2F|
-// {"_":"telegram","amperage_at_phase_1_a": 0,"amperage_at_phase_2_a": 0,"amperage_at_phase_3_a": 0,"error": "","frequency_hz": 50,"id": "02020202","media": "electricity","meter": "em24","name": "Elen2","power_kw": 0,"status": "OK","timestamp": "1111-11-11T11:11:11Z","total_apparent_energy_consumption_kvah": 0.1,"total_apparent_energy_production_kvah": null,"total_energy_consumption_kwh": 0.1,"total_reactive_energy_consumption_kvarh": 0,"total_reactive_energy_production_kvarh": 0,"voltage_at_phase_1_v": 232.7,"voltage_at_phase_2_v": 126,"voltage_at_phase_3_v": 126}
-// |Elen2;02020202;0.1;null;0;0;0.1;null;1111-11-11 11:11.11
diff --git a/components/wmbus_common/driver_emerlin868.cpp b/components/wmbus_common/driver_emerlin868.cpp
deleted file mode 100644
index 1991ff94..00000000
--- a/components/wmbus_common/driver_emerlin868.cpp
+++ /dev/null
@@ -1,77 +0,0 @@
-/*
- Copyright (C) 2017-2022 Fredrik Öhrström (gpl-3.0-or-later)
- Copyright (C) 2018 David Mallon (gpl-3.0-or-later)
-
- 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 .
-*/
-
-#include"meters_common_implementation.h"
-
-namespace
-{
- struct Driver : public virtual MeterCommonImplementation
- {
- Driver(MeterInfo &mi, DriverInfo &di);
- };
-
- static bool ok = staticRegisterDriver([](DriverInfo&di)
- {
- di.setName("emerlin868");
- di.setDefaultFields("name,id,total_m3,target_m3,timestamp");
- di.setMeterType(MeterType::WaterMeter);
- di.addLinkMode(LinkMode::T1);
- di.addMVT(MANUFACTURER_ELR, 0x37, 0x11);
- di.setConstructor([](MeterInfo& mi, DriverInfo& di){ return shared_ptr(new Driver(mi, di)); });
- });
-
- Driver::Driver(MeterInfo &mi, DriverInfo &di) : MeterCommonImplementation(mi, di)
- {
- addNumericFieldWithExtractor(
- "total",
- "The total water consumption recorded by this meter.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Volume,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::Volume)
- );
-
- addNumericFieldWithExtractor(
- "target",
- "The target water consumption recorded at previous period.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Volume,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::Volume)
- .set(StorageNr(1)));
-
- addStringFieldWithExtractor(
- "target_date",
- "Date when previous period ended.",
- DEFAULT_PRINT_PROPERTIES,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::Date)
- .set(StorageNr(1)));
-
- }
-}
-
-// Test: Vodda emerlin868 95949392 NOKEY
-// telegram=|2E4492159293949511377ABE0020252F2F_04135515000004FD971D80800000441300000000426C000002FDFD02B300|
-// {"_":"telegram","media":"radio converter (meter side)","meter":"emerlin868","name":"Vodda","id":"95949392","total_m3":5.461,"target_m3":0,"target_date":"2000-00-00","timestamp":"1111-11-11T11:11:11Z"}
-// |Vodda;95949392;5.461;0;1111-11-11 11:11.11
diff --git a/components/wmbus_common/driver_enercal.cpp b/components/wmbus_common/driver_enercal.cpp
deleted file mode 100644
index d91b7d25..00000000
--- a/components/wmbus_common/driver_enercal.cpp
+++ /dev/null
@@ -1,248 +0,0 @@
-/*
- Copyright (C) 2022 Fredrik Öhrström (gpl-3.0-or-later)
-
- 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 .
-*/
-
-#include"meters_common_implementation.h"
-
-namespace
-{
- struct Driver : public virtual MeterCommonImplementation {
- Driver(MeterInfo &mi, DriverInfo &di);
- };
-
- static bool ok = staticRegisterDriver([](DriverInfo&di)
- {
- di.setName("enercal");
- di.setDefaultFields("name,id,status,total_kwh,target_kwh,total_m3,target_m3,timestamp");
- di.setMeterType(MeterType::HeatMeter);
- di.addLinkMode(LinkMode::MBUS);
- di.addMVT(MANUFACTURER_GWF, 0x04, 0x08);
- di.setConstructor([](MeterInfo& mi, DriverInfo& di){ return shared_ptr(new Driver(mi, di)); });
- });
-
- Driver::Driver(MeterInfo &mi, DriverInfo &di) : MeterCommonImplementation(mi, di)
- {
- addStringField(
- "status",
- "Meter status.",
- PrintProperty::STATUS | PrintProperty::INCLUDE_TPL_STATUS);
-
- addNumericFieldWithExtractor(
- "total",
- "The total energy consumption recorded by this meter.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Energy,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyEnergyVIF)
- );
-
- addNumericFieldWithExtractor(
- "target",
- "The energy consumption recorded by this meter at the set date.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Energy,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyEnergyVIF)
- .set(StorageNr(1))
- );
-
- addNumericFieldWithExtractor(
- "power",
- "The active power consumption.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Power,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyPowerVIF)
- );
-
- addNumericFieldWithExtractor(
- "flow",
- "The flow of water.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Flow,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::VolumeFlow)
- );
-
- addNumericFieldWithExtractor(
- "flow_max",
- "The maximum forward flow of water since the last set date?",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Flow,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Maximum)
- .set(VIFRange::VolumeFlow)
- );
-
- addNumericFieldWithExtractor(
- "forward",
- "The forward temperature of the water.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Temperature,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::FlowTemperature)
- );
-
- addNumericFieldWithExtractor(
- "return",
- "The return temperature of the water.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Temperature,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::ReturnTemperature)
- );
-
- addNumericFieldWithExtractor(
- "difference",
- "The temperature difference forward-return for the water.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Temperature,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::TemperatureDifference)
- );
-
- addNumericFieldWithExtractor(
- "total",
- "The total amount of water that has passed through this meter.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Volume,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::Volume)
- );
-
- addNumericFieldWithExtractor(
- "target",
- "The amount of water that had passed through this meter at the set date.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Volume,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::Volume)
- .set(StorageNr(1))
- );
-
- addNumericFieldWithExtractor(
- "subunit1",
- "The amount of water that has passed through subunit 1.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Volume,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::Volume)
- .set(SubUnitNr(1))
- );
-
- addNumericFieldWithExtractor(
- "subunit1_target",
- "The amount of water that had passed through the subunit 1 at the set date.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Volume,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::Volume)
- .set(StorageNr(1))
- .set(SubUnitNr(1))
- );
-
- addNumericFieldWithExtractor(
- "subunit1",
- "The current heat cost allocation for subunit 1.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::HCA,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::HeatCostAllocation)
- .set(SubUnitNr(1))
- );
-
- addNumericFieldWithExtractor(
- "subunit1_target",
- "The heat cost allocation for subunit 1 at the target date.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::HCA,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::HeatCostAllocation)
- .set(SubUnitNr(1))
- .set(StorageNr(1))
- );
-
- addNumericFieldWithExtractor(
- "subunit2",
- "The current heat cost allocation for subunit 2.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::HCA,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::HeatCostAllocation)
- .set(SubUnitNr(2))
- );
-
- addNumericFieldWithExtractor(
- "subunit2_target",
- "The heat cost allocation for subunit 2 at the target date.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::HCA,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::HeatCostAllocation)
- .set(SubUnitNr(2))
- .set(StorageNr(1))
- );
-
- addStringFieldWithExtractor(
- "target_date",
- "The most recent billing period date.",
- DEFAULT_PRINT_PROPERTIES,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::Date)
- .set(StorageNr(1))
- );
-
- addOptionalLibraryFields("operating_time_h,on_time_h,meter_datetime");
- }
-}
-
-// Test: Heat enercal 00309924 NOKEY
-// Comment:
-// telegram=|688e8e6808017224993000e61e080406000000040681460600041488f2350084401426e02600025B2600025f220002622a00042247880200042647880200043B00000000042c00000000046d2d0ede2784406e000000008480406e00000000c4800006B1450600c48000143Be43500c4c000142fd22600c4c0006e00000000c480406e00000000c280006cc1271f00000000f416|
-// {"_":"telegram","media":"heat","meter":"enercal","name":"Heat","id":"00309924","status":"OK","total_kwh":411265,"target_kwh":411057,"power_kw":0,"flow_m3h":0,"forward_c":38,"return_c":34,"difference_c":4.2,"total_m3":35354.96,"target_m3":35318.35,"subunit1_m3":25477.5,"subunit1_target_m3":25441.75,"subunit1_hca":0,"subunit1_target_hca":0,"subunit2_hca":0,"subunit2_target_hca":0,"target_date":"2022-07-01","operating_time_h":165959,"on_time_h":165959,"meter_datetime":"2022-07-30 14:45","timestamp":"1111-11-11T11:11:11Z"}
-// |Heat;00309924;OK;411265;411057;35354.96;35318.35;1111-11-11 11:11.11
diff --git a/components/wmbus_common/driver_engelmann_faw.cpp b/components/wmbus_common/driver_engelmann_faw.cpp
deleted file mode 100644
index 253709f3..00000000
--- a/components/wmbus_common/driver_engelmann_faw.cpp
+++ /dev/null
@@ -1,111 +0,0 @@
-/*
- Copyright (C) 2020-2022 Fredrik Öhrström (gpl-3.0-or-later)
-
- 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 .
-*/
-
-#include"meters_common_implementation.h"
-
-namespace
-{
- struct Driver : public virtual MeterCommonImplementation
- {
- Driver(MeterInfo &mi, DriverInfo &di);
- };
-
- static bool ok = staticRegisterDriver([](DriverInfo&di)
- {
- di.setName("engelmann-faw");
- di.setDefaultFields("name,id,status,reporting_date,consumption_at_reporting_date_m3,timestamp");
- di.addLinkMode(LinkMode::T1);
- di.addMVT(MANUFACTURER_EFE, 0x07, 0x00);
- di.setConstructor([](MeterInfo& mi, DriverInfo& di){ return shared_ptr(new Driver(mi, di)); });
- });
-
- Driver::Driver(MeterInfo &mi, DriverInfo &di) :
- MeterCommonImplementation(mi, di)
- {
- addStringFieldWithExtractorAndLookup(
- "status",
- "Status and error flags.",
- DEFAULT_PRINT_PROPERTIES | PrintProperty::STATUS | PrintProperty::INCLUDE_TPL_STATUS,
- FieldMatcher::build()
- .set(VIFRange::ErrorFlags),
- {
- {
- {
- "ERROR_FLAGS",
- Translate::MapType::BitToString,
- AlwaysTrigger, MaskBits(0xff),
- "OK",
- {
- { 0x01, "VOLUME_DETECTION_COILS_DEFECT" },
- { 0x02, "RESET" },
- { 0x04, "CRC_ERROR" },
- { 0x08, "REMOVAL_DETECTED" },
- { 0x10, "MAGNETIC_MANIPULATION" },
- { 0x20, "LEAKAGE" },
- { 0x40, "BLOCKED" },
- { 0x80, "REVERSE_FLOW" },
- }
- },
- },
- });
-
- addStringFieldWithExtractor(
- "reporting_date",
- "The reporting date of the last billing period.",
- DEFAULT_PRINT_PROPERTIES,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::Date)
- .set(StorageNr(1))
- );
-
- addNumericFieldWithExtractor(
- "consumption_at_reporting_date",
- "The water consumption at the last billing period date.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Volume,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::Volume)
- .set(StorageNr(1)));
-
- for (int i=2; i<=16; ++i)
- {
- std::string name, info;
- strprintf(&name, "consumption_%d_months_ago", i-1);
- strprintf(&info, "Water consumption %d month(s) ago.", i-1);
-
- addNumericFieldWithExtractor(
- name,
- info,
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Volume,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::Volume)
- .set(StorageNr(i)));
- }
- }
-}
-
-
-// Test: Wasserzaehler engelmann-faw 43000255 NOKEY
-// telegram=|8f44c5145502004301077260402520c51400076b0000002f2f426cbf2c441322e9000001fd17008401133c340100c40113ae2d010084021303290100c402137e21010084031313180100c403138a0e010084041337060100c40413b2fc00008405139af30000c4051322e90000840613c1df0000c40613cdd5000084071365ce0000c407136dc500008408138dbf0000|
-// {"_":"telegram","media":"water","meter":"engelmann-faw","name":"Wasserzaehler","id":"20254060","status":"OK","reporting_date":"2021-12-31","consumption_at_reporting_date_m3":59.682,"consumption_1_months_ago_m3":78.908,"consumption_2_months_ago_m3":77.23,"consumption_3_months_ago_m3":76.035,"consumption_4_months_ago_m3":74.11,"consumption_5_months_ago_m3":71.699,"consumption_6_months_ago_m3":69.258,"consumption_7_months_ago_m3":67.127,"consumption_8_months_ago_m3":64.69,"consumption_9_months_ago_m3":62.362,"consumption_10_months_ago_m3":59.682,"consumption_11_months_ago_m3":57.281,"consumption_12_months_ago_m3":54.733,"consumption_13_months_ago_m3":52.837,"consumption_14_months_ago_m3":50.541,"consumption_15_months_ago_m3":49.037,"timestamp":"1111-11-11T11:11:11Z"}
-// |Wasserzaehler;20254060;OK;2021-12-31;59.682;1111-11-11 11:11.11
diff --git a/components/wmbus_common/driver_esyswm.cpp b/components/wmbus_common/driver_esyswm.cpp
deleted file mode 100644
index d1d5d52c..00000000
--- a/components/wmbus_common/driver_esyswm.cpp
+++ /dev/null
@@ -1,160 +0,0 @@
-/*
- Copyright (C) 2020-2022 Fredrik Öhrström (gpl-3.0-or-later)
-
- 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 .
-*/
-
-#include"meters_common_implementation.h"
-
-namespace
-{
- struct Driver : public virtual MeterCommonImplementation
- {
- Driver(MeterInfo &mi, DriverInfo &di);
- };
-
- static bool ok = staticRegisterDriver([](DriverInfo&di)
- {
- di.setName("esyswm");
- di.setDefaultFields("name,id,total_energy_consumption_kwh,current_power_consumption_kw,total_energy_production_kwh,total_energy_consumption_tariff1_kwh,total_energy_consumption_tariff2_kwh,current_power_consumption_phase1_kw,current_power_consumption_phase2_kw,current_power_consumption_phase3_kw,enhanced_id,timestamp");
- di.setMeterType(MeterType::ElectricityMeter);
- di.addLinkMode(LinkMode::T1);
- di.addMVT(MANUFACTURER_ESY, 0x37, 0x30); // Wireless adapter for electricity meter.
- di.addMVT(MANUFACTURER_ESY, 0x02, 0x11);
- di.setConstructor([](MeterInfo& mi, DriverInfo& di){ return shared_ptr(new Driver(mi, di)); });
- });
-
- Driver::Driver(MeterInfo &mi, DriverInfo &di) : MeterCommonImplementation(mi, di)
- {
- addOptionalLibraryFields("fabrication_no,enhanced_id,location");
-
- addStringFieldWithExtractor(
- "location_hex",
- "Meter installed at this customer location. Deprecate field.",
- DEFAULT_PRINT_PROPERTIES | PrintProperty::DEPRECATED,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::Location)
- );
-
- addNumericFieldWithExtractor(
- "total_energy_consumption",
- "The total energy consumption recorded by this meter.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Energy,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyEnergyVIF)
- );
-
- addNumericFieldWithExtractor(
- "current_power_consumption",
- "Calculated sum of power consumption of all phases.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Power,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyPowerVIF)
- );
-
- addNumericFieldWithExtractor(
- "total_energy_production",
- "The total energy production recorded by this meter.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Energy,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyEnergyVIF)
- .add(VIFCombinable::BackwardFlow)
- );
-
- addNumericFieldWithExtractor(
- "total_energy_consumption_tariff1",
- "The total energy consumption recorded by this meter on tariff 1.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Energy,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyEnergyVIF)
- .set(TariffNr(1))
- );
-
- addNumericFieldWithExtractor(
- "total_energy_consumption_tariff2",
- "The total energy consumption recorded by this meter on tariff 2.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Energy,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyEnergyVIF)
- .set(TariffNr(2))
- );
-
- addNumericFieldWithExtractor(
- "current_power_consumption_phase1",
- "Current power consumption phase 1.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Power,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(DifVifKey("04A9FF01"))
- );
-
- addNumericFieldWithExtractor(
- "current_power_consumption_phase2",
- "Current power consumption phase 2.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Power,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(DifVifKey("04A9FF02"))
- );
-
- addNumericFieldWithExtractor(
- "current_power_consumption_phase3",
- "Current power consumption phase 3.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Power,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(DifVifKey("04A9FF03"))
- );
-
- addStringFieldWithExtractor(
- "version",
- "Static version information.",
- DEFAULT_PRINT_PROPERTIES,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(DifVifKey("0DFD09"))
- );
-
- }
-}
-
-// Test: Elen2 esyswm 77997799 NOKEY
-// Comment: Static telegram
-// telegram=|7B4479169977997730378C208B900F002C25E4EF0A002EA98E7D58B3ADC57299779977991611028B005087102F2F#0DFD090F34302e3030562030303030303030300D790E31323334353637383839595345310DFD100AAAAAAAAAAAAAAAAAAAAA0D780E31323334353637383930594553312F2F2F2F2F2F2F2F2F2F2F|
-// {"_":"telegram","media":"electricity","meter":"esyswm","name":"Elen2","id":"77997799","fabrication_no":"1SEY0987654321","enhanced_id":"1ESY9887654321","location":"AAAAAAAAAAAAAAAAAAAA","location_hex":"AAAAAAAAAAAAAAAAAAAA","version":"00000000 V00.04","timestamp":"1111-11-11T11:11:11Z"}
-// |Elen2;77997799;null;null;null;null;null;null;null;null;1ESY9887654321;1111-11-11 11:11.11
-
-// Comment: Dynamic telegram
-// telegram=|7B4479169977997730378C20F0900F002C2549EE0A0077C19D3D1A08ABCD729977997779161102F0005007102F2F#0702F5C3FA000000000007823C5407000000000000841004E081020084200415000000042938AB000004A9FF01FA0A000004A9FF02050A000004A9FF03389600002F2F2F2F2F2F2F2F2F2F2F2F2F|
-// {"_":"telegram","media":"electricity","meter":"esyswm","name":"Elen2","id":"77997799","fabrication_no":"1SEY0987654321","enhanced_id":"1ESY9887654321","location":"AAAAAAAAAAAAAAAAAAAA","location_hex":"AAAAAAAAAAAAAAAAAAAA","total_energy_consumption_kwh":1643.4165,"current_power_consumption_kw":0.43832,"total_energy_production_kwh":0.1876,"total_energy_consumption_tariff1_kwh":1643.2,"total_energy_consumption_tariff2_kwh":0.21,"current_power_consumption_phase1_kw":0.0281,"current_power_consumption_phase2_kw":0.02565,"current_power_consumption_phase3_kw":0.38456,"version":"00000000 V00.04","timestamp":"1111-11-11T11:11:11Z"}
-// |Elen2;77997799;1643.4165;0.43832;0.1876;1643.2;0.21;0.0281;0.02565;0.38456;1ESY9887654321;1111-11-11 11:11.11
diff --git a/components/wmbus_common/driver_eurisii.cpp b/components/wmbus_common/driver_eurisii.cpp
deleted file mode 100644
index 3c645ad0..00000000
--- a/components/wmbus_common/driver_eurisii.cpp
+++ /dev/null
@@ -1,144 +0,0 @@
-/*
- Copyright (C) 2019-2023 Fredrik Öhrström (gpl-3.0-or-later)
-
- 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 .
-*/
-
-#include"meters_common_implementation.h"
-
-namespace
-{
- struct Driver : public virtual MeterCommonImplementation
- {
- Driver(MeterInfo &mi, DriverInfo &di);
- };
-
- static bool ok = staticRegisterDriver([](DriverInfo&di)
- {
- di.setName("eurisii");
- di.setDefaultFields("name,id,current_consumption_hca,status,timestamp");
- di.setMeterType(MeterType::HeatCostAllocationMeter);
- di.addLinkMode(LinkMode::T1);
- di.addMVT(MANUFACTURER_INE, 0x08, 0x55);
- di.addMVT(MANUFACTURER_RAM, 0x08, 0x55);
- di.setConstructor([](MeterInfo& mi, DriverInfo& di){ return shared_ptr(new Driver(mi, di)); });
- });
-
- Driver::Driver(MeterInfo &mi, DriverInfo &di) : MeterCommonImplementation(mi, di)
- {
- addStringFieldWithExtractorAndLookup(
- "status",
- "Meter status from error flags and tpl status field.",
- DEFAULT_PRINT_PROPERTIES |
- PrintProperty::STATUS | PrintProperty::INCLUDE_TPL_STATUS,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::ErrorFlags),
- Translate::Lookup(
- {
- {
- {
- "ERROR_FLAGS",
- Translate::MapType::BitToString,
- AlwaysTrigger, MaskBits(0xffff),
- "OK",
- {
- { 0x0001, "MEASUREMENT" },
- { 0x0002, "SABOTAGE" },
- { 0x0004, "BATTERY" },
- { 0x0008, "CS" },
- { 0x0010, "HF" },
- { 0x0020, "RESET" }
- }
- },
- },
- }));
-
- addStringFieldWithExtractorAndLookup(
- "error_flags",
- "Deprecated field! Use status instead.",
- DEFAULT_PRINT_PROPERTIES | PrintProperty::DEPRECATED |
- PrintProperty::STATUS | PrintProperty::INCLUDE_TPL_STATUS,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::ErrorFlags),
- Translate::Lookup(
- {
- {
- {
- "ERROR_FLAGS",
- Translate::MapType::BitToString,
- AlwaysTrigger, MaskBits(0xffff),
- "OK",
- {
- { 0x0001, "MEASUREMENT" },
- { 0x0002, "SABOTAGE" },
- { 0x0004, "BATTERY" },
- { 0x0008, "CS" },
- { 0x0010, "HF" },
- { 0x0020, "RESET" }
- }
- },
- },
- }));
-
- addNumericFieldWithExtractor(
- "current_consumption",
- "The current heat cost allocation.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::HCA,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::HeatCostAllocation)
- );
-
- addNumericFieldWithExtractor(
- "consumption_at_set_date_{storage_counter}",
- "The heat cost allocation at set date #.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::HCA,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::HeatCostAllocation)
- .set(StorageNr(1),StorageNr(17))
- );
-
- addNumericFieldWithExtractor(
- "consumption_at_set_date",
- "Deprecated field.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::HCA,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::HeatCostAllocation)
- .set(StorageNr(1))
- );
-
-
- }
-}
-
-// Test: HeatMeter eurisii 88018801 NOKEY
-// telegram=|7644C52501880188550872_01880188C5255508010000002F2F0B6E332211426E110182016E1102C2016E110382026E1104C2026E110582036E1106C2036E110782046E1108C2046E110982056E1110C2056E111182066E1112C2066E111382076E1114C2076E111582086E1116C2086E111702FD172100|
-// {"_":"telegram","media":"heat cost allocation","meter":"eurisii","name":"HeatMeter","id":"88018801","status":"MEASUREMENT RESET","current_consumption_hca":112233,"consumption_at_set_date_hca":273,"consumption_at_set_date_1_hca":273,"consumption_at_set_date_2_hca":529,"consumption_at_set_date_3_hca":785,"consumption_at_set_date_4_hca":1041,"consumption_at_set_date_5_hca":1297,"consumption_at_set_date_6_hca":1553,"consumption_at_set_date_7_hca":1809,"consumption_at_set_date_8_hca":2065,"consumption_at_set_date_9_hca":2321,"consumption_at_set_date_10_hca":4113,"consumption_at_set_date_11_hca":4369,"consumption_at_set_date_12_hca":4625,"consumption_at_set_date_13_hca":4881,"consumption_at_set_date_14_hca":5137,"consumption_at_set_date_15_hca":5393,"consumption_at_set_date_16_hca":5649,"consumption_at_set_date_17_hca":5905,"error_flags":"MEASUREMENT RESET","timestamp":"1111-11-11T11:11:11Z"}
-// |HeatMeter;88018801;112233;MEASUREMENT RESET;1111-11-11 11:11.11
-
-
-// Test: HeatMeter2 eurisii 60200770 NOKEY
-// telegram=|76442D4870072060550872700720602D485508280060052F2F_0B6E320100426E550082016E3500C2016E1F0082026E1F00C2026E130082036E1300C2036E130082046E1300C2046E120082056E7D01C2056E440182066E0601C2066EB80082076E7F00C2076E320082086E1C00C2086E1C0002FD170000
-// {"_":"telegram","media":"heat cost allocation","meter":"eurisii","name":"HeatMeter2","id":"60200770","status":"OK","error_flags":"OK","current_consumption_hca":132,"consumption_at_set_date_4_hca":31,"consumption_at_set_date_5_hca":19,"consumption_at_set_date_6_hca":19,"consumption_at_set_date_2_hca":53,"consumption_at_set_date_14_hca":127,"consumption_at_set_date_8_hca":19,"consumption_at_set_date_1_hca":85,"consumption_at_set_date_10_hca":381,"consumption_at_set_date_17_hca":28,"consumption_at_set_date_13_hca":184,"consumption_at_set_date_11_hca":324,"consumption_at_set_date_3_hca":31,"consumption_at_set_date_9_hca":18,"consumption_at_set_date_15_hca":50,"consumption_at_set_date_16_hca":28,"consumption_at_set_date_7_hca":19,"consumption_at_set_date_12_hca":262,"consumption_at_set_date_hca":85,"timestamp":"1111-11-11T11:11:11Z"}
-// |HeatMeter2;60200770;132;OK;1111-11-11 11:11.11
diff --git a/components/wmbus_common/driver_ev200.cpp b/components/wmbus_common/driver_ev200.cpp
deleted file mode 100644
index a12035f0..00000000
--- a/components/wmbus_common/driver_ev200.cpp
+++ /dev/null
@@ -1,67 +0,0 @@
-/*
- Copyright (C) 2017-2022 Fredrik Öhrström (gpl-3.0-or-later)
- Copyright (C) 2018 David Mallon (gpl-3.0-or-later)
-
- 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 .
-*/
-
-#include"meters_common_implementation.h"
-
-namespace
-{
- struct Driver : public virtual MeterCommonImplementation
- {
- Driver(MeterInfo &mi, DriverInfo &di);
- };
-
- static bool ok = staticRegisterDriver([](DriverInfo&di)
- {
- di.setName("ev200");
- di.setDefaultFields("name,id,total_m3,target_m3,timestamp");
- di.setMeterType(MeterType::WaterMeter);
- di.addLinkMode(LinkMode::T1);
- di.addMVT(MANUFACTURER_ELR, 0x07, 0x0d);
- di.setConstructor([](MeterInfo& mi, DriverInfo& di){ return shared_ptr(new Driver(mi, di)); });
- });
-
- Driver::Driver(MeterInfo &mi, DriverInfo &di) : MeterCommonImplementation(mi, di)
- {
- addNumericFieldWithExtractor(
- "total",
- "The total water consumption recorded by this meter.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Volume,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::Volume)
- );
-
- addNumericFieldWithExtractor(
- "target",
- "The target water consumption recorded at previous period.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Volume,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::Volume)
- .set(StorageNr(1)));
- }
-}
-
-// Test: Voda ev200 99993030 NOKEY
-// telegram=|2E449215303099990D077AB50820452F2F_0C12495849004C12557545000FB10445007022C50BFFFFFFFF0000FFF000|
-// {"_":"telegram","media":"water","meter":"ev200","name":"Voda","id":"99993030","total_m3":49.5849,"target_m3":45.7555,"timestamp":"1111-11-11T11:11:11Z"}
-// |Voda;99993030;49.5849;45.7555;1111-11-11 11:11.11
diff --git a/components/wmbus_common/driver_evo868.cpp b/components/wmbus_common/driver_evo868.cpp
deleted file mode 100644
index 5440f53e..00000000
--- a/components/wmbus_common/driver_evo868.cpp
+++ /dev/null
@@ -1,193 +0,0 @@
-/*
- Copyright (C) 2017-2022 Fredrik Öhrström (gpl-3.0-or-later)
-
- 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 .
-*/
-
-#include"meters_common_implementation.h"
-
-namespace
-{
- struct Driver : public virtual MeterCommonImplementation
- {
- Driver(MeterInfo &mi, DriverInfo &di);
- };
-
- static bool ok = staticRegisterDriver([](DriverInfo&di)
- {
- di.setName("evo868");
- di.setDefaultFields("name,id,total_m3,current_status,consumption_at_set_date_m3,set_date,timestamp");
- di.setMeterType(MeterType::WaterMeter);
- di.addLinkMode(LinkMode::T1);
- di.setConstructor([](MeterInfo& mi, DriverInfo& di){ return shared_ptr(new Driver(mi, di)); });
- di.addMVT(MANUFACTURER_MAD, 0x06, 0x50);
- di.addMVT(MANUFACTURER_MAD, 0x07, 0x50);
- di.addMVT(MANUFACTURER_MAD, 0x16, 0x50);
-
- });
-
- Driver::Driver(MeterInfo &mi, DriverInfo &di) : MeterCommonImplementation(mi, di)
- {
- addStringFieldWithExtractorAndLookup(
- "current_status",
- "Status of meter.",
- DEFAULT_PRINT_PROPERTIES
- | PrintProperty::STATUS | PrintProperty::INCLUDE_TPL_STATUS,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::ErrorFlags),
- {
- {
- {
- "ERROR_FLAGS",
- Translate::MapType::BitToString,
- AlwaysTrigger, MaskBits(0xffff),
- "OK",
- {
- // { 0x01 , "WOOT" },
- }
- },
- },
- });
-
- addOptionalLibraryFields("fabrication_no");
- addOptionalLibraryFields("total_m3");
-
- addNumericFieldWithExtractor(
- "consumption_at_set_date",
- "The total water consumption at the most recent billing period date.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Volume,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::Volume)
- .set(StorageNr(1))
- );
-
- addStringFieldWithExtractor(
- "set_date",
- "The most recent billing period date.",
- DEFAULT_PRINT_PROPERTIES,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::Date)
- .set(StorageNr(1))
- );
-
- addNumericFieldWithExtractor(
- "consumption_at_set_date_2",
- "The total water consumption at the second most recent billing period date.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Volume,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::Volume)
- .set(StorageNr(2))
- );
-
- addStringFieldWithExtractor(
- "set_date_2",
- "The second most recent billing period date.",
- DEFAULT_PRINT_PROPERTIES,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::Date)
- .set(StorageNr(2))
- );
-
- addNumericFieldWithExtractor(
- "max_flow_since_datetime",
- "Maximum water flow since date time.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Flow,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Maximum)
- .set(VIFRange::VolumeFlow)
- .set(StorageNr(3))
- );
-
- addStringFieldWithExtractor(
- "max_flow_datetime",
- "The datetime to which maximum flow is measured.",
- DEFAULT_PRINT_PROPERTIES,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::DateTime)
- .set(StorageNr(3))
- );
-
- addNumericFieldWithExtractor(
- "consumption_at_history_{storage_counter-7counter}",
- "The total water consumption at the historic date.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Volume,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::Volume)
- .set(StorageNr(8),StorageNr(19))
- );
-
- addNumericFieldWithExtractor(
- "history_reference",
- "Reference date for history.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::PointInTime,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::Date)
- .set(StorageNr(8)),
- Unit::DateLT
- );
-
- addNumericFieldWithCalculatorAndMatcher(
- "history_{storage_counter-7counter}",
- "The historic date #.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::PointInTime,
- "history_reference_date - ((storage_counter-8counter) * 1 month)",
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::Volume)
- .set(StorageNr(8),StorageNr(19)),
- Unit::DateLT
- );
-
- addStringFieldWithExtractor(
- "device_date_time",
- "Date and time when the meter sent the telegram.",
- DEFAULT_PRINT_PROPERTIES,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::DateTime)
- );
- }
-
-}
-
-// Test: Votchka evo868 79787776 NOKEY
-// Comment: Test Maddalena EVO 868 wmbus module on water meter
-// telegram=|aa4424347677787950077ac10000202f2f041306070000046d1e31b12104fd17000000000e787880048120004413c9040000426c9f2c840113c904000082016c9f2cd3013b9a0200c4016d0534a7218104fd280182046c9f2c840413c9040000c404131b00000084051300000000c405130000000084061300000000c406130000000084071300000000c407130000000084081300000000c408130000000084091300000000c4091300000000ffff|
-// {"_":"telegram","media":"water","meter":"evo868","name":"Votchka","id":"79787776","total_m3":1.798,"device_date_time":"2021-01-17 17:30","current_status":"OK","fabrication_no":"002081048078","consumption_at_set_date_m3":1.225,"set_date":"2020-12-31","consumption_at_set_date_2_m3":1.225,"set_date_2":"2020-12-31","max_flow_since_datetime_m3h":0.666,"max_flow_datetime":"2021-01-07 20:05","history_reference_date":"2020-12-31","consumption_at_history_1_m3":1.225,"history_1_date":"2020-12-31","consumption_at_history_2_m3":0.027,"history_2_date":"2020-11-30","consumption_at_history_3_m3":0,"history_3_date":"2020-10-31","consumption_at_history_4_m3":0,"history_4_date":"2020-09-30","consumption_at_history_5_m3":0,"history_5_date":"2020-08-31","consumption_at_history_6_m3":0,"history_6_date":"2020-07-31","consumption_at_history_7_m3":0,"history_7_date":"2020-06-30","consumption_at_history_8_m3":0,"history_8_date":"2020-05-31","consumption_at_history_9_m3":0,"history_9_date":"2020-04-30","consumption_at_history_10_m3":0,"history_10_date":"2020-03-31","consumption_at_history_11_m3":0,"history_11_date":"2020-02-29","consumption_at_history_12_m3":0,"history_12_date":"2020-01-31","timestamp":"1111-11-11T11:11:11Z"}
-// |Votchka;79787776;1.798;OK;1.225;2020-12-31;1111-11-11 11:11.11
-
-// Test: Wasser evo868 21031894 NOKEY
-// Comment: Test Maddalena EVO 868 wmbus module on water meter
-// telegram=|AC4424349418032150167A39000020_2F2F041356850000046D0A340B3C04FD17000000000E786858000000004413D0620000426CFF2C8401137A83000082016C1E3BD3013BCF0200C4016D09210E338104FD280182046C1E3B8404137A830000C40413BE7E00008405131A7B0000C405136B7600008406132A700000C40613ED6A00008407130F690000C40713536800008408136E670000C40813EE65000084091330640000C40913D0620000|
-// {"_":"telegram","media":"cold water","meter":"evo868","name":"Wasser","id":"21031894","consumption_at_history_1_m3":33.658,"consumption_at_history_10_m3":26.094,"consumption_at_history_11_m3":25.648,"consumption_at_history_12_m3":25.296,"consumption_at_history_2_m3":32.446,"consumption_at_history_3_m3":31.514,"consumption_at_history_4_m3":30.315,"consumption_at_history_5_m3":28.714,"consumption_at_history_6_m3":27.373,"consumption_at_history_7_m3":26.895,"consumption_at_history_8_m3":26.707,"consumption_at_history_9_m3":26.478,"consumption_at_set_date_m3":25.296,"consumption_at_set_date_2_m3":33.658,"history_1_date":"2024-11-30","history_10_date":"2024-02-29","history_11_date":"2024-01-31","history_12_date":"2023-12-31","history_2_date":"2024-10-31","history_3_date":"2024-09-30","history_4_date":"2024-08-31","history_5_date":"2024-07-31","history_6_date":"2024-06-30","history_7_date":"2024-05-31","history_8_date":"2024-04-30","history_9_date":"2024-03-31","history_reference_date":"2024-11-30","max_flow_since_datetime_m3h":0.719,"total_m3":34.134,"current_status":"OK","device_date_time":"2024-12-11 20:10","fabrication_no":"000000005868","max_flow_datetime":"2024-03-14 01:09","set_date":"2023-12-31","set_date_2":"2024-11-30","timestamp":"1111-11-11T11:11:11Z"}
-// |Wasser;21031894;34.134;OK;25.296;2023-12-31;1111-11-11 11:11.11
diff --git a/components/wmbus_common/driver_fhkvdataiii.cpp b/components/wmbus_common/driver_fhkvdataiii.cpp
deleted file mode 100644
index 362987a3..00000000
--- a/components/wmbus_common/driver_fhkvdataiii.cpp
+++ /dev/null
@@ -1,227 +0,0 @@
-/*
- Copyright (C) 2019-2022 Fredrik Öhrström (gpl-3.0-or-later)
-
- 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 .
-*/
-
-#include"meters_common_implementation.h"
-
-namespace
-{
- struct Driver : public virtual MeterCommonImplementation
- {
- Driver(MeterInfo &mi, DriverInfo &di);
-
- private:
-
- void processContent(Telegram *t);
- std::string leadingZeroString(int num);
- };
-
- static bool ok = staticRegisterDriver([](DriverInfo&di)
- {
- di.setName("fhkvdataiii");
- di.setDefaultFields("name,id,current_hca,current_date,previous_hca,previous_date,temp_room_c,temp_radiator_c,timestamp");
- di.setMeterType(MeterType::HeatCostAllocationMeter);
- di.addLinkMode(LinkMode::T1);
- di.addMVT(MANUFACTURER_TCH, 0x80, 0x69);
- di.addMVT(MANUFACTURER_TCH, 0x80, 0x94);
- di.usesProcessContent();
- di.setConstructor([](MeterInfo& mi, DriverInfo& di){ return shared_ptr(new Driver(mi, di)); });
- });
-
- Driver::Driver(MeterInfo &mi, DriverInfo &di) : MeterCommonImplementation(mi, di)
- {
- addNumericField("current",
- Quantity::HCA,
- DEFAULT_PRINT_PROPERTIES,
- "Energy consumption so far in this billing period.");
-
- addStringField("current_date",
- "Date of current billing period.",
- DEFAULT_PRINT_PROPERTIES);
-
- addNumericField("previous",
- Quantity::HCA,
- DEFAULT_PRINT_PROPERTIES,
- "Energy consumption in previous billing period.");
-
- addStringField("previous_date",
- "Date of last billing period.",
- DEFAULT_PRINT_PROPERTIES);
-
- addNumericField("temp_room",
- Quantity::Temperature,
- DEFAULT_PRINT_PROPERTIES,
- "Current room temperature.");
-
- addNumericField("temp_radiator",
- Quantity::Temperature,
- DEFAULT_PRINT_PROPERTIES,
- "Current radiator temperature.");
- }
-
- void Driver::processContent(Telegram *t)
- {
- // The Techem FHKV data ii/iii is mostly a proprietary protocol
- // simple wrapped inside a wmbus telegram since the ci-field is 0xa0.
- // Which means that the entire payload is manufacturer specific.
-
- std::vector content;
-
- t->extractPayload(&content);
-
- if (content.size() < 14)
- {
- // Not enough data.
- debugPayload("(fhkvdataiii) not enough data", content);
- return;
- }
-
-
- // Previous Date ////////////////////////////////////////////////////////
-
- uchar date_prev_lo = content[1];
- uchar date_prev_hi = content[2];
- int date_prev = (256.0*date_prev_hi+date_prev_lo);
-
- int day_prev = (date_prev >> 0) & 0x1F;
- int month_prev = (date_prev >> 5) & 0x0F;
- int year_prev = 2000 + ((date_prev >> 9) & 0x3F);
-
- std::string previous_date =
- std::to_string(year_prev) + "-" +
- leadingZeroString(month_prev) + "-" +
- leadingZeroString(day_prev) + "T02:00:00Z";
-
- setStringValue("previous_date", previous_date, NULL);
-
- std::string bytes = tostrprintf("%02x%02x", content[1], content[2]);
- std::string info = "*** "+bytes+" previous_date = %s";
-
- t->addSpecialExplanation(t->header_size+1, 2, KindOfData::CONTENT, Understanding::FULL,
- info.c_str(), previous_date.c_str());
-
- // Previous Consumption //////////////////////////////////////////////////////////
-
- uchar prev_lo = content[3];
- uchar prev_hi = content[4];
- double prev = (256.0*prev_hi+prev_lo);
- double previous_hca = prev;
- setNumericValue("previous", Unit::HCA, previous_hca);
-
- bytes = tostrprintf("%02x%02x", content[3], content[4]);
- info = "*** "+bytes+" previous_hca = %.17g";
-
- t->addSpecialExplanation(t->header_size+3, 2, KindOfData::CONTENT, Understanding::FULL,
- info.c_str(), previous_hca);
-
- // Current Date //////////////////////////////////////////////////////////////////
-
- uchar date_curr_lo = content[5];
- uchar date_curr_hi = content[6];
- int date_curr = (256.0*date_curr_hi+date_curr_lo);
-
- int day_curr = (date_curr >> 4) & 0x1F;
- if (day_curr <= 0) day_curr = 1;
- int month_curr = (date_curr >> 9) & 0x0F;
- if (month_curr <= 0) month_curr = 12;
- int year_curr = year_prev;
- if (month_curr < month_prev || (month_curr == month_prev && day_curr <= day_prev)) {
- year_curr++;
- }
-
- std::string current_date =
- std::to_string(year_curr) + "-" +
- leadingZeroString(month_curr) + "-" +
- leadingZeroString(day_curr) + "T02:00:00Z";
-
- setStringValue("current_date", current_date, NULL);
-
- bytes = tostrprintf("%02x%02x", content[5], content[6]);
- info = "*** "+bytes+" current_date = %s";
-
- t->addSpecialExplanation(t->header_size+5, 2, KindOfData::CONTENT, Understanding::FULL,
- info.c_str(), current_date.c_str());
-
- // Current Consumption ///////////////////////////////////////////////////////////
-
- uchar curr_lo = content[7];
- uchar curr_hi = content[8];
- double current_hca = (256.0*curr_hi+curr_lo);
-
- setNumericValue("current", Unit::HCA, current_hca);
-
- bytes = tostrprintf("%02x%02x", content[7], content[8]);
- info = "*** "+bytes+" current_hca = %.17g";
-
- t->addSpecialExplanation(t->header_size+7, 2, KindOfData::CONTENT, Understanding::FULL,
- info.c_str(), current_hca);
-
- // Temperatures //////////////////////////////////////////////
-
- uchar room_tlo;
- uchar room_thi;
- uchar radiator_tlo;
- uchar radiator_thi;
- int offset = 9;
-
- if(t->dll_version == 0x94)
- {
- offset= 10;
- }
-
- room_tlo = content[offset];
- room_thi = content[offset+1];
- radiator_tlo = content[offset+2];
- radiator_thi = content[offset+3];
-
- // Room Temperature ////////////////////////////////////////////////////////////////
-
- double temp_room_c = (256.0*room_thi+room_tlo)/100;
- setNumericValue("temp_room", Unit::C, temp_room_c);
-
- bytes = tostrprintf("%02x%02x", content[offset], content[offset+1]);
- info = "*** "+bytes+" temp_room_c = %.17g";
-
- t->addSpecialExplanation(t->header_size+offset, 2, KindOfData::CONTENT, Understanding::FULL,
- info.c_str(), temp_room_c);
-
- // Radiator Temperature ////////////////////////////////////////////////////////////
-
- double temp_radiator_c = (256.0*radiator_thi+radiator_tlo)/100;
- setNumericValue("temp_radiator", Unit::C, temp_radiator_c);
-
- bytes = tostrprintf("%02x%02x", content[offset+2], content[offset+3]);
- info = "*** "+bytes+" temp_radiator_c = %.17g";
-
- t->addSpecialExplanation(t->header_size+offset+2, 2, KindOfData::CONTENT, Understanding::FULL,
- info.c_str(), temp_radiator_c);
- }
-
- std::string Driver::leadingZeroString(int num) {
- std::string new_num = (num < 10 ? "0": "") + std::to_string(num);
- return new_num;
- }
-}
-
-// Test: Room fhkvdataiii 11776622 NOKEY
-// telegram=|31446850226677116980A0119F27020480048300C408F709143C003D341A2B0B2A0707000000000000062D114457563D71A1850000|
-// {"_":"telegram","media":"heat cost allocator","meter":"fhkvdataiii","name":"Room","id":"11776622","current_hca":131,"current_date":"2020-02-08T02:00:00Z","previous_hca":1026,"previous_date":"2019-12-31T02:00:00Z","temp_room_c":22.44,"temp_radiator_c":25.51,"timestamp":"1111-11-11T11:11:11Z"}
-// |Room;11776622;131;2020-02-08T02:00:00Z;1026;2019-12-31T02:00:00Z;22.44;25.51;1111-11-11 11:11.11
-
-// Test: Rooom fhkvdataiii 11111234 NOKEY
-// telegram=|33446850341211119480A2_0F9F292D005024040011BD08380904000000070000000000000000000000000001000000000003140E|
-// {"_":"telegram","media":"heat cost allocator","meter":"fhkvdataiii","name":"Rooom","id":"11111234","current_hca":4,"current_date":"2021-02-05T02:00:00Z","previous_hca":45,"previous_date":"2020-12-31T02:00:00Z","temp_room_c":22.37,"temp_radiator_c":23.6,"timestamp":"1111-11-11T11:11:11Z"}
-// |Rooom;11111234;4;2021-02-05T02:00:00Z;45;2020-12-31T02:00:00Z;22.37;23.6;1111-11-11 11:11.11
diff --git a/components/wmbus_common/driver_fhkvdataiv.cpp b/components/wmbus_common/driver_fhkvdataiv.cpp
deleted file mode 100644
index adcb0826..00000000
--- a/components/wmbus_common/driver_fhkvdataiv.cpp
+++ /dev/null
@@ -1,123 +0,0 @@
-/*
- Copyright (C) 2019-2022 Fredrik Öhrström (gpl-3.0-or-later)
-
- 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 .
-*/
-
-
-#include"meters_common_implementation.h"
-
-namespace
-{
- struct Driver : public virtual MeterCommonImplementation
- {
- Driver(MeterInfo &mi, DriverInfo &di);
- };
-
- static bool ok = staticRegisterDriver([](DriverInfo&di)
- {
- di.setName("fhkvdataiv");
- di.setDefaultFields("name,id,current_consumption_hca,set_date,consumption_at_set_date_hca,timestamp");
- di.setMeterType(MeterType::HeatCostAllocationMeter);
- di.addLinkMode(LinkMode::T1);
- di.addMVT(MANUFACTURER_TCH, 0x08, 0x69);
- di.addMVT(MANUFACTURER_TCH, 0x08, 0x94);
-
- di.setConstructor([](MeterInfo& mi, DriverInfo& di){ return shared_ptr(new Driver(mi, di)); });
- });
-
- Driver::Driver(MeterInfo &mi, DriverInfo &di) : MeterCommonImplementation(mi, di)
- {
- addStringField(
- "status",
- "Meter status from tpl status field.",
- DEFAULT_PRINT_PROPERTIES |
- PrintProperty::STATUS | PrintProperty::INCLUDE_TPL_STATUS);
-
- addNumericFieldWithExtractor(
- "current_consumption",
- "The current heat cost allocation.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::HCA,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- );
-
- addStringFieldWithExtractor(
- "set_date",
- "The most recent billing period date.",
- DEFAULT_PRINT_PROPERTIES,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::Date)
- .set(StorageNr(1)));
-
- addNumericFieldWithExtractor(
- "consumption_at_set_date",
- "Heat cost allocation at the most recent billing period date.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::HCA,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(StorageNr(1))
- );
-
- addStringFieldWithExtractor(
- "set_date_1",
- "The most recent billing period date.",
- DEFAULT_PRINT_PROPERTIES,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::Date)
- .set(StorageNr(1)));
-
- addNumericFieldWithExtractor(
- "consumption_at_set_date_1",
- "Heat cost allocation at the most recent billing period date.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::HCA,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(StorageNr(1))
- );
-
- addStringFieldWithExtractor(
- "set_date_8",
- "The 8 billing period date.",
- DEFAULT_PRINT_PROPERTIES,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::Date)
- .set(StorageNr(8)));
-
- addNumericFieldWithExtractor(
- "consumption_at_set_date_8",
- "Heat cost allocation at the 8 billing period date.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::HCA,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(StorageNr(8))
- );
- }
-}
-
-// Test: Rooom fhkvdataiv 14542076 FCF41938F63432975B52505F547FCEDF
-// telegram=|4E4468507620541494087AAD004005089D86B62A329B3439873999738F82461ABDE3C7AC78692B363F3B41EB68607F9C9160F550769B065B6EA00A2E44346E29FF5DC5CB86283C69324AD33D137F6F|
-// {"_":"telegram","media":"heat cost allocation","meter":"fhkvdataiv","name":"Rooom","id":"14542076","status":"OK","current_consumption_hca":2,"set_date":"2020-12-31","consumption_at_set_date_hca":25,"set_date_1":"2020-12-31","consumption_at_set_date_1_hca":25,"set_date_8":"2019-10-31","consumption_at_set_date_8_hca":0,"timestamp":"1111-11-11T11:11:11Z"}
-// |Rooom;14542076;2;2020-12-31;25;1111-11-11 11:11.11
diff --git a/components/wmbus_common/driver_flowiq2200.cpp b/components/wmbus_common/driver_flowiq2200.cpp
deleted file mode 100644
index a3377d4f..00000000
--- a/components/wmbus_common/driver_flowiq2200.cpp
+++ /dev/null
@@ -1,331 +0,0 @@
-/*
- Copyright (C) 2017-2022 Fredrik Öhrström (gpl-3.0-or-later)
-
- 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 .
-*/
-
-#include"meters_common_implementation.h"
-
-namespace
-{
- struct Driver : public virtual MeterCommonImplementation {
- Driver(MeterInfo &mi, DriverInfo &di);
- };
-
- static bool ok = staticRegisterDriver([](DriverInfo&di)
- {
- di.setName("flowiq2200");
- di.setDefaultFields("name,id,status,total_m3,target_m3,timestamp");
-
- di.setMeterType(MeterType::WaterMeter);
- di.addLinkMode(LinkMode::C1);
- // FlowIQ2200
- di.addMVT(MANUFACTURER_KAW, 0x16, 0x3a);
- di.addMVT(MANUFACTURER_KAW, 0x16, 0x3c);
- // FlowIQ3100
- di.addMVT(MANUFACTURER_KAM, 0x16, 0x1d);
-
- di.setConstructor([](MeterInfo& mi, DriverInfo& di){ return shared_ptr(new Driver(mi, di)); });
- });
-
- Driver::Driver(MeterInfo &mi, DriverInfo &di) : MeterCommonImplementation(mi, di)
- {
- addStringFieldWithExtractorAndLookup(
- "status",
- "Status of meter. Not fully understood!",
- DEFAULT_PRINT_PROPERTIES | PrintProperty::STATUS,
- FieldMatcher::build()
- .set(DifVifKey("04FF23")),
- {
- {
- {
- "ERROR_FLAGS",
- Translate::MapType::BitToString,
- AlwaysTrigger, MaskBits(0xffffffff),
- "OK",
- {
- /* Maybe these are the same as the multical21 but we do not know!
- And there are more bits here. */
- { 0x01 , "DRY" },
- { 0x02 , "REVERSE" },
- { 0x04 , "LEAK" },
- { 0x08 , "BURST" },
- }
- },
- },
- });
-
- addNumericFieldWithExtractor(
- "total",
- "The total water consumption recorded by this meter.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Volume,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::Volume)
- );
-
- addNumericFieldWithExtractor(
- "target",
- "The total water consumption recorded at the beginning of this month.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Volume,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::Volume)
- .set(StorageNr(1))
- );
-
- addStringFieldWithExtractor(
- "target_date",
- "The date at the beginning of this month.",
- DEFAULT_PRINT_PROPERTIES,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::Date)
- .set(StorageNr(1))
- );
-
- addNumericFieldWithExtractor(
- "flow",
- "The current flow of water through the meter.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Flow,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::VolumeFlow)
- );
-
- addNumericFieldWithExtractor(
- "min_flow_temperature",
- "The water temperature.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Temperature,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Minimum)
- .set(VIFRange::FlowTemperature)
- .set(StorageNr(2))
- );
-
- addNumericFieldWithExtractor(
- "max_flow_temperature",
- "The maximum water temperature.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Temperature,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Maximum)
- .set(VIFRange::FlowTemperature)
- .set(StorageNr(2))
- );
-
- addNumericFieldWithExtractor(
- "min_external_temperature",
- "The external temperature outside of the meter.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Temperature,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Minimum)
- .set(VIFRange::ExternalTemperature)
- .set(StorageNr(2))
- );
-
- addNumericFieldWithExtractor(
- "max_flow",
- "The maximum flow recorded during previous period.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Flow,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Maximum)
- .set(VIFRange::VolumeFlow)
- .set(StorageNr(2))
- );
-
- addNumericFieldWithExtractor(
- "min_flow",
- "The minimum flow recorded during previous period.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Flow,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Minimum)
- .set(VIFRange::VolumeFlow)
- .set(StorageNr(2))
- );
-
- addNumericFieldWithExtractor(
- "max_external_temperature",
- "The maximum temperature recorded during previous period.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Temperature,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Maximum)
- .set(VIFRange::ExternalTemperature)
- .set(StorageNr(1))
- );
-
- addNumericFieldWithExtractor(
- "min_external_temperature",
- "The minimum flow recorded during previous period.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Temperature,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Minimum)
- .set(VIFRange::ExternalTemperature)
- .set(StorageNr(1))
- );
-
- addNumericFieldWithExtractor(
- "max_flow",
- "The maximum flow recorded during previous period.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Flow,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Maximum)
- .set(VIFRange::VolumeFlow)
- .set(StorageNr(1))
- );
-
- addStringFieldWithExtractorAndLookup(
- "time_dry",
- "Amount of time the meter has been dry.",
- DEFAULT_PRINT_PROPERTIES,
- FieldMatcher::build()
- .set(DifVifKey("02FF20")),
- {
- {
- {
- "DRY",
- Translate::MapType::IndexToString,
- AlwaysTrigger, MaskBits(0x0070),
- "",
- {
- { 0x0000, "" },
- { 0x0010, "1-8 hours" },
- { 0x0020, "9-24 hours" },
- { 0x0030, "2-3 days" },
- { 0x0040, "4-7 days" },
- { 0x0050, "8-14 days" },
- { 0x0060, "15-21 days" },
- { 0x0070, "22-31 days" },
- },
- },
- },
- });
-
- addStringFieldWithExtractorAndLookup(
- "time_reversed",
- "Amount of time the meter has been reversed.",
- DEFAULT_PRINT_PROPERTIES,
- FieldMatcher::build()
- .set(DifVifKey("02FF20")),
- {
- {
- {
- "REVERSED",
- Translate::MapType::IndexToString,
- AlwaysTrigger, MaskBits(0x0380),
- "",
- {
- { 0x0000, "" },
- { 0x0080, "1-8 hours" },
- { 0x0100, "9-24 hours" },
- { 0x0180, "2-3 days" },
- { 0x0200, "4-7 days" },
- { 0x0280, "8-14 days" },
- { 0x0300, "15-21 days" },
- { 0x0380, "22-31 days" },
- },
- },
- },
- });
-
- addStringFieldWithExtractorAndLookup(
- "time_leaking",
- "Amount of time the meter has been leaking.",
- DEFAULT_PRINT_PROPERTIES,
- FieldMatcher::build()
- .set(DifVifKey("02FF20")),
- {
- {
- {
- "LEAKING",
- Translate::MapType::IndexToString,
- AlwaysTrigger, MaskBits(0x1c00),
- "",
- {
- { 0x0000, "" },
- { 0x0400, "1-8 hours" },
- { 0x0800, "9-24 hours" },
- { 0x0c00, "2-3 days" },
- { 0x1000, "4-7 days" },
- { 0x1400, "8-14 days" },
- { 0x1800, "15-21 days" },
- { 0x1c00, "22-31 days" },
- },
- },
- },
- });
-
- addStringFieldWithExtractorAndLookup(
- "time_bursting",
- "Amount of time the meter has been bursting.",
- DEFAULT_PRINT_PROPERTIES,
- FieldMatcher::build()
- .set(DifVifKey("02FF20")),
- {
- {
- {
- "BURSTING",
- Translate::MapType::IndexToString,
- AlwaysTrigger, MaskBits(0xe000),
- "",
- {
- { 0x0000, "" },
- { 0x2000, "1-8 hours" },
- { 0x4000, "9-24 hours" },
- { 0x6000, "2-3 days" },
- { 0x8000, "4-7 days" },
- { 0xa000, "8-14 days" },
- { 0xc000, "15-21 days" },
- { 0xe000, "22-31 days" },
- },
- },
- },
- });
-
- }
-}
-
-// Test: VATTEN flowiq2200 52525252 NOKEY
-// telegram=|4D44372C525252523A168D203894DF7920F93278_04FF23000000000413AEAC0000441364A80000426C812A023B000092013BEF01A2013B000006FF1B067000097000A1015B0C91015B14A1016713|
-// {"_":"telegram","media":"cold water","meter":"flowiq2200","name":"VATTEN","id":"52525252","status":"OK","total_m3":44.206,"target_m3":43.108,"target_date":"2020-10-01","flow_m3h":0,"min_flow_temperature_c":12,"max_flow_temperature_c":20,"min_external_temperature_c":19,"max_flow_m3h":0.495,"min_flow_m3h":0,"timestamp":"1111-11-11T11:11:11Z"}
-// |VATTEN;52525252;OK;44.206;43.108;1111-11-11 11:11.11
-
-// Test: Votten flowiq2200 23813076 NOKEY
-// telegram=|4744372C763081233C168D2056D1ED11205A3C78_04FF2300080000441300000000523B000006FF1B08900008F0FF426CC12B61670A51671B023B000004131F0F00008101E7FF0F13|
-// {"_":"telegram","flow_m3h": 0,"id":"23813076","max_external_temperature_c": 27,"max_flow_m3h": 0,"media":"cold water","meter":"flowiq2200","min_external_temperature_c": 10,"name":"Votten","status":"ERROR_FLAGS_800","target_date":"2022-11-01","target_m3": 0,"timestamp":"1111-11-11T11:11:11Z","total_m3": 3.871}
-// |Votten;23813076;ERROR_FLAGS_800;3.871;0;1111-11-11 11:11.11
-// telegram=|3244372C763081233C168D2057D2ED11205a817905095480_0008000000000000000008900008F0FFC12B0A1B23001F0F000013|
-// {"_":"telegram","media":"cold water","meter":"flowiq2200","name":"Votten","id":"23813076","status":"ERROR_FLAGS_800","total_m3":3.871,"target_m3":0,"target_date":"2022-11-01","flow_m3h":0.035,"max_external_temperature_c":27,"min_external_temperature_c":10,"max_flow_m3h":0,"timestamp":"1111-11-11T11:11:11Z"}
-// |Votten;23813076;ERROR_FLAGS_800;3.871;0;1111-11-11 11:11.11
diff --git a/components/wmbus_common/driver_gransystems.cpp b/components/wmbus_common/driver_gransystems.cpp
deleted file mode 100644
index 8bdc6503..00000000
--- a/components/wmbus_common/driver_gransystems.cpp
+++ /dev/null
@@ -1,287 +0,0 @@
-/*
- Copyright (C) 2018-2022 Fredrik Öhrström (gpl-3.0-or-later)
-
- 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 .
-*/
-
-/*
-Implemented January 2021 Xael South:
-Implements GSS, CC101 and CC301 energy meter.
-This T1 WM-Bus meter broadcasts:
-- accumulated energy consumption
-- accumulated energy consumption till yesterday
-- current date
-- actually measured voltage
-- actually measured current
-- actually measured frequency
-- meter status and errors
-
-The single-phase and three-phase send apparently the same datagram:
-three-phase meter sends voltage and current values for every phase
-L1 .. L3.
-
-Meter version. Implementation tested against meters:
-- CC101 one-phase with firmware version 0x01.
-- CC301 three-phase with firmware version 0x01.
-
-Encryption: None.
-*/
-
-#include"meters_common_implementation.h"
-
-namespace
-{
- struct Driver : public virtual MeterCommonImplementation
- {
- Driver(MeterInfo &mi, DriverInfo &di);
- };
-
- static bool ok = staticRegisterDriver([](DriverInfo&di)
- {
- di.setName("gransystems");
- di.setDefaultFields("name,id,total_energy_consumption_kwh,timestamp");
- di.setMeterType(MeterType::ElectricityMeter);
- di.addLinkMode(LinkMode::T1);
- di.addMVT(MANUFACTURER_GSS, 0x02, 0x01);
- di.setConstructor([](MeterInfo& mi, DriverInfo& di){ return shared_ptr(new Driver(mi, di)); });
- });
-
- Driver::Driver(MeterInfo &mi, DriverInfo &di) : MeterCommonImplementation(mi, di)
- {
- addStringFieldWithExtractorAndLookup(
- "status",
- "Status of meter.",
- DEFAULT_PRINT_PROPERTIES | PrintProperty::STATUS | PrintProperty::INCLUDE_TPL_STATUS,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::ErrorFlags),
- {
- {
- {
- "ERROR_FLAGS",
- Translate::MapType::BitToString,
- AlwaysTrigger,
- AutoMask,
- "OK",
- {
- { 0x0001, "METER_HARDWARE_ERROR" },
- { 0x0002, "RTC_ERROR" },
- { 0x0100, "DSP_COMMUNICATION_ERROR" },
- { 0x0200, "DSP_HARDWARE_ERROR" },
- { 0x4000, "RAM_ERROR" },
- { 0x8000, "ROM_ERROR" }
- }
- },
- {
- "ERROR_FLAGS_SINGLE_PHASE",
- Translate::MapType::BitToString,
- TriggerBits(0x01020000),
- AutoMask,
- "OK",
- {
- { 0x0008, "DEVICE_NOT_CONFIGURED" },
- { 0x0010, "INTERNAL_ERROR" },
- { 0x0020, "BATTERY_LOW" },
- { 0x0040, "MAGNETIC_FRAUD_PRESENT" },
- { 0x0080, "MAGNETIC_FRAUD_PAST" },
- }
- },
- {
- "ERROR_FLAGS_THREE_PHASE",
- Translate::MapType::BitToString,
- TriggerBits(0x01010000),
- AutoMask,
- "OK",
- {
- { 0x0008, "CALIBRATION_EEPROM_ERROR" },
- { 0x0010, "NETWORK_INTERFERENCE" },
- { 0x0800, "CALIBRATION_EEPROM_ERROR" },
- { 0x1000, "EEPROM1_ERROR" }
- }
- },
- },
- });
-
- addStringFieldWithExtractorAndLookup(
- "info",
- "Is it a three phase or single phase meter.",
- DEFAULT_PRINT_PROPERTIES,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::ErrorFlags),
- {
- {
- {
- "INFO_FLAGS",
- Translate::MapType::IndexToString,
- AlwaysTrigger,
- AutoMask,
- "",
- {
- { 0x01020000, "SINGLE_PHASE_METER" },
- { 0x01010000, "THREE_PHASE_METER" }
- }
- }
- },
- });
-
- addNumericFieldWithExtractor(
- "total_energy_consumption",
- "The total energy consumption recorded by this meter.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Energy,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyEnergyVIF)
- );
-
- addNumericFieldWithExtractor(
- "total_energy_consumption_tariff_{tariff_counter}",
- "The total energy consumption recorded by this meter.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Energy,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyEnergyVIF)
- .set(TariffNr(1), TariffNr(4))
- );
-
- addNumericFieldWithExtractor(
- "target",
- "Last day?",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::PointInTime,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::DateTime)
- .set(StorageNr(2))
- );
-
- addNumericFieldWithExtractor(
- "target_energy_consumption",
- "Last day energy consumption?",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Energy,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyEnergyVIF)
- .set(StorageNr(2))
- );
-
- addNumericFieldWithExtractor(
- "target_energy_consumption_tariff_{tariff_counter}",
- "Last day energy consumption for tariff?",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Energy,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyEnergyVIF)
- .set(TariffNr(1), TariffNr(4))
- .set(StorageNr(2))
- );
-
- addNumericFieldWithExtractor(
- "device",
- "Device date time when telegram was sent.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::PointInTime,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::DateTime)
- );
-
- addNumericFieldWithExtractor(
- "voltage_at_phase_1",
- "Voltage for single phase meter.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Voltage,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::Voltage)
- .set(SubUnitNr(0))
- );
-
- addNumericFieldWithExtractor(
- "voltage_at_phase_{subunit_counter}",
- "Voltage at phase L#.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Voltage,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::Voltage)
- .set(SubUnitNr(1), SubUnitNr(3))
- );
-
- addNumericFieldWithExtractor(
- "current_at_phase_1",
- "Amperage for single phase meter.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Amperage,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::Amperage)
- .set(SubUnitNr(0))
- );
-
- addNumericFieldWithExtractor(
- "current_at_phase_{subunit_counter}",
- "Amperage at phase L#.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Amperage,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::Amperage)
- .set(SubUnitNr(1), SubUnitNr(3))
- );
-
- addNumericFieldWithExtractor(
- "raw_frequency",
- "Raw input to frequency.",
- DEFAULT_PRINT_PROPERTIES | PrintProperty::HIDE,
- Quantity::Frequency,
- VifScaling::None, DifSignedness::Signed,
- FieldMatcher::build()
- .set(DifVifKey("02FB2D"))
- );
-
- addNumericFieldWithCalculator(
- "frequency",
- "Frequency of AC.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Frequency,
- "raw_frequency_hz / 100 counter");
- }
-}
-
-// Test: Gran101 gransystems 18046178 NOKEY
-// Comment: Gran-System-S electricity meter 101
-// telegram=|7844731e78610418010278046d0f13bc21040394030000841003690300008420032b00000084300300000000848010030000000084016d0000bc2184010394030000841103690300008421032b00000084310300000000848110030000000004fd482e09000004fd5b0000000002fb2d861304fd1700000201|
-// {"_":"telegram","media":"electricity","meter":"gransystems","name":"Gran101","id":"18046178","status":"OK","info":"SINGLE_PHASE_METER","total_energy_consumption_kwh":0.916,"total_energy_consumption_tariff_4_kwh":0,"total_energy_consumption_tariff_2_kwh":0.043,"total_energy_consumption_tariff_3_kwh":0,"total_energy_consumption_tariff_1_kwh":0.873,"target_datetime":"2021-01-28 00:00","target_energy_consumption_kwh":0.916,"target_energy_consumption_tariff_2_kwh":0.043,"target_energy_consumption_tariff_4_kwh":0,"target_energy_consumption_tariff_3_kwh":0,"target_energy_consumption_tariff_1_kwh":0.873,"device_datetime":"2021-01-28 19:15","voltage_at_phase_1_v":235,"current_at_phase_1_a":0,"frequency_hz":49.98,"timestamp":"1111-11-11T11:11:11Z"}
-// |Gran101;18046178;0.916;1111-11-11 11:11.11
-
-// Test: Gran301 gransystems 20100117 NOKEY
-// Comment: Gran-System-S electricity meter 301
-// telegram=|9e44731e17011020010278046d0813bc21040300000000841003000000008420030000000084300300000000848010030000000084016d0000bc218401030000000084110300000000842103000000008431030000000084811003000000008440fd4825090000848040fd480000000084c040fd48000000008440fd5b00000000848040fd5b0000000084c040fd5b0000000002fb2d881304fd1702000101|
-// {"_":"telegram","media":"electricity","meter":"gransystems","name":"Gran301","id":"20100117","status":"RTC_ERROR","info":"THREE_PHASE_METER","total_energy_consumption_kwh":0,"total_energy_consumption_tariff_2_kwh":0,"total_energy_consumption_tariff_3_kwh":0,"total_energy_consumption_tariff_4_kwh":0,"total_energy_consumption_tariff_1_kwh":0,"target_datetime":"2021-01-28 00:00","target_energy_consumption_kwh":0,"target_energy_consumption_tariff_1_kwh":0,"target_energy_consumption_tariff_2_kwh":0,"target_energy_consumption_tariff_4_kwh":0,"target_energy_consumption_tariff_3_kwh":0,"device_datetime":"2021-01-28 19:08","voltage_at_phase_2_v":0,"voltage_at_phase_3_v":0,"voltage_at_phase_1_v":234.1,"current_at_phase_1_a":0,"current_at_phase_2_a":0,"current_at_phase_3_a":0,"frequency_hz":50,"timestamp":"1111-11-11T11:11:11Z"}
-// |Gran301;20100117;0;1111-11-11 11:11.11
diff --git a/components/wmbus_common/driver_gwfwater.cpp b/components/wmbus_common/driver_gwfwater.cpp
deleted file mode 100644
index de10d1cd..00000000
--- a/components/wmbus_common/driver_gwfwater.cpp
+++ /dev/null
@@ -1,110 +0,0 @@
-/*
- Copyright (C) 2023 Fredrik Öhrström (gpl-3.0-or-later)
-
- 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 .
-*/
-
-#include"meters_common_implementation.h"
-
-namespace
-{
- struct Driver : public virtual MeterCommonImplementation
- {
- Driver(MeterInfo &mi, DriverInfo &di);
-
- void processContent(Telegram *t);
- };
-
- static bool ok = staticRegisterDriver([](DriverInfo&di)
- {
- di.setName("gwfwater");
- di.setDefaultFields("name,id,total_m3,timestamp");
- di.setMeterType(MeterType::WaterMeter);
- di.addMVT(MANUFACTURER_GWF, 0x0e, 0x01);
- di.addMVT(MANUFACTURER_GWF, 0x07, 0x3c);
- di.usesProcessContent();
- di.setConstructor([](MeterInfo& mi, DriverInfo& di){ return shared_ptr(new Driver(mi, di)); });
- });
-
- Driver::Driver(MeterInfo &mi, DriverInfo &di) : MeterCommonImplementation(mi, di)
- {
- addOptionalLibraryFields("actuality_duration_s");
- addOptionalLibraryFields("total_m3,target_m3,target_date");
-
- addStringField(
- "status",
- "Meter status.",
- PrintProperty::INCLUDE_TPL_STATUS | PrintProperty::STATUS);
-
- addStringField(
- "mfct_status",
- "Mfct status bit.",
- PrintProperty::INJECT_INTO_STATUS | PrintProperty::HIDE);
-
- addStringField(
- "power_mode",
- "Normal or saving.",
- DEFAULT_PRINT_PROPERTIES);
-
- addNumericField(
- "battery",
- Quantity::Time,
- DEFAULT_PRINT_PROPERTIES,
- "Remaining battery life in years.",
- Unit::Year);
- }
-
- void Driver::processContent(Telegram *t)
- {
- std::vector bytes;
- t->extractMfctData(&bytes); // Extract raw frame data after the DIF 0x0F.
-
- if (bytes.size() < 3) return;
-
- uchar type = bytes[0];
- uchar a = bytes[1];
- uchar b = bytes[2];
-
- if (type != 1)
- {
- setStringValue("mfct_status", tostrprintf("UKNOWN_MFCT_STATUS=%02x%02x%02x", type, a, b), NULL);
- return;
- }
-
- std::string info;
-
- if (a & 0x02) info += "CONTINUOUS_FLOW ";
- if (a & 0x08) info += "BROKEN_PIPE ";
- if (a & 0x20) info += "BATTERY_LOW ";
- if (a & 0x40) info += "BACKFLOW ";
-
- if (info.size() > 0) info.pop_back();
- setStringValue("mfct_status", info, NULL);
-
- setStringValue("power_mode", (b & 0x01) ? "SAVING" : "NORMAL", NULL);
-
- double battery_semesters = (b >> 3); // Half years.
- setNumericValue("battery", Unit::Year, battery_semesters/2.0);
- }
-}
-
-// Test: Wateroo gwfwater 20221031 NOKEY
-// telegram=|3144E61E31102220010E8C04F47ABE0420452F2F_037410000004133E0000004413FFFFFFFF426CFFFF0F0120012F2F2F2F2F|
-// {"_":"telegram","actuality_duration_s": 16,"battery_y": 0,"id": "20221031","media": "bus/system component","meter": "gwfwater","name": "Wateroo","power_mode": "SAVING","status": "BATTERY_LOW POWER_LOW","target_date": "2128-03-31","target_m3": -0.001,"timestamp": "1111-11-11T11:11:11Z","total_m3": 0.062}
-// |Wateroo;20221031;0.062;1111-11-11 11:11.11
-
-// Test: Watertwo gwfwater 19680750 NOKEY
-// telegram=|3944E61E46441510020E8C04427250076819E61E3C07B10020452F2F_03748B05000413DC690D004413A4630D00426C213C02FD74B5140F0100E3|
-// {"_": "telegram","actuality_duration_s": 1419,"battery_y": 14,"id": "19680750","media": "water","meter": "gwfwater","name": "Watertwo","power_mode": "SAVING","status": "OK","target_date": "2025-12-01","target_m3": 877.476,"timestamp": "1111-11-11T11:11:11Z","total_m3": 879.068}
-// |Watertwo;19680750;879.068;1111-11-11 11:11.11
diff --git a/components/wmbus_common/driver_hcae2.cpp b/components/wmbus_common/driver_hcae2.cpp
deleted file mode 100644
index 91050144..00000000
--- a/components/wmbus_common/driver_hcae2.cpp
+++ /dev/null
@@ -1,115 +0,0 @@
-/*
- Copyright (C) 2019-2023 Fredrik Öhrström (gpl-3.0-or-later)
-
- 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 .
-*/
-
-#include"meters_common_implementation.h"
-
-namespace
-{
- struct Driver : public virtual MeterCommonImplementation
- {
- Driver(MeterInfo &mi, DriverInfo &di);
- };
-
- static bool ok = staticRegisterDriver([](DriverInfo&di)
- {
- di.setName("hcae2");
- di.setDefaultFields("name,id,current_consumption_hca,status,timestamp");
- di.setMeterType(MeterType::HeatCostAllocationMeter);
- di.addLinkMode(LinkMode::T1);
- di.addMVT(MANUFACTURER_EFE, 0x08, 0x31);
- di.setConstructor([](MeterInfo& mi, DriverInfo& di){ return shared_ptr(new Driver(mi, di)); });
- });
-
- Driver::Driver(MeterInfo &mi, DriverInfo &di) : MeterCommonImplementation(mi, di)
- {
- addStringFieldWithExtractorAndLookup(
- "status",
- "Meter status from error flags and tpl status field.",
- DEFAULT_PRINT_PROPERTIES |
- PrintProperty::STATUS | PrintProperty::INCLUDE_TPL_STATUS,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::ErrorFlags),
- Translate::Lookup(
- {
- {
- {
- "ERROR_FLAGS",
- Translate::MapType::BitToString,
- AlwaysTrigger, MaskBits(0xffff),
- "OK",
- {
- { 0x0001, "MEASUREMENT" },
- { 0x0002, "SABOTAGE" },
- { 0x0004, "BATTERY" },
- { 0x0008, "CS" },
- { 0x0010, "HF" },
- { 0x0020, "RESET" }
- }
- },
- },
- }));
-
- addNumericFieldWithExtractor(
- "current_consumption",
- "The current heat cost allocation.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::HCA,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::HeatCostAllocation)
- );
-
- addNumericFieldWithExtractor(
- "consumption_at_set_date_{storage_counter}",
- "The heat cost allocation at set date #.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::HCA,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::HeatCostAllocation)
- .set(StorageNr(1),StorageNr(17))
- );
-
- addNumericFieldWithExtractor(
- "consumption_at_set_date",
- "Deprecated field.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::HCA,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::HeatCostAllocation)
- .set(StorageNr(1))
- );
-
-
- }
-}
-
-// Test: HeatMeter hcae2 88018801 NOKEY
-// telegram=|7644C52501880188550872_01880188C5255508010000002F2F0B6E332211426E110182016E1102C2016E110382026E1104C2026E110582036E1106C2036E110782046E1108C2046E110982056E1110C2056E111182066E1112C2066E111382076E1114C2076E111582086E1116C2086E111702FD172100|
-// {"_":"telegram","media":"heat cost allocation","meter":"hcae2","name":"HeatMeter","id":"88018801","status":"MEASUREMENT RESET","current_consumption_hca":112233,"consumption_at_set_date_hca":273,"consumption_at_set_date_1_hca":273,"consumption_at_set_date_2_hca":529,"consumption_at_set_date_3_hca":785,"consumption_at_set_date_4_hca":1041,"consumption_at_set_date_5_hca":1297,"consumption_at_set_date_6_hca":1553,"consumption_at_set_date_7_hca":1809,"consumption_at_set_date_8_hca":2065,"consumption_at_set_date_9_hca":2321,"consumption_at_set_date_10_hca":4113,"consumption_at_set_date_11_hca":4369,"consumption_at_set_date_12_hca":4625,"consumption_at_set_date_13_hca":4881,"consumption_at_set_date_14_hca":5137,"consumption_at_set_date_15_hca":5393,"consumption_at_set_date_16_hca":5649,"consumption_at_set_date_17_hca":5905,"timestamp":"1111-11-11T11:11:11Z"}
-// |HeatMeter;88018801;112233;MEASUREMENT RESET;1111-11-11 11:11.11
-
-
-// Test: HeatMeter2 hcae2 60200770 NOKEY
-// telegram=|76442D4870072060550872700720602D485508280060052F2F_0B6E320100426E550082016E3500C2016E1F0082026E1F00C2026E130082036E1300C2036E130082046E1300C2046E120082056E7D01C2056E440182066E0601C2066EB80082076E7F00C2076E320082086E1C00C2086E1C0002FD170000
-// {"_":"telegram","media":"heat cost allocation","meter":"hcae2","name":"HeatMeter2","id":"60200770","status":"OK","current_consumption_hca":132,"consumption_at_set_date_4_hca":31,"consumption_at_set_date_5_hca":19,"consumption_at_set_date_6_hca":19,"consumption_at_set_date_2_hca":53,"consumption_at_set_date_14_hca":127,"consumption_at_set_date_8_hca":19,"consumption_at_set_date_1_hca":85,"consumption_at_set_date_10_hca":381,"consumption_at_set_date_17_hca":28,"consumption_at_set_date_13_hca":184,"consumption_at_set_date_11_hca":324,"consumption_at_set_date_3_hca":31,"consumption_at_set_date_9_hca":18,"consumption_at_set_date_15_hca":50,"consumption_at_set_date_16_hca":28,"consumption_at_set_date_7_hca":19,"consumption_at_set_date_12_hca":262,"consumption_at_set_date_hca":85,"timestamp":"1111-11-11T11:11:11Z"}
-// |HeatMeter2;60200770;132;OK;1111-11-11 11:11.11
diff --git a/components/wmbus_common/driver_hydrocalm3.cpp b/components/wmbus_common/driver_hydrocalm3.cpp
deleted file mode 100644
index b326a36c..00000000
--- a/components/wmbus_common/driver_hydrocalm3.cpp
+++ /dev/null
@@ -1,164 +0,0 @@
-/*
- Copyright (C) 2018-2022 Fredrik Öhrström (gpl-3.0-or-later)
-
- 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 .
-*/
-
-#include"meters_common_implementation.h"
-
-namespace
-{
- struct Driver : public virtual MeterCommonImplementation
- {
- Driver(MeterInfo &mi, DriverInfo &di);
- };
-
- static bool ok = staticRegisterDriver([](DriverInfo&di)
- {
- di.setName("hydrocalm3");
- di.setDefaultFields("name,id,total_heating_kwh,total_cooling_kwh,timestamp");
- di.setMeterType(MeterType::HeatMeter);
- di.addLinkMode(LinkMode::T1);
- di.addMVT(MANUFACTURER_BMT, 0x0d, 0x0b);
- di.setConstructor([](MeterInfo& mi, DriverInfo& di){ return shared_ptr(new Driver(mi, di)); });
- });
-
- Driver::Driver(MeterInfo &mi, DriverInfo &di) : MeterCommonImplementation(mi, di)
- {
- setMfctTPLStatusBits(
- Translate::Lookup()
- .add(Translate::Rule("TPL_STS", Translate::MapType::BitToString)
- .set(MaskBits(0xe0))
- .set(DefaultMessage("OK"))
- .add(Translate::Map(0x80 ,"SABOTAGE_ENCLOSURE", TestBit::Set))));
-
- addStringField(
- "status",
- "Meter status from tpl status field.",
- DEFAULT_PRINT_PROPERTIES |
- PrintProperty::STATUS | PrintProperty::INCLUDE_TPL_STATUS);
-
- addNumericFieldWithExtractor(
- "total_heating",
- "The total heating energy consumption recorded by this meter.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Energy,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyEnergyVIF)
- .set(IndexNr(1))
- );
-
- addNumericFieldWithExtractor(
- "device",
- "The date time when the recording was made.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::PointInTime,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::DateTime)
- );
-
- addNumericFieldWithExtractor(
- "total_cooling",
- "The total cooling energy consumption recorded by this meter.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Energy,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyEnergyVIF)
- .set(IndexNr(2))
- );
-
- addNumericFieldWithExtractor(
- "total_heating",
- "Total heating volume of media.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Volume,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::Volume)
- .set(IndexNr(1))
- );
-
- addNumericFieldWithExtractor(
- "total_cooling",
- "Total cooling volume of media.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Volume,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::Volume)
- .set(IndexNr(2))
- );
-
- addNumericFieldWithExtractor(
- "c1_volume",
- "Supply c1 volume.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Volume,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::Volume)
- .set(IndexNr(3))
- );
-
- addNumericFieldWithExtractor(
- "c2_volume",
- "Return c2 volume.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Volume,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::Volume)
- .set(IndexNr(4))
- );
-
- addNumericFieldWithExtractor(
- "supply_temperature",
- "The supply t1 pipe temperature.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Temperature,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::FlowTemperature)
- .set(IndexNr(1))
- );
-
- addNumericFieldWithExtractor(
- "return_temperature",
- "The return t2 pipe temperature.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Temperature,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::ReturnTemperature)
- );
-
- }
-}
-
-// Test: HeatCool hydrocalm3 71727374 NOKEY
-// telegram=|8E44B409747372710B0D7A798080052F2F_0C0E59600100046D1D36B9290C13679947000C0E000000000C13590000000C13000000000C13000000000A5A18020A5E11020F823D06003D06003D06003D0600140600620500480400E402001601000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002F2F|
-// {"_":"telegram","media":"heat/cooling load","meter":"hydrocalm3","name":"HeatCool","id":"71727374","status": "SABOTAGE_ENCLOSURE","total_heating_kwh":4460.833333,"total_cooling_kwh":0,"device_datetime":"2021-09-25 22:29","total_heating_m3":479.967,"total_cooling_m3":0.059,"c1_volume_m3":0,"c2_volume_m3":0,"supply_temperature_c":21.8,"return_temperature_c":21.1,"timestamp":"1111-11-11T11:11:11Z"}
-// |HeatCool;71727374;4460.833333;0;1111-11-11 11:11.11
\ No newline at end of file
diff --git a/components/wmbus_common/driver_hydroclima.cpp b/components/wmbus_common/driver_hydroclima.cpp
deleted file mode 100644
index d0671fa9..00000000
--- a/components/wmbus_common/driver_hydroclima.cpp
+++ /dev/null
@@ -1,254 +0,0 @@
-/*
- Copyright (C) 2022 Fredrik Öhrström (gpl-3.0-or-later)
-
- 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 .
-*/
-
-#include"meters_common_implementation.h"
-
-namespace
-{
- struct Driver : public virtual MeterCommonImplementation
- {
- Driver(MeterInfo &mi, DriverInfo &di);
- void processContent(Telegram *t);
- void decodeRF_RKN0(Telegram *t);
- void decodeRF_RKN9(Telegram *t);
- };
-
- static bool ok = staticRegisterDriver([](DriverInfo&di)
- {
- di.setName("hydroclima");
- di.setDefaultFields("name,id,current_consumption_hca,average_ambient_temperature_c,timestamp");
- di.setMeterType(MeterType::HeatCostAllocationMeter);
- di.addLinkMode(LinkMode::T1);
- di.addMVT(MANUFACTURER_BMP, 0x08, 0x53);
- di.usesProcessContent();
- di.setConstructor([](MeterInfo& mi, DriverInfo& di){ return shared_ptr(new Driver(mi, di)); });
- });
-
- Driver::Driver(MeterInfo &mi, DriverInfo &di) : MeterCommonImplementation(mi, di)
- {
- addNumericFieldWithExtractor(
- "current_consumption",
- "The current heat cost allocation.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::HCA,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::HeatCostAllocation)
- );
-
- addNumericField("average_ambient_temperature",
- Quantity::Temperature,
- DEFAULT_PRINT_PROPERTIES,
- "Average ambient temperature since this beginning of this month.");
-
- addNumericField("max_ambient_temperature",
- Quantity::Temperature,
- DEFAULT_PRINT_PROPERTIES,
- "Max ambient temperature since the beginning of this month.");
-
- addNumericField("average_ambient_temperature_last_month",
- Quantity::Temperature,
- DEFAULT_PRINT_PROPERTIES,
- "Average ambient temperature last month.");
-
- addNumericField("average_heater_temperature_last_month",
- Quantity::Temperature,
- DEFAULT_PRINT_PROPERTIES,
- "Average heater temperature last month.");
- }
-
- double toTemperature(uchar hi, uchar lo)
- {
- return ((double)((hi<<8) | lo))/100.0;
- }
-
- double toIndicationU(uchar hi, uchar lo)
- {
- return ((double)((hi<<8) | lo))/10.0;
- }
-
- double toTotalIndicationU(uchar hihi, uchar hi, uchar lo)
- {
- int x = (hihi << 16) | (hi<<8) | lo;
- return ((double)x)/10.0;
- }
-
- void Driver::processContent(Telegram *t)
- {
- if (t->mfct_0f_index == -1) return; // Check that there is mfct data.
-
- if (t->dv_entries.count("036E") != 0)
- {
- decodeRF_RKN0(t);
- }
- else
- {
- decodeRF_RKN9(t);
- }
- }
-
- void Driver::decodeRF_RKN0(Telegram *t)
- {
- int offset = t->header_size+t->mfct_0f_index;
-
- std::vector bytes;
- t->extractMfctData(&bytes); // Extract raw frame data after the DIF 0x0F.
-
- debugPayload("(hydroclima mfct)", bytes);
-
- int i = 0;
- int len = bytes.size();
- std::string info;
-
- if (i >= len) return;
- uchar frame_identifier = bytes[i];
- t->addSpecialExplanation(i+offset, 1, KindOfData::PROTOCOL, Understanding::FULL,
- "*** %02X frame identifier %s", frame_identifier,
- frame_identifier == 0x10 ? "OK" :"UNKNOWN");
- i++;
-
- if (i+1 >= len) return;
- uint16_t status = bytes[i+1]<<8 | bytes[i];
- t->addSpecialExplanation(i+offset, 2, KindOfData::CONTENT, Understanding::FULL,
- "*** %02X%02X status", bytes[i], bytes[i+1], status);
- i+=2;
-
- if (i+1 >= len) return;
- uint16_t time = bytes[i+1]<<8 | bytes[i];
- t->addSpecialExplanation(i+offset, 2, KindOfData::CONTENT, Understanding::FULL,
- "*** %02X%02X time", bytes[i], bytes[i+1], time);
- i+=2;
-
- if (i+1 >= len) return;
- uint16_t date = bytes[i+1]<<8 | bytes[i];
- t->addSpecialExplanation(i+offset, 2, KindOfData::CONTENT, Understanding::FULL,
- "*** %02X%02X date %x", bytes[i], bytes[i+1], date);
- i+=2;
-
- if (i+1 >= len) return;
- double average_ambient_temperature_c = toTemperature(bytes[i+1], bytes[i]);
- setNumericValue("average_ambient_temperature", Unit::C, average_ambient_temperature_c);
- info = renderJsonOnlyDefaultUnit("average_ambient_temperature", Quantity::Temperature);
- t->addSpecialExplanation(i+offset, 2, KindOfData::CONTENT, Understanding::FULL,
- "*** %02X%02X (%s)", bytes[i], bytes[i+1], info.c_str());
- i+=2;
-
- if (i+1 >= len) return;
- double max_ambient_temperature_c = toTemperature(bytes[i+1], bytes[i]);
- setNumericValue("max_ambient_temperature", Unit::C, max_ambient_temperature_c);
- info = renderJsonOnlyDefaultUnit("max_ambient_temperature", Quantity::Temperature);
- t->addSpecialExplanation(i+offset, 2, KindOfData::CONTENT, Understanding::FULL,
- "*** %02X%02X (%s)", bytes[i], bytes[i+1], info.c_str());
- i+=2;
-
- if (i+1 >= len) return;
- uint16_t max_date = bytes[i+1]<<8 | bytes[i];
- t->addSpecialExplanation(i+offset, 2, KindOfData::CONTENT, Understanding::FULL,
- "*** %02X%02X max date %x", bytes[i], bytes[i+1], max_date);
- i+=2;
-
- if (i+1 >= len) return;
- uint16_t num_measurements = bytes[i+1]<<8 | bytes[i];
- t->addSpecialExplanation(i+offset, 2, KindOfData::CONTENT, Understanding::FULL,
- "*** %02X%02X num measurements %d", bytes[i], bytes[i+1], num_measurements);
- i+=2;
-
- if (i+1 >= len) return;
- double average_ambient_temperature_last_month_c = toTemperature(bytes[i+1], bytes[i]);
- setNumericValue("average_ambient_temperature_last_month", Unit::C,
- average_ambient_temperature_last_month_c);
- info = renderJsonOnlyDefaultUnit("average_ambient_temperature_last_month", Quantity::Temperature);
- t->addSpecialExplanation(i+offset, 2, KindOfData::CONTENT, Understanding::FULL,
- "*** %02X%02X (%s)", bytes[i], bytes[i+1], info.c_str());
- i+=2;
-
- if (i+1 >= len) return;
- double average_heater_temperature_last_month_c = toTemperature(bytes[i+1], bytes[i]);
- setNumericValue("average_heater_temperature_last_month", Unit::C,
- average_heater_temperature_last_month_c);
- info = renderJsonOnlyDefaultUnit("average_heater_temperature_last_month", Quantity::Temperature);
- t->addSpecialExplanation(i+offset, 2, KindOfData::CONTENT, Understanding::FULL,
- "*** %02X%02X (%s)", bytes[i], bytes[i+1], info.c_str());
- i+=2;
-
- if (i+1 >= len) return;
- double indication_u = toIndicationU(bytes[i+1], bytes[i]);
- t->addSpecialExplanation(i+offset, 2, KindOfData::CONTENT, Understanding::FULL,
- "*** %02X%02X indication u %f", bytes[i], bytes[i+1], indication_u);
- i+=2;
-
- if (i+2 >= len) return;
- double total_indication_u = toTotalIndicationU(bytes[i+2], bytes[i+1], bytes[i]);
- t->addSpecialExplanation(i+offset, 3, KindOfData::CONTENT, Understanding::FULL,
- "*** %02X%02X%02X total indication u %f", bytes[i], bytes[i+1], bytes[i+2], total_indication_u);
- i+=3;
- }
-
- void Driver::decodeRF_RKN9(Telegram *t)
- {
- int offset = t->header_size+t->mfct_0f_index;
-
- std::vector bytes;
- t->extractMfctData(&bytes); // Extract raw frame data after the DIF 0x0F.
-
- debugPayload("(hydroclima mfct)", bytes);
-
- int i = 0;
- int len = bytes.size();
- std::string info;
-
- double last_x_month_uc {};
- for (int m = 1; m <= 12; ++m)
- {
- if (i+1 >= len) return;
- last_x_month_uc = toIndicationU(bytes[i+1], bytes[i]);
- t->addSpecialExplanation(i+offset, 2, KindOfData::CONTENT, Understanding::FULL,
- "*** %02X%02X last %d month uc %f", bytes[i], bytes[i+1], m, last_x_month_uc);
- i+=2;
- }
-
- if (i+1 >= len) return;
- uint16_t date_case_opened = bytes[i+1]<<8 | bytes[i];
- t->addSpecialExplanation(i+offset, 2, KindOfData::CONTENT, Understanding::FULL,
- "*** %02X%02X date case opened %x", bytes[i], bytes[i+1], date_case_opened);
- i+=2;
-
- if (i+1 >= len) return;
- uint16_t date_start_month = bytes[i+1]<<8 | bytes[i];
- t->addSpecialExplanation(i+offset, 2, KindOfData::CONTENT, Understanding::FULL,
- "*** %02X%02X date start month %x", bytes[i], bytes[i+1], date_start_month);
- i+=2;
-
- // The test telegram I have has more data, but the specification I have ends here!?!
- }
-
-}
-
-
-// Test: HCA hydroclima 68036198 NOKEY
-// Comment:
-// telegram=|2e44b0099861036853087a000020002F2F036E0000000F100043106A7D2C4A078F12202CB1242A06D3062100210000|
-// {"_":"telegram","media":"heat cost allocation","meter":"hydroclima","name":"HCA","id":"68036198","current_consumption_hca":0,"average_ambient_temperature_c":18.66,"max_ambient_temperature_c":47.51,"average_ambient_temperature_last_month_c":15.78,"average_heater_temperature_last_month_c":17.47,"timestamp":"1111-11-11T11:11:11Z"}
-// |HCA;68036198;0;18.66;1111-11-11 11:11.11
-
-
-// Test: HCAA hydroclima 74393723 NOKEY
-// Comment:
-// telegram=|2D44B009233739743308780F9D1300023ED97AEC7BC5908A32C15D8A32C126915AC15AC126912691269187912689|
-// {"_":"telegram","media":"heat cost allocation","meter":"hydroclima","name":"HCAA","id":"74393723","timestamp":"1111-11-11T11:11:11Z"}
-// |HCAA;74393723;null;null;1111-11-11 11:11.11
diff --git a/components/wmbus_common/driver_hydrodigit.cpp b/components/wmbus_common/driver_hydrodigit.cpp
deleted file mode 100644
index 6b010f7b..00000000
--- a/components/wmbus_common/driver_hydrodigit.cpp
+++ /dev/null
@@ -1,402 +0,0 @@
-/*
- Copyright (C) 2019-2023 Fredrik Öhrström (gpl-3.0-or-later)
- Copyright (C) 2025 Karel Blavka (gpl-3.0-or-later)
-
- 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 .
- */
-
-#include"meters_common_implementation.h"
-
-#include
-
-namespace {
- struct Driver: public virtual MeterCommonImplementation {
- Driver(MeterInfo &mi, DriverInfo &di);
- void processContent(Telegram *t);
- };
-
- static bool ok = staticRegisterDriver([](DriverInfo &di) {
- di.setName("hydrodigit");
- di.setDefaultFields("name,id,total_m3,meter_datetime,timestamp");
- di.setMeterType(MeterType::WaterMeter);
- di.addLinkMode(LinkMode::T1);
- di.addMVT(MANUFACTURER_BMT, 0x06, 0x13);
- di.addMVT(MANUFACTURER_BMT, 0x06, 0x17);
- di.addMVT(MANUFACTURER_BMT, 0x07, 0x13);
- di.addMVT(MANUFACTURER_BMT, 0x07, 0x15);
- di.addMVT(MANUFACTURER_BMT, 0x07, 0x17);
- di.usesProcessContent();
- di.setConstructor([](MeterInfo &mi, DriverInfo &di) {
- return shared_ptr(new Driver(mi, di));
- });
- });
-
- Driver::Driver(MeterInfo &mi, DriverInfo &di) : MeterCommonImplementation(
- mi, di) {
- addNumericFieldWithExtractor("total",
- "The total water consumption recorded by this meter.",
- DEFAULT_PRINT_PROPERTIES, Quantity::Volume, VifScaling::Auto,
- DifSignedness::Signed,
- FieldMatcher::build().set(MeasurementType::Instantaneous).set(
- VIFRange::Volume));
-
- addNumericFieldWithExtractor("meter",
- "Meter timestamp for measurement.",
- DEFAULT_PRINT_PROPERTIES, Quantity::PointInTime,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build().set(MeasurementType::Instantaneous).set(
- VIFRange::DateTime), Unit::DateTimeLT);
- addStringField("contents", "Contents of this telegrams",
- DEFAULT_PRINT_PROPERTIES);
- addNumericField("voltage", Quantity::Voltage, DEFAULT_PRINT_PROPERTIES,
- "Voltage of the battery inside the meter", Unit::Volt);
- addStringField("fraud_type", "Type of fraud detected by the meter",
- DEFAULT_PRINT_PROPERTIES);
- addStringField("fraud_date", "Date of fraud detected by the meter",
- DEFAULT_PRINT_PROPERTIES);
- addStringField("leak_date", "Date of leakage detected by meter",
- DEFAULT_PRINT_PROPERTIES);
- addNumericField("backflow", Quantity::Volume, DEFAULT_PRINT_PROPERTIES,
- "Backflow detected by the meter", Unit::M3);
- addNumericField("January_total", Quantity::Volume,
- DEFAULT_PRINT_PROPERTIES, "Total value at the end of January",
- Unit::M3);
- addNumericField("February_total", Quantity::Volume,
- DEFAULT_PRINT_PROPERTIES, "Total value at the end of February",
- Unit::M3);
- addNumericField("March_total", Quantity::Volume,
- DEFAULT_PRINT_PROPERTIES, "Total value at the end of March",
- Unit::M3);
- addNumericField("April_total", Quantity::Volume,
- DEFAULT_PRINT_PROPERTIES, "Total value at the end of April",
- Unit::M3);
- addNumericField("May_total", Quantity::Volume, DEFAULT_PRINT_PROPERTIES,
- "Total value at the end of May", Unit::M3);
- addNumericField("June_total", Quantity::Volume,
- DEFAULT_PRINT_PROPERTIES, "Total value at the end of June",
- Unit::M3);
- addNumericField("July_total", Quantity::Volume,
- DEFAULT_PRINT_PROPERTIES, "Total value at the end of July",
- Unit::M3);
- addNumericField("August_total", Quantity::Volume,
- DEFAULT_PRINT_PROPERTIES, "Total value at the end of August",
- Unit::M3);
- addNumericField("September_total", Quantity::Volume,
- DEFAULT_PRINT_PROPERTIES, "Total value at the end of September",
- Unit::M3);
- addNumericField("October_total", Quantity::Volume,
- DEFAULT_PRINT_PROPERTIES, "Total value at the end of October",
- Unit::M3);
- addNumericField("November_total", Quantity::Volume,
- DEFAULT_PRINT_PROPERTIES, "Total value at the end of November",
- Unit::M3);
- addNumericField("December_total", Quantity::Volume,
- DEFAULT_PRINT_PROPERTIES, "Total value at the end of December",
- Unit::M3);
-
- }
-
- double fromMonthly(uchar first_byte, uchar second_byte, uchar third_byte) {
- return ((double) (third_byte << 16 | second_byte << 8 | first_byte))
- / 100.0;
- }
-
- double fromBackflow(uchar first_byte, uchar second_byte, uchar third_byte,
- uchar fourth_byte) {
- return ((double) (fourth_byte << 24 | third_byte << 16
- | second_byte << 8 | first_byte)) / 1000.0;
- }
-
- std::string getMonth(int month) {
- switch (month) {
- case 1:
- return "January";
- case 2:
- return "February";
- case 3:
- return "March";
- case 4:
- return "April";
- case 5:
- return "May";
- case 6:
- return "June";
- case 7:
- return "July";
- case 8:
- return "August";
- case 9:
- return "September";
- case 10:
- return "October";
- case 11:
- return "November";
- case 12:
- return "December";
- default:
- return "Month out of range :)";
- }
- }
-
- double getVoltage(uchar voltage_byte) {
- uchar relevant_half = voltage_byte & 0x0F; // ensure dumping of top half
- switch (relevant_half) {
- case 0x01:
- return 1.9;
- case 0x02:
- return 2.1;
- case 0x03:
- return 2.2;
- case 0x04:
- return 2.3;
- case 0x05:
- return 2.4;
- case 0x06:
- return 2.5;
- case 0x07:
- return 2.65;
- case 0x08:
- return 2.8;
- case 0x09:
- return 2.9;
- case 0x0A:
- return 3.05;
- case 0x0B:
- return 3.2;
- case 0x0C:
- return 3.35;
- case 0x0D:
- return 3.5;
- default:
- return 3.7; // 0, E and F all are 3.7
- }
-
- }
-
- constexpr uint8_t MASK_BATTERY_VOLTAGE_PRESENT = 1 << 0; // bit 0
- constexpr uint8_t MASK_FRAUD_DATE_PRESENT = 1 << 1; // bit 1
- constexpr uint8_t MASK_BACKWARD_FLOW_PRESENT = 1 << 2; // bit 2
- constexpr uint8_t MASK_DATA_HISTORY_PRESENT = 1 << 4; // bit 4
- constexpr uint8_t MASK_WATER_LOSS_DATE_PRESENT = 1 << 7; // bit 7
-
- void Driver::processContent(Telegram *t) {
- if (t->mfct_0f_index == -1) return; // Check that there is mfct data.
- int offset = t->header_size + t->mfct_0f_index;
-
- std::vector bytes;
- t->extractMfctData(&bytes); // Extract raw frame data after the DIF 0x0F.
-
- debugPayload("(hydrodigit mfct)", bytes);
-
- int i = 0;
- int len = bytes.size();
-
- if (i >= len) return;
- uchar frame_identifier = bytes[i];
-
- t->addSpecialExplanation(i + offset, 1, KindOfData::PROTOCOL,
- Understanding::FULL, "*** %02X frame content", frame_identifier);
-
- std::string explanation;
-
- if (frame_identifier == 0x00) {
- setStringValue("contents", "");
- return;
- }
-
- i++;
- if (i >= len) return;
-
- if (frame_identifier & MASK_BATTERY_VOLTAGE_PRESENT) {
-
- if (!explanation.empty()) explanation += " ";
- explanation += "BATTERY_VOLTAGE";
-
- // only the bottom half changes the voltage, top half's purpose is unknown
- // values obtained from software by changing value from 0x00-0F
- // changing the top half didn't seem to change anything, but it might require a combination with other bytes
- // my meters have 0x0A and 0x2A but the data seems same
- uchar somethingAndVoltage = bytes[i];
- double voltage = getVoltage(somethingAndVoltage & 0x0F);
- t->addSpecialExplanation(i + offset, 1, KindOfData::CONTENT,
- Understanding::FULL,
- "*** %02X voltage of battery %0.2f V",
- somethingAndVoltage, voltage);
- setNumericValue("voltage", Unit::Volt, voltage);
- i++;
- }
-
- if (frame_identifier & MASK_FRAUD_DATE_PRESENT) {
- if (i + 2 >= len) return;
-
- if (!explanation.empty()) explanation += " ";
- explanation += "FRAUD_DATE";
-
- uchar fraud_year = bytes[i];
- uchar fraud_month_raw = bytes[i + 1];
- uchar fraud_day = bytes[i + 2];
-
- uchar fraud_month = fraud_month_raw & 0x0F;
- uchar fraud_flags = fraud_month_raw & 0xE0;
-
- // Vytvoření popisu typů podvodu
- std::string fraud_type_desc;
- if (fraud_flags & 0x80) { // bit 7
- if (!fraud_type_desc.empty()) fraud_type_desc += ", ";
- fraud_type_desc += "Magnetic fraud attempt";
- }
- if (fraud_flags & 0x40) { // bit 6
- if (!fraud_type_desc.empty()) fraud_type_desc += ", ";
- fraud_type_desc += "Sensor fraud attempt";
- }
- if (fraud_flags & 0x20) { // bit 5
- if (!fraud_type_desc.empty()) fraud_type_desc += ", ";
- fraud_type_desc += "Module removed";
- }
-
- t->addSpecialExplanation(i + offset, 3, KindOfData::CONTENT,
- Understanding::FULL,
- "*** %02X%02X%02X fraud date: %02X.%02X.20%02X [%s]",
- fraud_year, fraud_month_raw, fraud_day,
- fraud_day, fraud_month, fraud_year,
- fraud_type_desc.empty() ? "no type info" : fraud_type_desc.c_str());
-
- char buffer[32];
- snprintf(buffer, sizeof(buffer), "20%02X-%02X-%02X", fraud_year, fraud_month, fraud_day);
- setStringValue("fraud_date", buffer);
- setStringValue("fraud_type", fraud_type_desc.empty() ? "no type info" : fraud_type_desc);
-
- i += 3;
- }
-
- if (frame_identifier & MASK_WATER_LOSS_DATE_PRESENT) {
- if (i + 2 >= len) return;
-
- if (!explanation.empty()) explanation += " ";
- explanation += "LEAK_DATE";
-
- uchar leak_year = bytes[i];
- uchar leak_month = bytes[i + 1];
- uchar leak_day = bytes[i + 2];
- // its BCD so I just print it as is
- t->addSpecialExplanation(i + offset, 3, KindOfData::CONTENT,
- Understanding::FULL,
- "*** %02X%02X%02X date of leakage: %02X.%02X.20%02X",
- bytes[i],
- bytes[i + 1], bytes[i + 2], leak_day, leak_month,
- leak_year);
- char buffer[17];
- memset(buffer, 0, sizeof(buffer));
- snprintf(buffer, 16, "20%02X-%02X-%02X", leak_year, leak_month, leak_day);
- setStringValue("leak_date", buffer);
-
- i += 3;
- }
-
- if (frame_identifier & MASK_BACKWARD_FLOW_PRESENT) {
- if (i + 3 >= len) return;
-
- if (!explanation.empty()) explanation += " ";
- explanation += "BACKFLOW";
-
- // backflow detected by the meter, has higher precision than normal values
- double backflow = fromBackflow(bytes[i], bytes[i + 1], bytes[i + 2],
- bytes[i + 3]);
- t->addSpecialExplanation(i + offset, 4, KindOfData::CONTENT,
- Understanding::FULL, "*** %02X%02X%02X%02X backflow: %0.3f m3",
- bytes[i], bytes[i + 1], bytes[i + 2], bytes[i + 3],
- backflow);
- setNumericValue("backflow", Unit::M3, backflow);
-
- i += 4;
- }
-
- if (frame_identifier & MASK_DATA_HISTORY_PRESENT) {
- if (!explanation.empty()) explanation += " ";
- explanation += "MONTHLY_DATA";
- double monthData;
- string month_field_name;
- for (int month = 1; month <= 12; ++month) {
- if (i + 2 >= len) return;
- month_field_name.clear();
- month_field_name.append(getMonth(month)).append("_total");
- monthData = fromMonthly(bytes[i], bytes[i + 1], bytes[i + 2]);
- // one of our meters was installed backwards so we know the max value is 99999,99
- // anything above (which should only be FFFFFF, more on that below) should be 0
- // FFFFFF (167772,15) means module was not active back then (before installation) - translates to 0
- if (monthData >= 100000) monthData = 0;
- t->addSpecialExplanation(i + offset, 3, KindOfData::CONTENT,
- Understanding::FULL,
- "*** %02X%02X%02X total consumption at the end of %s: %0.2f m3",
- bytes[i], bytes[i + 1], bytes[i + 2],
- getMonth(month).c_str(),
- monthData);
- setNumericValue(month_field_name, Unit::M3, monthData);
- i += 3;
- }
- }
-
- t->addSpecialExplanation(i + offset, 1, KindOfData::PROTOCOL,
- Understanding::FULL, "*** frame content: %s", explanation.c_str());
-
- setStringValue("contents", explanation);
-
- if (i >= len) return;
- // changing this byte didn't seem to do anything
- // and it's always 00 on my meters
- uchar unknown = bytes[i];
- t->addSpecialExplanation(i + offset, 1, KindOfData::CONTENT,
- Understanding::NONE,
- "*** %02X unknown data",
- unknown);
- i++;
- // there should not be any more data
- }
-}
-
-// Test: HydrodigitWater hydrodigit 86868686 NOKEY
-// telegram=|4E44B4098686868613077AF0004005_2F2F0C1366380000046D27287E2A0F150E00000000C10000D10000E60000FD00000C01002F0100410100540100680100890000A00000B30000002F2F2F2F2F2F|
-// {"_":"telegram","April_total_m3": 2.53,"August_total_m3": 3.4,"December_total_m3": 1.79,"February_total_m3": 2.09,"January_total_m3": 1.93,"July_total_m3": 3.21,"June_total_m3": 3.03,"March_total_m3": 2.3,"May_total_m3": 2.68,"November_total_m3": 1.6,"October_total_m3": 1.37,"September_total_m3": 3.6,"backflow_m3": 0,"contents": "BATTERY_VOLTAGE BACKFLOW MONTHLY_DATA","id": "86868686","media": "water","meter": "hydrodigit","meter_datetime": "2019-10-30 08:39","name": "HydrodigitWater","timestamp": "1111-11-11T11:11:11Z","total_m3": 3.866,"voltage_v": 3.7}
-// |HydrodigitWater;86868686;3.866;2019-10-30 08:39;1111-11-11 11:11.11
-
-// Test: HydridigitWaterr hydrodigit 03245501 NOKEY
-// telegram=|2444B4090155240317068C00487AC0000000_0C1335670000046D172EEA280F030000000000|
-// {"_":"telegram","contents": "BATTERY_VOLTAGE FRAUD_DATE","id": "03245501","media": "warm water","meter": "hydrodigit","meter_datetime": "2023-08-10 14:23","name": "HydridigitWaterr","timestamp": "1111-11-11T11:11:11Z","total_m3": 6.735, "voltage_v": 3.7, "fraud_date": "2000-00-00", "fraud_type": "no type info"}
-// |HydridigitWaterr;03245501;6.735;2023-08-10 14:23;1111-11-11 11:11.11
-
-
-// Test: Hydro3 hydrodigit 87654321 NOKEY
-// Comment: This is a nice one to showcase the backflow encoding.
-// telegram=|4644B4092143658713077A9C000000_0C1364390400_046D212F1635_0F152A0F000000440F00C00F00511000D51000B20B00180C007C0C00E60C00560D00D10D00400E00C60E0000|
-// {"_":"telegram","media":"water","meter":"hydrodigit","name":"Hydro3","id":"87654321","April_total_m3":43.09,"August_total_m3":33.02,"December_total_m3":37.82,"February_total_m3":40.32,"January_total_m3":39.08,"July_total_m3":31.96,"June_total_m3":30.96,"March_total_m3":41.77,"May_total_m3":29.94,"November_total_m3":36.48,"October_total_m3":35.37,"September_total_m3":34.14,"backflow_m3":0.015,"meter_datetime":"2024-05-22 15:33","total_m3":43.964,"voltage_v":3.05,"contents":"BATTERY_VOLTAGE BACKFLOW MONTHLY_DATA","timestamp":"1111-11-11T11:11:11Z"}
-// |Hydro3;87654321;43.964;2024-05-22 15:33;1111-11-11 11:11.11
-
-
-// Test: Hydro4 hydrodigit 87654322 NOKEY
-// Comment: This one adds a leak date to the definition, plus shows how the monthly data looks before module installation.
-// telegram=|4944B4092243658713077A7F000000_0C1363020400_046D242C1236_0F950A24042507000000A405006E0700850900CA0B004A0E00FFFFFFFFFFFF020000020000250000B3010095030000|
-// {"_":"telegram","media":"water","meter":"hydrodigit","name":"Hydro4","id":"87654322","April_total_m3":30.18,"August_total_m3":0.02,"December_total_m3":9.17,"February_total_m3":19.02,"January_total_m3":14.44,"July_total_m3":0,"June_total_m3":0,"March_total_m3":24.37,"May_total_m3":36.58,"November_total_m3":4.35,"October_total_m3":0.37,"September_total_m3":0.02,"backflow_m3":0.007,"meter_datetime":"2024-06-18 12:36","total_m3":40.263,"voltage_v":3.05,"contents":"BATTERY_VOLTAGE LEAK_DATE BACKFLOW MONTHLY_DATA","leak_date":"2024-04-25","timestamp":"1111-11-11T11:11:11Z"}
-// |Hydro4;87654322;40.263;2024-06-18 12:36;1111-11-11 11:11.11
-
-
-// Test: HydrodigitWaterrr hydrodigit 23746391 NOKEY
-// Comment: This one has a backflow and a leak date, but no monthly data.
-// telegram=|2144B4099163742315077A400000000C1399999999046D092A30340F050B01000000
-// {"_":"telegram", "media":"water", "meter":"hydrodigit", "name":"HydrodigitWaterrr", "id":"23746391", "backflow_m3":0.001, "meter_datetime":"2025-04-16 10:09", "total_m3":99999.999, "voltage_v":3.2, "contents":"BATTERY_VOLTAGE BACKFLOW", "timestamp":"1111-11-11T11:11:11Z" }
-// |HydrodigitWaterrr;23746391;99999.999;2025-04-16 10:09;1111-11-11 11:11.11
-
-// Test: hydro7 hydrodigit 03122061 NOKEY
-// telegram=|4C44B4096120120317077AB90000000C1330000000046D132E3E360F8F000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000|
-// {"_": "telegram","backflow_m3": 0,"contents": "BATTERY_VOLTAGE FRAUD_DATE LEAK_DATE BACKFLOW","fraud_date": "2000-00-00","fraud_type": "no type info","id": "03122061","leak_date": "2000-00-00","media": "water","meter": "hydrodigit","meter_datetime": "2025-06-30 14:19","name": "hydro7","timestamp": "1111-11-11T11:11:11Z","total_m3": 0.03,"voltage_v": 3.7}
-// |hydro7;03122061;0.03;2025-06-30 14:19;1111-11-11 11:11.11
diff --git a/components/wmbus_common/driver_hydrus.cpp b/components/wmbus_common/driver_hydrus.cpp
deleted file mode 100644
index 0b99fa5d..00000000
--- a/components/wmbus_common/driver_hydrus.cpp
+++ /dev/null
@@ -1,197 +0,0 @@
-/*
- Copyright (C) 2019-2023 Fredrik Öhrström (gpl-3.0-or-later)
- Copyright (C) 2021 Vincent Privat (gpl-3.0-or-later)
-
- 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 .
-*/
-
-#include"meters_common_implementation.h"
-
-namespace
-{
- struct Driver : public virtual MeterCommonImplementation
- {
- Driver(MeterInfo &mi, DriverInfo &di);
- };
-
- static bool ok = staticRegisterDriver([](DriverInfo&di)
- {
- di.setName("hydrus");
- di.setDefaultFields("name,id,total_m3,total_at_date_m3,status,timestamp");
- di.setMeterType(MeterType::WaterMeter);
- di.addLinkMode(LinkMode::T1);
- di.addMVT(MANUFACTURER_DME, 0x07, 0x70);
- di.addMVT(MANUFACTURER_DME, 0x07, 0x76);
- di.addMVT(MANUFACTURER_HYD, 0x07, 0x24);
- di.addMVT(MANUFACTURER_HYD, 0x07, 0x8b);
- di.addMVT(MANUFACTURER_HYD, 0x06, 0x8b);
- di.addMVT(MANUFACTURER_DME, 0x06, 0x70);
- di.addMVT(MANUFACTURER_DME, 0x16, 0x70);
-
- di.setConstructor([](MeterInfo& mi, DriverInfo& di){ return shared_ptr(new Driver(mi, di)); });
- });
-
- Driver::Driver(MeterInfo &mi, DriverInfo &di) : MeterCommonImplementation(mi, di)
- {
- addOptionalLibraryFields("operating_time_h,actuality_duration_s,meter_datetime,customer");
- addOptionalLibraryFields("flow_temperature_c,external_temperature_c");
-
- addStringField(
- "status",
- "Status of meter.",
- PrintProperty::STATUS | PrintProperty::INCLUDE_TPL_STATUS);
-
- addNumericFieldWithExtractor(
- "total",
- "The total water consumption recorded by this meter.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Volume,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::Volume)
- );
-
- addNumericFieldWithExtractor(
- "total_tariff{tariff_counter}",
- "The total water consumption recorded on tariff # by this meter.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Volume,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::Volume)
- .set(TariffNr(1),TariffNr(2))
- );
-
- addNumericFieldWithExtractor(
- "total_tariff{tariff_counter}_at_date",
- "The total water consumption recorded on tariff # by this meter at billing date.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Volume,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::Volume)
- .set(TariffNr(1),TariffNr(2))
- .set(StorageNr(1))
- );
-
- addNumericFieldWithExtractor(
- "flow",
- "The current water flow.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Flow,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::VolumeFlow));
-
- addNumericFieldWithExtractor(
- "total_at_date",
- "The total water consumption recorded at date.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Volume,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::Volume)
- .set(StorageNr(1))
- );
-
- addNumericFieldWithExtractor(
- "at",
- "The last billing period date.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::PointInTime,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::Date)
- .set(StorageNr(1)),
- Unit::DateLT
- );
-
- addNumericFieldWithExtractor(
- "target",
- "The total water consumption recorded at the end of last month.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Volume,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::Volume)
- .set(StorageNr(3))
- );
-
- addNumericFieldWithExtractor(
- "target",
- "The end of last month.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::PointInTime,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::DateTime)
- .set(StorageNr(3))
- );
-
- addNumericFieldWithExtractor(
- "remaining_battery_life",
- "Remaining battery life in years.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Time,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::RemainingBattery),
- Unit::Year);
- }
-}
-
-// Test: HydrusWater hydrus 64646464 NOKEY
-// Comment:
-// telegram=|4E44A5116464646470077AED004005_2F2F01FD08300C13741100007C1300000000FC101300000000FC201300000000726C00000B3B00000002FD748713025A6800C4016D3B177F2ACC011300020000|
-// {"_":"telegram","media":"water","meter":"hydrus","name":"HydrusWater","id":"64646464","total_m3":1.174,"flow_m3h":0,"flow_temperature_c":10.4,"remaining_battery_life_y":13.686797,"status":"OK","target_datetime":"2019-10-31 23:59","target_m3": 0.2,"timestamp":"1111-11-11T11:11:11Z"}
-// |HydrusWater;64646464;1.174;null;OK;1111-11-11 11:11.11
-
-// Test: HydrusVater hydrus 65656565 NOKEY
-// Comment:
-// telegram=|3E44A5116565656570067AFB0030052F2F_0C13503400000DFD110A383731303134423032410B3B00000002FD74DC15C4016D3B178D29CC0113313400002F2F|
-// {"_":"telegram","media":"warm water","meter":"hydrus","name":"HydrusVater","id":"65656565","flow_m3h":0,"customer": "A20B410178","total_m3":3.45,"remaining_battery_life_y":15.321328,"target_datetime":"2020-09-13 23:59","target_m3": 3.431,"status":"OK","timestamp":"1111-11-11T11:11:11Z"}
-// |HydrusVater;65656565;3.45;null;OK;1111-11-11 11:11.11
-
-// Test: HydrusAES hydrus 64745666 NOKEY
-// Comment:
-// telegram=||6644242328001081640E7266567464A51170071F0050052C411A08674048DD6BA82A0DF79FFD401309179A893A1BE3CE8EDC50C2A45CD7AFEC3B4CE765820BE8056C124A17416C3722985FFFF7FCEB7094901AB3A16294B511B9A740C9F9911352B42A72FB3B0C|
-// {"_":"telegram","media":"water","meter":"hydrus","name":"HydrusAES","id":"64745666","total_m3":137.291,"total_tariff1_m3":0,"total_tariff2_m3":137.291,"flow_m3h":0,"flow_temperature_c":24.5,"external_temperature_c":23.9,"total_at_date_m3":128.638,"total_tariff1_at_date_m3":0,"total_tariff2_at_date_m3":128.638,"at_date":"2020-12-31","actuality_duration_s":6673,"operating_time_h":14678,"meter_datetime": "2021-01-23 08:27","status":"OK","timestamp":"1111-11-11T11:11:11Z"}
-// |HydrusAES;64745666;137.291;128.638;OK;1111-11-11 11:11.11
-
-// Test: HydrusIzarRS hydrus 60897379 NOKEY
-// Comment: Hydrus IZAR RS 868 water meter
-// telegram=|1E4424238B07797389607A8F00107D_041312170100426CBF23441344100100|
-// {"_":"telegram","media":"water","meter":"hydrus","name":"HydrusIzarRS","id":"60897379","total_m3":71.442,"at_date": "2021-03-31","total_at_date_m3":69.7,"status":"OK","timestamp":"1111-11-11T11:11:11Z"}
-// |HydrusIzarRS;60897379;71.442;69.7;OK;1111-11-11 11:11.11
-
-// Test: HydrusIzarRSWarm hydrus 60904720 NOKEY
-// Comment: Hydrus IZAR RS 868 water meter warm
-// telegram=|1E4424238B06204790607A2A0010D8_0413DDC00000426CBF23441382BB0000|
-// {"_":"telegram","media":"warm water","meter":"hydrus","name":"HydrusIzarRSWarm","id":"60904720","total_m3":49.373,"total_at_date_m3":48.002,"at_date":"2021-03-31","status":"OK","timestamp":"1111-11-11T11:11:11Z"}
-// |HydrusIzarRSWarm;60904720;49.373;48.002;OK;1111-11-11 11:11.11
-
-// Test: HydrusFoo hydrus 64641820 NOKEY
-// Comment: Negative power values.
-// telegram=|6344A5112018646470078C00D7900F002C256AB59B00F0F13032019092DE7A6A004007102F2F0C13896729004C1323462400CC101300000000CC201323462400426CDF2C0B3B0200F002FD742F0D025AC100C4016D3B17FE29CC01132841290001FD089F|
-// {"_":"telegram","at_date": "2022-12-31","target_datetime": "2023-09-30 23:59","flow_m3h": -0.002,"flow_temperature_c": 19.3,"id": "64641820","media": "water","meter": "hydrus","name": "HydrusFoo","remaining_battery_life_y": 9.240436,"status": "OK","timestamp": "1111-11-11T11:11:11Z","total_at_date_m3":244.623,"target_m3": 294.128,"total_m3": 296.789,"total_tariff1_at_date_m3": 0,"total_tariff2_at_date_m3": 244.623}
-// |HydrusFoo;64641820;296.789;244.623;OK;1111-11-11 11:11.11
diff --git a/components/wmbus_common/driver_iem3000.cpp b/components/wmbus_common/driver_iem3000.cpp
deleted file mode 100644
index 547d22a2..00000000
--- a/components/wmbus_common/driver_iem3000.cpp
+++ /dev/null
@@ -1,1661 +0,0 @@
-/*
- Copyright (C) 2024 Fredrik Öhrström (gpl-3.0-or-later)
-
- 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 .
-*/
-
-#include"meters_common_implementation.h"
-
-namespace
-{
- struct Driver : public virtual MeterCommonImplementation
- {
- Driver(MeterInfo &mi, DriverInfo &di);
- };
-
- static bool ok = staticRegisterDriver([](DriverInfo&di)
- {
- di.setName("iem3000");
- di.setDefaultFields("name,id,total_energy_consumption_kwh,timestamp");
- di.setMeterType(MeterType::ElectricityMeter);
- di.addLinkMode(LinkMode::T1);
- di.addMVT(MANUFACTURER_SEC, 0x02, 0x13);
- di.addMVT(MANUFACTURER_SEC, 0x02, 0x15);
- di.addMVT(MANUFACTURER_SEC, 0x02, 0x18);
- di.setConstructor([](MeterInfo& mi, DriverInfo& di){ return shared_ptr(new Driver(mi, di)); });
- });
-
- Driver::Driver(MeterInfo &mi, DriverInfo &di) : MeterCommonImplementation(mi, di)
- {
- addOptionalLibraryFields("firmware_version,manufacturer,meter_datetime,model_version");
-
- addStringField(
- "status",
- "Status and error flags.",
- PrintProperty::STATUS | PrintProperty::INCLUDE_TPL_STATUS);
-
- addStringFieldWithExtractorAndLookup(
- "error_flags",
- "Error flags.",
- PrintProperty::INJECT_INTO_STATUS,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::ErrorFlags),
- Translate::Lookup()
- .add(Translate::Rule("ERROR_FLAGS", Translate::MapType::IndexToString)
- .set(MaskBits(0xffffff))
- .add(Translate::Map(0x000000, "CODE_101_EEPROM_ERROR", TestBit::Set))
- .add(Translate::Map(0x000010, "CODE_102_NO_CALIBRATION_TABLE", TestBit::Set))
- .add(Translate::Map(0x000020, "CODE_201_MISMATCH_BETWEEN_FREQUENCY_SETTINGS_AND_FREQUENCY_MEASUREMENTS", TestBit::Set))
- .add(Translate::Map(0x000030, "CODE_202_PHASE_SEQUENCE_REVERSED", TestBit::Set))
- .add(Translate::Map(0x000040, "CODE_203_PHASE_SEQUENCE_REVERSED", TestBit::Set))
- .add(Translate::Map(0x000050, "CODE_204_TOTAL_ACTIVE_ENERGY_NEGATIVE_DUE_TO_INCORRECT_V_OR_A_CONNECTIONS", TestBit::Set))
- .add(Translate::Map(0x000060, "CODE_205_DATE_TIME_RESET_DUE_TO_POWER_FAILUER", TestBit::Set))
- .add(Translate::Map(0x000070, "CODE_206_PULSE_MISSING_DUE_TO_OVERSPEED_OF_ENERGY_PULSE_OUTPUT", TestBit::Set))
- .add(Translate::Map(0x000080, "CODE_207_ABNORMAL_INTERNAL_CLOCK_FUNCTION", TestBit::Set))
- .add(Translate::Map(0x000090, "INTERNAL_DATA_BUS_COMUNICATION_ERROR", TestBit::Set))
- ));
-
- addNumericFieldWithExtractor(
- "total_energy_consumption",
- "Total cumulative active imported energy.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Energy,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyEnergyVIF)
- );
-
- addNumericFieldWithExtractor(
- "partial_energy_consumption",
- "Partial cumulative active imported energy.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Energy,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyEnergyVIF)
- .add(VIFCombinableRaw(0x7f0d))
- );
-
- addNumericFieldWithExtractor(
- "partial_reactive_energy_consumption",
- "Partial cumulative reactive imported energy.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Energy,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyEnergyVIF)
- .set(SubUnitNr(1))
- .add(VIFCombinableRaw(0x7f0d))
- );
-
- addNumericFieldWithExtractor(
- "total_energy_consumption_tariff_{tariff_counter}",
- "Total cumulative active imported energy per tariff.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Energy,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyEnergyVIF)
- .set(TariffNr(1),TariffNr(4))
- );
-
- addNumericFieldWithExtractor(
- "total_energy_production",
- "Total cumulative active exported energy.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Energy,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyEnergyVIF)
- .add(VIFCombinableRaw(0x7f09))
- );
-
- addNumericFieldWithExtractor(
- "total_energy_production_tariff_{tariff_counter}",
- "Total cumulative active exported energy per tariff.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Energy,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyEnergyVIF)
- .set(TariffNr(1),TariffNr(4))
- .set(SubUnitNr(1))
- );
-
- addNumericFieldWithExtractor(
- "active_tariff",
- "Active tariff.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Dimensionless,
- VifScaling::None, DifSignedness::Signed,
- FieldMatcher::build()
- .set(DifVifKey("03FF10")),
- Unit::NUMBER
- );
-
- addNumericFieldWithExtractor(
- "ct_numerator",
- "Current transformer ratio (numerator).",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Dimensionless,
- VifScaling::None, DifSignedness::Signed,
- FieldMatcher::build()
- .set(DifVifKey("04FFA015")),
- Unit::NUMBER
- );
-
- addNumericFieldWithExtractor(
- "vt_numerator",
- "Voltage transformer ratio (numerator).",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Dimensionless,
- VifScaling::None, DifSignedness::Signed,
- FieldMatcher::build()
- .set(DifVifKey("04FFA115")),
- Unit::NUMBER
- );
-
- addNumericFieldWithExtractor(
- "ct_denominator",
- "Current transformer ratio (denominator).",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Dimensionless,
- VifScaling::None, DifSignedness::Signed,
- FieldMatcher::build()
- .set(DifVifKey("04FFA215")),
- Unit::NUMBER
- );
-
- addNumericFieldWithExtractor(
- "vt_denominator",
- "Voltage transformer ratio (denominator).",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Dimensionless,
- VifScaling::None, DifSignedness::Signed,
- FieldMatcher::build()
- .set(DifVifKey("04FFA315")),
- Unit::NUMBER
- );
-
- addStringFieldWithExtractorAndLookup(
- "error_flags",
- "Error flags.",
- DEFAULT_PRINT_PROPERTIES | PrintProperty::INJECT_INTO_STATUS,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(DifVifKey("07FFA600")),
- Translate::Lookup()
- .add(Translate::Rule("ERROR_FLAGS", Translate::MapType::BitToString)
- .set(MaskBits(0xffffffffffffffff))
- .set(DefaultMessage("OK"))
- ));
-
- addStringFieldWithExtractorAndLookup(
- "warning_flags",
- "Warning flags.",
- DEFAULT_PRINT_PROPERTIES | PrintProperty::INJECT_INTO_STATUS,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(DifVifKey("07FFA700")),
- Translate::Lookup()
- .add(Translate::Rule("WARNING_FLAGS", Translate::MapType::BitToString)
- .set(MaskBits(0xffffffffffffffff))
- .set(DefaultMessage("OK"))
- ));
-
- addStringFieldWithExtractorAndLookup(
- "information_flags",
- "Information flags.",
- DEFAULT_PRINT_PROPERTIES,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(DifVifKey("07FFA800")),
- Translate::Lookup()
- .add(Translate::Rule("INFORMATION_FLAGS", Translate::MapType::BitToString)
- .set(MaskBits(0xffffffffffffffff))
- .set(DefaultMessage(""))
- ));
-
- addStringFieldWithExtractorAndLookup(
- "alarm_flags",
- "alarm flags.",
- DEFAULT_PRINT_PROPERTIES | PrintProperty::INJECT_INTO_STATUS,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(DifVifKey("07FFA900")),
- Translate::Lookup()
- .add(Translate::Rule("ALARM_FLAGS", Translate::MapType::BitToString)
- .set(MaskBits(0xfffffffffffffff))
- .set(DefaultMessage("OK"))
- ));
-
- addStringFieldWithExtractorAndLookup(
- "unknown_vif_FFAD",
- "Unknown byte.",
- DEFAULT_PRINT_PROPERTIES,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(DifVifKey("01FFAD00")),
- Translate::Lookup()
- .add(Translate::Rule("UNKNOWN", Translate::MapType::BitToString)
- .set(MaskBits(0xff))
- .set(DefaultMessage("OK"))
- ));
-
- addStringFieldWithExtractor(
- "product_no",
- "The meter device product number.",
- DEFAULT_PRINT_PROPERTIES,
- FieldMatcher::build().
- set(DifVifKey("0DFFAA00")));
-
- addNumericFieldWithExtractor(
- "power_fail",
- "Power fail counter.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Dimensionless,
- VifScaling::None, DifSignedness::Signed,
- FieldMatcher::build()
- .set(DifVifKey("04FF9800"))
- );
-
- addNumericFieldWithExtractor(
- "active_consumption",
- "Instantaneous total active imported power.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Power,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyPowerVIF)
- );
-
- addNumericFieldWithExtractor(
- "active_consumption_l1",
- "Instantaneous active imported power for L1 phase.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Power,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyPowerVIF)
- .add(VIFCombinableRaw(0x7f01))
- );
-
- addNumericFieldWithExtractor(
- "active_consumption_l2",
- "Instantaneous active imported power for L2 phase.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Power,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyPowerVIF)
- .add(VIFCombinableRaw(0x7f02))
- );
-
- addNumericFieldWithExtractor(
- "active_consumption_l3",
- "Instantaneous active imported power for L3 phase.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Power,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyPowerVIF)
- .add(VIFCombinableRaw(0x7f03))
- );
-
- addNumericFieldWithExtractor(
- "reactive_consumption",
- "Instantaneous total reactive imported power.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Power,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyPowerVIF)
- .set(SubUnitNr(1))
- );
-
- addNumericFieldWithExtractor(
- "reactive_consumption_l1",
- "Instantaneous reactive imported power for L1 phase.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Power,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyPowerVIF)
- .set(SubUnitNr(2))
- .add(VIFCombinableRaw(0x7f01))
- );
-
- addNumericFieldWithExtractor(
- "reactive_consumption_l2",
- "Instantaneous reactive imported power for L2 phase.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Power,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyPowerVIF)
- .set(SubUnitNr(2))
- .add(VIFCombinableRaw(0x7f02))
- );
-
- addNumericFieldWithExtractor(
- "reactive_consumption_l3",
- "Instantaneous reactive imported power for L3 phase.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Power,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyPowerVIF)
- .set(SubUnitNr(2))
- .add(VIFCombinableRaw(0x7f03))
- );
-
- addNumericFieldWithExtractor(
- "apparent_consumption",
- "Instantaneous total apparent imported power.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Power,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyPowerVIF)
- .set(SubUnitNr(2))
- );
-
- addNumericFieldWithExtractor(
- "apparent_consumption_l1",
- "Instantaneous apparent imported power for L1 phase.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Power,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyPowerVIF)
- .set(SubUnitNr(4))
- .add(VIFCombinableRaw(0x7f01))
- );
-
- addNumericFieldWithExtractor(
- "apparent_consumption_l2",
- "Instantaneous apparent imported power for L2 phase.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Power,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyPowerVIF)
- .set(SubUnitNr(4))
- .add(VIFCombinableRaw(0x7f02))
- );
-
- addNumericFieldWithExtractor(
- "apparent_consumption_l3",
- "Instantaneous apparent imported power for L3 phase.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Power,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyPowerVIF)
- .set(SubUnitNr(4))
- .add(VIFCombinableRaw(0x7f03))
- );
-
- addNumericFieldWithExtractor(
- "voltage_l1_n",
- "Instantaneous voltage between L1 and neutral.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Voltage,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::Voltage)
- .add(VIFCombinableRaw(0x7f01))
- );
-
- addNumericFieldWithExtractor(
- "voltage_l2_n",
- "Instantaneous voltage between L2 and neutral.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Voltage,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::Voltage)
- .add(VIFCombinableRaw(0x7f02))
- );
-
- addNumericFieldWithExtractor(
- "voltage_l3_n",
- "Instantaneous voltage between L3 and neutral.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Voltage,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::Voltage)
- .add(VIFCombinableRaw(0x7f03))
- );
-
- addNumericFieldWithExtractor(
- "voltage_average_ln",
- "Average voltage line to neutral.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Voltage,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::Voltage)
- .add(VIFCombinableRaw(0x7f04))
- );
-
- addNumericFieldWithExtractor(
- "voltage_l1_l2",
- "Instantaneous voltage between L1 and L2.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Voltage,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::Voltage)
- .add(VIFCombinableRaw(0x7f05))
- );
-
- addNumericFieldWithExtractor(
- "voltage_l2_l3",
- "Instantaneous voltage between L2 and L3.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Voltage,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::Voltage)
- .add(VIFCombinableRaw(0x7f06))
- );
-
- addNumericFieldWithExtractor(
- "voltage_l3_l1",
- "Instantaneous voltage between L3 and L1.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Voltage,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::Voltage)
- .add(VIFCombinableRaw(0x7f07))
- );
-
- addNumericFieldWithExtractor(
- "voltage_average_ll",
- "Average voltage line to line.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Voltage,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::Voltage)
- .add(VIFCombinableRaw(0x7f08))
- );
-
- addNumericFieldWithExtractor(
- "current_l1",
- "Instantaneous current in the L1 phase.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Amperage,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::Amperage)
- .add(VIFCombinableRaw(0x7f01))
- );
-
- addNumericFieldWithExtractor(
- "current_l2",
- "Instantaneous current in the L2 phase.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Amperage,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::Amperage)
- .add(VIFCombinableRaw(0x7f02))
- );
-
- addNumericFieldWithExtractor(
- "current_l3",
- "Instantaneous current in the L3 phase.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Amperage,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::Amperage)
- .add(VIFCombinableRaw(0x7f03))
- );
-
- addNumericFieldWithExtractor(
- "current_average",
- "Average current in all phases.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Amperage,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::Amperage)
- .add(VIFCombinableRaw(0x7f00))
- );
-
- addNumericFieldWithExtractor(
- "frequency",
- "Frequency of AC",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Frequency,
- VifScaling::None, DifSignedness::Signed,
- FieldMatcher::build()
- .set(DifVifKey("05FF0B")),
- Unit::HZ);
-
- addNumericFieldWithExtractor(
- "power",
- "Power factor.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Dimensionless,
- VifScaling::None, DifSignedness::Signed,
- FieldMatcher::build()
- .set(DifVifKey("05FF0A")),
- Unit::FACTOR);
-
- addNumericFieldWithExtractor(
- "power_l1",
- "Power factor for phase L1.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Dimensionless,
- VifScaling::None, DifSignedness::Signed,
- FieldMatcher::build()
- .set(DifVifKey("02FFE0FF8100")),
- Unit::FACTOR,
- 0.001
- );
-
- addNumericFieldWithExtractor(
- "power_l2",
- "Power factor for phase L2.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Dimensionless,
- VifScaling::None, DifSignedness::Signed,
- FieldMatcher::build()
- .set(DifVifKey("02FFE0FF8200")),
- Unit::FACTOR,
- 0.001
- );
-
- addNumericFieldWithExtractor(
- "power_l3",
- "Power factor.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Dimensionless,
- VifScaling::None, DifSignedness::Signed,
- FieldMatcher::build()
- .set(DifVifKey("02FFE0FF8300")),
- Unit::FACTOR,
- 0.001
- );
-
- addNumericFieldWithExtractor(
- "power_phase_angle",
- "Total power phase angle.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Angle,
- VifScaling::None, DifSignedness::Signed,
- FieldMatcher::build()
- .set(DifVifKey("02FFD200")),
- Unit::DEGREE,
- 0.1
- );
-
- addNumericFieldWithExtractor(
- "phase_angle_power_l1",
- "Power phase angle for phase L1.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Angle,
- VifScaling::None, DifSignedness::Signed,
- FieldMatcher::build()
- .set(DifVifKey("02FFD2FF8100")),
- Unit::DEGREE,
- 0.1
- );
-
- addNumericFieldWithExtractor(
- "phase_angle_power_l2",
- "Power phase angle for phase L2.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Angle,
- VifScaling::None, DifSignedness::Signed,
- FieldMatcher::build()
- .set(DifVifKey("02FFD2FF8200")),
- Unit::DEGREE,
- 0.1
- );
-
- addNumericFieldWithExtractor(
- "phase_angle_power_l3",
- "Power phase angle for phase L3.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Angle,
- VifScaling::None, DifSignedness::Signed,
- FieldMatcher::build()
- .set(DifVifKey("02FFD2FF8300")),
- Unit::DEGREE,
- 0.1
- );
-
- addNumericFieldWithExtractor(
- "total_reactive_energy_consumption",
- "Total cumulative reactive kvarh imported energy.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Reactive_Energy,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyEnergyVIF)
- .set(SubUnitNr(1)),
- Unit::KVARH
- );
-
- addNumericFieldWithExtractor(
- "total_reactive_energy_consumption_tariff_{tariff_counter}",
- "Total cumulative reactive kvarh imported energy per tariff.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Energy,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyEnergyVIF)
- .set(SubUnitNr(2))
- .set(TariffNr(1),TariffNr(4))
- );
-
- addNumericFieldWithExtractor(
- "total_reactive_energy_production",
- "Total cumulative reactive kvarh exported energy.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Reactive_Energy,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyEnergyVIF)
- .set(SubUnitNr(1))
- .add(VIFCombinableRaw(0x7f09)),
- Unit::KVARH
- );
-
- addNumericFieldWithExtractor(
- "total_reactive_energy_production_tariff_{tariff_counter}",
- "Total cumulative reactive kvarh exported energy per tariff.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Energy,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyEnergyVIF)
- .set(SubUnitNr(3))
- .set(TariffNr(1),TariffNr(4))
- );
-
- addNumericFieldWithExtractor(
- "current_quadrant",
- "The quadrant in which the current is measured.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Dimensionless,
- VifScaling::None, DifSignedness::Signed,
- FieldMatcher::build()
- .set(DifVifKey("01FF9700")),
- Unit::NUMBER
- );
-
- addNumericFieldWithExtractor(
- "current_quadrant_l1",
- "The quadrant in which the current is measured for phase L1.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Dimensionless,
- VifScaling::None, DifSignedness::Signed,
- FieldMatcher::build()
- .set(DifVifKey("01FF97FF8100")),
- Unit::NUMBER
- );
-
- addNumericFieldWithExtractor(
- "current_quadrant_l2",
- "The quadrant in which the current is measured for phase L2.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Dimensionless,
- VifScaling::None, DifSignedness::Signed,
- FieldMatcher::build()
- .set(DifVifKey("01FF97FF8200")),
- Unit::NUMBER
- );
-
- addNumericFieldWithExtractor(
- "current_quadrant_l3",
- "The quadrant in which the current is measured for phase L3.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Dimensionless,
- VifScaling::None, DifSignedness::Signed,
- FieldMatcher::build()
- .set(DifVifKey("01FF97FF8300")),
- Unit::NUMBER
- );
-
- addNumericFieldWithExtractor(
- "input_metering_cumulation",
- "Input metering accumulation.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Dimensionless,
- VifScaling::None, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::CumulationCounter)
- );
-
- addNumericFieldWithExtractor(
- "pulse_duration",
- "Energy pulse duration.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Dimensionless,
- VifScaling::None, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(DifVifKey("03FF2C"))
- );
-
- addNumericFieldWithExtractor(
- "pulse_weight",
- "Energy pulse weight.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Dimensionless,
- VifScaling::None, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(DifVifKey("05FF2E"))
- );
-
- addNumericFieldWithExtractor(
- "pulse_constant",
- "Energy pulse constant.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Dimensionless,
- VifScaling::None, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(DifVifKey("05FF2F"))
- );
-
- addNumericFieldWithExtractor(
- "resettable_energy_consumption",
- "Resettable cumulative active imported energy.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Energy,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyEnergyVIF)
- .add(VIFCombinableRaw(0x7f72))
- );
-
- addNumericFieldWithExtractor(
- "resettable_energy_production",
- "Resettable cumulative active exported energy.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Energy,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyEnergyVIF)
- .set(SubUnitNr(1))
- .add(VIFCombinableRaw(0x7f72))
- );
-
- addNumericFieldWithExtractor(
- "resettable_reactive_energy_consumption",
- "Resettable cumulative reactive imported energy.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Energy,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyEnergyVIF)
- .set(SubUnitNr(2))
- .add(VIFCombinableRaw(0x7f72))
- );
-
- addNumericFieldWithExtractor(
- "resettable_reactive_energy_production",
- "Resettable cumulative reactive exported energy.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Energy,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyEnergyVIF)
- .set(SubUnitNr(3))
- .add(VIFCombinableRaw(0x7f72))
- );
-
- addNumericFieldWithExtractor(
- "reset_energy_consumption",
- "Number of times the resettable energy imported value has been reset.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Dimensionless,
- VifScaling::None, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRaw(0x7f71))
- );
-
- addNumericFieldWithExtractor(
- "reset_energy_production",
- "Number of times the resettable active energy exported value has been reset.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Dimensionless,
- VifScaling::None, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRaw(0x7f71))
- .set(SubUnitNr(1))
- );
-
- addNumericFieldWithExtractor(
- "reset_reactive_energy_consumption",
- "Number of times the resettable reactive energy imported value has been reset.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Dimensionless,
- VifScaling::None, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRaw(0x7f71))
- .set(SubUnitNr(2))
- );
-
- addNumericFieldWithExtractor(
- "reset_reactive_energy_production",
- "Number of times the resettable reactive energy exported value has been reset.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Dimensionless,
- VifScaling::None, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRaw(0x7f71))
- .set(SubUnitNr(3))
- );
-
- addNumericFieldWithExtractor(
- "energy_co2",
- "Energy in co2.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Mass,
- VifScaling::None, DifSignedness::Signed,
- FieldMatcher::build()
- .set(DifVifKey("0EFFF9C400")),
- Unit::KG,
- 0.01
- );
-
- addNumericFieldWithExtractor(
- "co2_conversion",
- "CO2 conversion factor (kg * 10-3 /kWh).",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Dimensionless,
- VifScaling::None, DifSignedness::Signed,
- FieldMatcher::build()
- .set(DifVifKey("04FFA400")),
- Unit::FACTOR
- );
-
- addNumericFieldWithExtractor(
- "energy_currency",
- "Energy in currency.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Dimensionless,
- VifScaling::None, DifSignedness::Signed,
- FieldMatcher::build()
- .set(DifVifKey("0EFFF9C900"))
- );
-
- addNumericFieldWithExtractor(
- "currency_conversion",
- "Currency conversion factor (curr * 10-3 /kWh).",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Dimensionless,
- VifScaling::None, DifSignedness::Signed,
- FieldMatcher::build()
- .set(DifVifKey("04FFA500")),
- Unit::FACTOR
- );
-
- addNumericFieldWithExtractor(
- "total_apparent_energy_consumption",
- "Total cumulative apparent kvah imported energy.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Apparent_Energy,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyEnergyVIF)
- .set(SubUnitNr(4)),
- Unit::KVAH
- );
-
- addNumericFieldWithExtractor(
- "total_apparent_energy_production",
- "Total cumulative apparent kvah exported energy.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Apparent_Energy,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyEnergyVIF)
- .set(SubUnitNr(5)),
- Unit::KVAH
- );
-
- addNumericFieldWithExtractor(
- "total_energy_consumption_l1",
- "Total imported active energy for L1 phase.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Energy,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyEnergyVIF)
- .add(VIFCombinableRaw(0x7f01))
- );
-
- addNumericFieldWithExtractor(
- "total_energy_consumption_l2",
- "Total imported active energy for L2 phase.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Energy,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyEnergyVIF)
- .add(VIFCombinableRaw(0x7f02))
- );
-
- addNumericFieldWithExtractor(
- "total_energy_consumption_l3",
- "Total imported active energy for L3 phase.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Energy,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyEnergyVIF)
- .add(VIFCombinableRaw(0x7f03))
- );
-
- addNumericFieldWithExtractor(
- "total_reactive_energy_consumption_l1",
- "Total imported reactive energy for L1 phase.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Energy,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyEnergyVIF)
- .set(SubUnitNr(2))
- .add(VIFCombinableRaw(0x7f01))
- );
-
- addNumericFieldWithExtractor(
- "total_reactive_energy_consumption_l2",
- "Total imported reactive energy for L2 phase.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Energy,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyEnergyVIF)
- .set(SubUnitNr(2))
- .add(VIFCombinableRaw(0x7f02))
- );
-
- addNumericFieldWithExtractor(
- "total_reactive_energy_consumption_l3",
- "Total imported reactive energy for L3 phase.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Energy,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyEnergyVIF)
- .set(SubUnitNr(2))
- .add(VIFCombinableRaw(0x7f03))
- );
-
- addNumericFieldWithExtractor(
- "total_apparent_energy_consumption_l1",
- "Total imported apparent energy for L1 phase.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Energy,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyEnergyVIF)
- .set(SubUnitNr(4))
- .add(VIFCombinableRaw(0x7f01))
- );
-
- addNumericFieldWithExtractor(
- "total_apparent_energy_consumption_l2",
- "Total imported apparent energy for L2 phase.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Energy,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyEnergyVIF)
- .set(SubUnitNr(4))
- .add(VIFCombinableRaw(0x7f02))
- );
-
- addNumericFieldWithExtractor(
- "total_apparent_energy_consumption_l3",
- "Total imported apparent energy for L3 phase.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Energy,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyEnergyVIF)
- .set(SubUnitNr(4))
- .add(VIFCombinableRaw(0x7f03))
- );
-
- addNumericFieldWithExtractor(
- "total_energy_production_l1",
- "Total exported active energy for L1 phase.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Energy,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyEnergyVIF)
- .set(SubUnitNr(1))
- .add(VIFCombinableRaw(0x7f01))
- );
-
- addNumericFieldWithExtractor(
- "total_energy_production_l2",
- "Total exported active energy for L2 phase.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Energy,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyEnergyVIF)
- .set(SubUnitNr(1))
- .add(VIFCombinableRaw(0x7f02))
- );
-
- addNumericFieldWithExtractor(
- "total_energy_production_l3",
- "Total exported active energy for L3 phase.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Energy,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyEnergyVIF)
- .set(SubUnitNr(1))
- .add(VIFCombinableRaw(0x7f03))
- );
-
- addNumericFieldWithExtractor(
- "total_reactive_energy_production_l1",
- "Total exported reactive energy for L1 phase.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Energy,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyEnergyVIF)
- .set(SubUnitNr(3))
- .add(VIFCombinableRaw(0x7f01))
- );
-
- addNumericFieldWithExtractor(
- "total_reactive_energy_production_l2",
- "Total exported reactive energy for L2 phase.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Energy,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyEnergyVIF)
- .set(SubUnitNr(3))
- .add(VIFCombinableRaw(0x7f02))
- );
-
- addNumericFieldWithExtractor(
- "total_reactive_energy_production_l3",
- "Total exported reactive energy for L3 phase.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Energy,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyEnergyVIF)
- .set(SubUnitNr(3))
- .add(VIFCombinableRaw(0x7f03))
- );
-
- addNumericFieldWithExtractor(
- "total_apparent_energy_production_l1",
- "Total exported apparent energy for L1 phase.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Energy,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyEnergyVIF)
- .set(SubUnitNr(5))
- .add(VIFCombinableRaw(0x7f01))
- );
-
- addNumericFieldWithExtractor(
- "total_apparent_energy_production_l2",
- "Total exported apparent energy for L2 phase.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Energy,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyEnergyVIF)
- .set(SubUnitNr(5))
- .add(VIFCombinableRaw(0x7f02))
- );
-
- addNumericFieldWithExtractor(
- "total_apparent_energy_production_l3",
- "Total exported apparent energy for L3 phase.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Energy,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyEnergyVIF)
- .set(SubUnitNr(5))
- .add(VIFCombinableRaw(0x7f03))
- );
-
- addNumericFieldWithExtractor(
- "total_net_energy",
- "Active net energy total.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Energy,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyEnergyVIF)
- .set(SubUnitNr(6))
- );
-
- addNumericFieldWithExtractor(
- "total_net_energy_l1",
- "Active net energy total for phase L1.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Energy,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyEnergyVIF)
- .set(SubUnitNr(6))
- .add(VIFCombinableRaw(0x7f01))
- );
-
- addNumericFieldWithExtractor(
- "total_net_energy_l2",
- "Active net energy total for phase L2.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Energy,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyEnergyVIF)
- .set(SubUnitNr(6))
- .add(VIFCombinableRaw(0x7f02))
- );
-
- addNumericFieldWithExtractor(
- "total_net_energy_l3",
- "Active net energy total for phase L3.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Energy,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyEnergyVIF)
- .set(SubUnitNr(6))
- .add(VIFCombinableRaw(0x7f03))
- );
-
- addNumericFieldWithExtractor(
- "total_net_reactive_energy",
- "Active net energy total.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Energy,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyEnergyVIF)
- .set(SubUnitNr(7)),
- Unit::KVARH
- );
-
- addNumericFieldWithExtractor(
- "total_net_reactive_energy_l1",
- "Active net reactive energy total for phase L1.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Energy,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyEnergyVIF)
- .set(SubUnitNr(7))
- .add(VIFCombinableRaw(0x7f01)),
- Unit::KVARH
- );
-
- addNumericFieldWithExtractor(
- "total_net_reactive_energy_l2",
- "Active net reactive energy total for phase L2.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Energy,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyEnergyVIF)
- .set(SubUnitNr(7))
- .add(VIFCombinableRaw(0x7f02)),
- Unit::KVARH
- );
-
- addNumericFieldWithExtractor(
- "total_net_reactive_energy_l3",
- "Active net reactive energy total for phase L3.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Energy,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyEnergyVIF)
- .set(SubUnitNr(7))
- .add(VIFCombinableRaw(0x7f03)),
- Unit::KVARH
- );
-
- addNumericFieldWithExtractor(
- "total_net_apparent_energy",
- "Active net energy total.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Energy,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyEnergyVIF)
- .set(SubUnitNr(8)),
- Unit::KVAH
- );
-
- addNumericFieldWithExtractor(
- "total_net_apparent_energy_l1",
- "Active net apparent energy total for phase L1.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Energy,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyEnergyVIF)
- .set(SubUnitNr(8))
- .add(VIFCombinableRaw(0x7f01)),
- Unit::KVAH
- );
-
- addNumericFieldWithExtractor(
- "total_net_apparent_energy_l2",
- "Active net apparent energy total for phase L2.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Energy,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyEnergyVIF)
- .set(SubUnitNr(8))
- .add(VIFCombinableRaw(0x7f02)),
- Unit::KVAH
- );
-
- addNumericFieldWithExtractor(
- "total_net_apparent_energy_l3",
- "Active net apparent energy total for phase L3.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Energy,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyEnergyVIF)
- .set(SubUnitNr(8))
- .add(VIFCombinableRaw(0x7f03)),
- Unit::KVAH
- );
-
- addNumericFieldWithExtractor(
- "last_partial_energy_reset",
- "Date and time of last partial energy reset,",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::PointInTime,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::DateTime)
- .add(VIFCombinableRaw(0x7f0c))
- );
-
- addNumericFieldWithExtractor(
- "last_input_metering_reset",
- "Date and time of last input metering reset.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::PointInTime,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::DateTime)
- .add(VIFCombinableRaw(0x7f0e))
- );
-
- addStringFieldWithExtractorAndLookup(
- "digital_input",
- "Digital input status.",
- DEFAULT_PRINT_PROPERTIES,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::DigitalInput),
- Translate::Lookup()
- .add(Translate::Rule("INPUT", Translate::MapType::BitToString)
- .set(MaskBits(0xffffff))
- ));
-
- addStringFieldWithExtractorAndLookup(
- "digital_input_status",
- "Digital input status.",
- DEFAULT_PRINT_PROPERTIES,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(DifVifKey("02FF32")),
- Translate::Lookup()
- .add(Translate::Rule("INPUT_STATUS", Translate::MapType::BitToString)
- .set(MaskBits(0xff))
- ));
-
- addStringFieldWithExtractorAndLookup(
- "digital_output",
- "Digital output status.",
- DEFAULT_PRINT_PROPERTIES,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::DigitalOutput),
- Translate::Lookup()
- .add(Translate::Rule("OUTPUT", Translate::MapType::BitToString)
- .set(MaskBits(0xffffff))
- ));
-
- addStringFieldWithExtractorAndLookup(
- "digital_output_association",
- "Digital output association.",
- DEFAULT_PRINT_PROPERTIES,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(DifVifKey("03FF2D")),
- Translate::Lookup()
- .add(Translate::Rule("OUTPUT_ASSOCIATION", Translate::MapType::BitToString)
- .set(MaskBits(0xffffff))
- ));
-
- addStringFieldWithExtractorAndLookup(
- "digital_input_association",
- "Digital input association.",
- DEFAULT_PRINT_PROPERTIES,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(DifVifKey("03FF30")),
- Translate::Lookup()
- .add(Translate::Rule("INPUT_ASSOCIATION", Translate::MapType::BitToString)
- .set(MaskBits(0xffffff))
- ));
-
- addStringFieldWithExtractorAndLookup(
- "digital_output_association",
- "Digital output association.",
- DEFAULT_PRINT_PROPERTIES,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(DifVifKey( "02FF36")),
- Translate::Lookup()
- .add(Translate::Rule("OUTPUT_ASSOCIATION", Translate::MapType::BitToString)
- .set(MaskBits(0xffff))
- ));
-
- addStringFieldWithExtractorAndLookup(
- "overload_alarm_setup",
- "Overload alarm setup.",
- DEFAULT_PRINT_PROPERTIES,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(DifVifKey("02FF34")),
- Translate::Lookup()
- .add(Translate::Rule("OVERLOAD_ALARM", Translate::MapType::BitToString)
- .set(MaskBits(0xff))
- ));
-
- addNumericFieldWithExtractor(
- "pickup_setpoint",
- "Pickup setpoint.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Dimensionless,
- VifScaling::None, DifSignedness::Signed,
- FieldMatcher::build()
- .set(DifVifKey("05FF35")));
-
- addStringFieldWithExtractorAndLookup(
- "activated_status",
- "Activated status.",
- DEFAULT_PRINT_PROPERTIES,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(DifVifKey("02FF37")),
- Translate::Lookup()
- .add(Translate::Rule("ACTIVATED_STATUS", Translate::MapType::BitToString)
- .set(MaskBits(0xff))
- ));
-
- addStringFieldWithExtractorAndLookup(
- "unack_status",
- "Unacknowledged status.",
- DEFAULT_PRINT_PROPERTIES,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(DifVifKey("02FF38")),
- Translate::Lookup()
- .add(Translate::Rule("UNACK_STATUS", Translate::MapType::BitToString)
- .set(MaskBits(0xff))
- ));
-
- addNumericFieldWithExtractor(
- "last_alarm",
- "Date and time of last alarm.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::PointInTime,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::DateTime)
- .add(VIFCombinableRaw(0x7f39))
- );
-
- addNumericFieldWithExtractor(
- "last_alarm",
- "Last alarm value.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Dimensionless,
- VifScaling::None, DifSignedness::Signed,
- FieldMatcher::build()
- .set(DifVifKey("05FF3A")),
- Unit::NUMBER);
-
- addNumericFieldWithExtractor(
- "operating_time",
- "Operating time. Unit is unknown, please fix!",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Time,
- VifScaling::None, DifSignedness::Signed,
- FieldMatcher::build()
- .set(DifVifKey("06FF20")),
- Unit::Year);
-
- addNumericFieldWithExtractor(
- "phases",
- "Number of phases.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Dimensionless,
- VifScaling::None, DifSignedness::Signed,
- FieldMatcher::build()
- .set(DifVifKey("03FF21")));
-
- addNumericFieldWithExtractor(
- "wires",
- "Number of wires.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Dimensionless,
- VifScaling::None, DifSignedness::Signed,
- FieldMatcher::build()
- .set(DifVifKey("03FF22")),
- Unit::NUMBER);
-
- addNumericFieldWithExtractor(
- "vts",
- "Number of voltaget transformers VT:s.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Dimensionless,
- VifScaling::None, DifSignedness::Signed,
- FieldMatcher::build()
- .set(DifVifKey("03FF25")));
-
- addNumericFieldWithExtractor(
- "vt_primary",
- "Primary voltage transformer VT.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Dimensionless,
- VifScaling::None, DifSignedness::Signed,
- FieldMatcher::build()
- .set(DifVifKey("05FF26")),
- Unit::NUMBER);
-
- addNumericFieldWithExtractor(
- "vt_secondary",
- "Secondary voltage transformer VT.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Dimensionless,
- VifScaling::None, DifSignedness::Signed,
- FieldMatcher::build()
- .set(DifVifKey("03FF27")),
- Unit::NUMBER);
-
- addNumericFieldWithExtractor(
- "cts",
- "Number of current transformers CT:s.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Dimensionless,
- VifScaling::None, DifSignedness::Signed,
- FieldMatcher::build()
- .set(DifVifKey("03FF28")));
-
- addNumericFieldWithExtractor(
- "ct_primary",
- "Primary current transformer CT.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Dimensionless,
- VifScaling::None, DifSignedness::Signed,
- FieldMatcher::build()
- .set(DifVifKey("03FF29")),
- Unit::NUMBER);
-
- addNumericFieldWithExtractor(
- "ct_secondary",
- "Secondary current transformer CT.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Dimensionless,
- VifScaling::None, DifSignedness::Signed,
- FieldMatcher::build()
- .set(DifVifKey("03FF2A")),
- Unit::NUMBER);
-
- addNumericFieldWithExtractor(
- "vt_connection_type",
- "Voltage transformer connection type.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Dimensionless,
- VifScaling::None, DifSignedness::Signed,
- FieldMatcher::build()
- .set(DifVifKey("03FF2B")),
- Unit::NUMBER);
-
- addStringFieldWithExtractorAndLookup(
- "power_system_configuration",
- "Power system configuration.",
- DEFAULT_PRINT_PROPERTIES,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(DifVifKey("03FF23")),
- Translate::Lookup()
- .add(Translate::Rule("POWER_SYS_CONFIG", Translate::MapType::BitToString)
- .set(MaskBits(0xffffff))
- ));
-
- addNumericFieldWithExtractor(
- "nominal_frequency",
- "Nominal frequency.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Frequency,
- VifScaling::None, DifSignedness::Signed,
- FieldMatcher::build()
- .set(DifVifKey("03FF24")),
- Unit::HZ);
-
- }
-}
diff --git a/components/wmbus_common/driver_itron.cpp b/components/wmbus_common/driver_itron.cpp
deleted file mode 100644
index d6249ae9..00000000
--- a/components/wmbus_common/driver_itron.cpp
+++ /dev/null
@@ -1,158 +0,0 @@
-/*
- Copyright (C) 2022-2023 Fredrik Öhrström (gpl-3.0-or-later)
-
- 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 .
-*/
-
-#include"meters_common_implementation.h"
-
-namespace
-{
- struct Driver : public virtual MeterCommonImplementation
- {
- Driver(MeterInfo &mi, DriverInfo &di);
- };
-
- static bool ok = staticRegisterDriver([](DriverInfo&di)
- {
- di.setName("itron");
- di.setDefaultFields("name,id,total_m3,target_m3,timestamp");
- di.setMeterType(MeterType::WaterMeter);
- di.addLinkMode(LinkMode::T1);
- di.addMVT(MANUFACTURER_ITW, 0x07, 0x00);
- di.addMVT(MANUFACTURER_ITW, 0x07, 0x03);
- di.addMVT(MANUFACTURER_ITW, 0x07, 0x33);
- di.addMVT(MANUFACTURER_ITW, 0x16, 0x00);
- di.setConstructor([](MeterInfo& mi, DriverInfo& di){ return shared_ptr(new Driver(mi, di)); });
- });
-
- Driver::Driver(MeterInfo &mi, DriverInfo &di) : MeterCommonImplementation(mi, di)
- {
- setMeterType(MeterType::WaterMeter);
-
- setExpectedTPLSecurityMode(TPLSecurityMode::AES_CBC_IV);
-
- addLinkMode(LinkMode::T1);
-
- addOptionalLibraryFields("enhanced_id,meter_datetime");
- addOptionalLibraryFields("total_m3,total_backward_m3,volume_flow_m3h");
-
- addStringFieldWithExtractorAndLookup(
- "status",
- "Status and error flags.",
- DEFAULT_PRINT_PROPERTIES | PrintProperty::STATUS | PrintProperty::INCLUDE_TPL_STATUS,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::ErrorFlags)
- .add(VIFCombinable::RecordErrorCodeMeterToController),
- {
- {
- {
- "ERROR_FLAGS",
- Translate::MapType::BitToString,
- AlwaysTrigger, MaskBits(0xffffff),
- "OK",
- {
- // No known layout for field
- }
- },
- },
- });
-
- addNumericFieldWithExtractor(
- "target",
- "The total water consumption recorded at the end of previous billing period.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Volume,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::Volume)
- .set(StorageNr(1))
- );
-
- addStringFieldWithExtractor(
- "target_date",
- "Date when previous billing period ended.",
- DEFAULT_PRINT_PROPERTIES,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::Date)
- .set(StorageNr(1))
- );
-
- addStringFieldWithExtractorAndLookup(
- "unknown_a",
- "Unknown flags.",
- DEFAULT_PRINT_PROPERTIES,
- FieldMatcher::build()
- .set(DifVifKey("047F")),
- {
- {
- {
- "WOOTA",
- Translate::MapType::BitToString,
- AlwaysTrigger, MaskBits(0xffffffff),
- "",
- {
- // No known layout for field
- }
- },
- },
- });
-
- addStringFieldWithExtractorAndLookup(
- "unknown_b",
- "Unknown flags.",
- DEFAULT_PRINT_PROPERTIES,
- FieldMatcher::build()
- .set(DifVifKey("027F")),
- {
- {
- {
- "WOOTB",
- Translate::MapType::BitToString,
- AlwaysTrigger, MaskBits(0xffff),
- "",
- {
- // No known layout for field
- }
- },
- },
- });
-
- }
-}
-
-// Test: SomeWater itron 12345698 NOKEY
-// Comment: Test ITRON T1 telegram not encrypted, which has no 2f2f markers.
-// telegram=|384497269856341203077AD90000A0#0413FD110000066D2C1AA1D521004413300F0000426CBF2C047F0000060C027F862A0E79678372082100|
-// {"_":"telegram","media":"water","meter":"itron","name":"SomeWater","id":"12345698","enhanced_id":"002108728367","meter_datetime":"2022-01-21 01:26:44","total_m3":4.605,"status":"OK","target_m3":3.888,"target_date":"2021-12-31","unknown_a":"WOOTA_C060000","unknown_b":"WOOTB_2A86","timestamp":"1111-11-11T11:11:11Z"}
-// |SomeWater;12345698;4.605;3.888;1111-11-11 11:11.11
-
-// Test: MoreWater itron 18000056 NOKEY
-// telegram=|46449726560000183307725600001897263307AF0030052F2F_066D0E1015C82A000C13771252000C933C000000000B3B0400004C1361045200426CC12A03FD971C0000002F2F2F|
-// {"_":"telegram","media":"water","meter":"itron","name":"MoreWater","id":"18000056","meter_datetime":"2022-10-08 21:16:14","total_m3":521.277,"total_backward_m3":0,"volume_flow_m3h":0.004,"status":"OK","target_m3":520.461,"target_date":"2022-10-01","timestamp":"1111-11-11T11:11:11Z"}
-// |MoreWater;18000056;521.277;520.461;1111-11-11 11:11.11
-
-// Test: AnyWater itron 20310959 NOKEY
-// telegram=|384497265909312000077a930000a0041360B50100066d101295f427004413ac570100426cdf2c047f0000060c027f6c2a0e79000000000000|
-// {"_":"telegram","enhanced_id": "000000000000","id": "20310959","media": "water","meter": "itron","meter_datetime": "2023-07-20 21:18:16","name": "AnyWater","status": "OK","target_date": "2022-12-31","target_m3": 87.98,"timestamp": "1111-11-11T11:11:11Z","total_m3": 111.968,"unknown_a": "WOOTA_C060000","unknown_b": "WOOTB_2A6C"}
-// |AnyWater;20310959;111.968;87.98;1111-11-11 11:11.11
-
-// Test: ColdWaterMeter itron 23362098 NOKEY
-// Comment: Allmess cold water with Itron Module programmed with type 0x16
-// telegram=|3A4497269820362300167AF60020A52F2F_04132E100000066D03260DE12B007413FEFEFEFE426C1F01047F1600060C027F9A2A0E79187103002300|
-// {"_":"telegram","enhanced_id": "002300037118", "id": "23362098", "media": "cold water", "meter": "itron", "meter_datetime": "2023-11-01 13:38:03", "name": "ColdWaterMeter", "status": "OK", "target_date": "2000-01-31", "timestamp": "1111-11-11T11:11:11Z", "total_m3": 4.142,"unknown_a": "WOOTA_C060016","unknown_b": "WOOTB_2A9A" }
-// |ColdWaterMeter;23362098;4.142;null;1111-11-11 11:11.11
diff --git a/components/wmbus_common/driver_iwmtx5.cpp b/components/wmbus_common/driver_iwmtx5.cpp
deleted file mode 100644
index 220e331c..00000000
--- a/components/wmbus_common/driver_iwmtx5.cpp
+++ /dev/null
@@ -1,71 +0,0 @@
-/*
- Copyright (C) 2023 Fredrik Öhrström (gpl-3.0-or-later)
-
- 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 .
-*/
-
-#include"meters_common_implementation.h"
-
-namespace
-{
- struct Driver : public virtual MeterCommonImplementation
- {
- Driver(MeterInfo &mi, DriverInfo &di);
- };
-
- static bool ok = staticRegisterDriver([](DriverInfo&di)
- {
- di.setName("iwmtx5");
- di.setDefaultFields("name,id,status,total_m3,timestamp");
-
- di.setMeterType(MeterType::WaterMeter);
- di.addLinkMode(LinkMode::T1);
- di.addMVT(MANUFACTURER_BMT, 0x07, 0x18);
- di.addMVT(MANUFACTURER_BMT, 0x06, 0x18);
- di.setConstructor([](MeterInfo& mi, DriverInfo& di){ return shared_ptr(new Driver(mi, di)); });
- });
-
- Driver::Driver(MeterInfo &mi, DriverInfo &di) : MeterCommonImplementation(mi, di)
- {
- setMfctTPLStatusBits(
- Translate::Lookup()
- .add(Translate::Rule("TPL_STS", Translate::MapType::BitToString)
- .set(MaskBits(0xe0))
- .set(DefaultMessage("OK"))
- .add(Translate::Map(0x40 ,"TAMPER", TestBit::Set))));
-
- addOptionalLibraryFields("meter_datetime");
- addOptionalLibraryFields("total_m3");
-
- addStringField(
- "status",
- "Status and error flags.",
- PrintProperty::STATUS | PrintProperty::INCLUDE_TPL_STATUS);
- }
-}
-
-// Test: WaterWater iwmtx5 22917370 00000000000000000000000000000000
-// telegram=|5144b4097073912218078c00247a0308400571e9615249ede52eaae09f61908f027c3877f3330ae9079528b23173ce124bcc255393e60b173c0a9f274c42dd92e4b23c14e8a41f042903358df01dd9268ad4|
-// {"_":"telegram","id": "22917370","media": "water","meter": "iwmtx5","meter_datetime": "2023-05-11 10:38:24","name": "WaterWater","status": "PERMANENT_ERROR","timestamp": "1111-11-11T11:11:11Z","total_m3": 0.025}
-// |WaterWater;22917370;PERMANENT_ERROR;0.025;1111-11-11 11:11.11
-
-// Test: WarmWater2 iwmtx5 23329344 NOKEY
-// telegram=|4244B4094493322318068C005B7A1C0000000C13072000000F05170000000000000000000000000000000000000000009D0000C20000C20000C8000000000000000000|
-// {"_":"telegram","id": "23329344","media": "warm water","meter": "iwmtx5","name": "WarmWater2","status": "OK","timestamp": "1111-11-11T11:11:11Z","total_m3": 2.007}
-// |WarmWater2;23329344;OK;2.007;1111-11-11 11:11.11
-
-// Test: WarmWater3 iwmtx5 24360570 NOKEY
-// telegram=|4A44B4097005362418068C00647A7D400000066D210A673139000C13589402000F0582030000550200660300C004000006005407009B0800030A00C10A00000000000000010000FC000000|
-// {"_": "telegram","id": "24360570","media": "warm water","meter": "iwmtx5","meter_datetime": "2025-09-17 07:10:33","name": "WarmWater3","status": "TAMPER","timestamp": "1111-11-11T11:11:11Z","total_m3": 29.458}
-// |WarmWater3;24360570;TAMPER;29.458;1111-11-11 11:11.11
diff --git a/components/wmbus_common/driver_izar.cpp b/components/wmbus_common/driver_izar.cpp
deleted file mode 100644
index b271a1f0..00000000
--- a/components/wmbus_common/driver_izar.cpp
+++ /dev/null
@@ -1,318 +0,0 @@
-/*
- Copyright (C) 2019 Jacek Tomasiak (gpl-3.0-or-later)
- Copyright (C) 2020-2023 Fredrik Öhrström (gpl-3.0-or-later)
- Copyright (C) 2021 Vincent Privat (gpl-3.0-or-later)
-
- 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 .
-*/
-
-#include"meters_common_implementation.h"
-#include"manufacturer_specificities.h"
-
-namespace
-{
- /** Contains all the booleans required to store the alarms of a PRIOS device. */
- struct IzarAlarms
- {
- bool general_alarm;
- bool leakage_currently;
- bool leakage_previously;
- bool meter_blocked;
- bool back_flow;
- bool underflow;
- bool overflow;
- bool submarine;
- bool sensor_fraud_currently;
- bool sensor_fraud_previously;
- bool mechanical_fraud_currently;
- bool mechanical_fraud_previously;
- };
-
- struct Driver : public virtual MeterCommonImplementation
- {
- Driver(MeterInfo &mi, DriverInfo &di);
-
- void processContent(Telegram *t);
-
- private:
-
- std::string currentAlarmsText(IzarAlarms &alarms);
- std::string previousAlarmsText(IzarAlarms &alarms);
-
- std::vector decodePrios(const std::vector &origin, const std::vector &payload, uint32_t key);
-
- std::vector keys;
- };
-
- static bool ok = staticRegisterDriver([](DriverInfo&di)
- {
- di.setName("izar");
- di.setDefaultFields("name,id,prefix,serial_number,total_m3,last_month_total_m3,"
- "last_month_measure_date,"
- "remaining_battery_life_y,"
- "current_alarms,"
- "previous_alarms,"
- "transmit_period_s,"
- "manufacture_year,timestamp");
- di.setMeterType(MeterType::WaterMeter);
- di.addLinkMode(LinkMode::T1);
- di.addMVT(MANUFACTURER_HYD, 0x07, 0x85);
- di.addMVT(MANUFACTURER_SAP, 0x15, -1);
- di.addMVT(MANUFACTURER_SAP, 0x04, -1);
- di.addMVT(MANUFACTURER_SAP, 0x07, 0x00);
- di.addMVT(MANUFACTURER_DME, 0x07, 0x78);
- di.addMVT(MANUFACTURER_DME, 0x06, 0x78);
- di.addMVT(MANUFACTURER_HYD, 0x07, 0x86);
- di.usesProcessContent();
-
- di.setConstructor([](MeterInfo& mi, DriverInfo& di){ return shared_ptr(new Driver(mi, di)); });
- });
-
- Driver::Driver(MeterInfo &mi, DriverInfo &di) : MeterCommonImplementation(mi, di)
- {
- initializeDiehlDefaultKeySupport(meterKeys()->confidentiality_key, keys);
-
- addStringField("prefix",
- "The alphanumeric prefix printed before serial number on device.",
- DEFAULT_PRINT_PROPERTIES);
-
- addStringField("serial_number",
- "The meter serial number.",
- DEFAULT_PRINT_PROPERTIES);
-
- addNumericField("total",
- Quantity::Volume,
- DEFAULT_PRINT_PROPERTIES,
- "The total water consumption recorded by this meter.");
-
- addNumericField("last_month_total",
- Quantity::Volume,
- DEFAULT_PRINT_PROPERTIES,
- "The total water consumption recorded by this meter around end of last month.");
-
- addStringField("last_month_measure_date",
- "The date when the meter recorded the most recent billing value.",
- DEFAULT_PRINT_PROPERTIES);
-
- addNumericField("remaining_battery_life",
- Quantity::Time,
- DEFAULT_PRINT_PROPERTIES,
- "How many more years the battery is expected to last",
- Unit::Year);
- addStringField("current_alarms",
- "Alarms currently reported by the meter.",
- DEFAULT_PRINT_PROPERTIES);
-
- addStringField("previous_alarms",
- "Alarms previously reported by the meter.",
- DEFAULT_PRINT_PROPERTIES);
-
- addNumericField("transmit_period", Quantity::Time,
- DEFAULT_PRINT_PROPERTIES,
- "The period at which the meter transmits its data.",
- Unit::Second);
-
- addStringField("manufacture_year",
- "The year during which the meter was manufactured.",
- DEFAULT_PRINT_PROPERTIES);
- }
-
- std::string Driver::currentAlarmsText(IzarAlarms &alarms)
- {
- std::string s;
- if (alarms.leakage_currently) {
- s.append("leakage,");
- }
- if (alarms.meter_blocked) {
- s.append("meter_blocked,");
- }
- if (alarms.back_flow) {
- s.append("back_flow,");
- }
- if (alarms.underflow) {
- s.append("underflow,");
- }
- if (alarms.overflow) {
- s.append("overflow,");
- }
- if (alarms.submarine) {
- s.append("submarine,");
- }
- if (alarms.sensor_fraud_currently) {
- s.append("sensor_fraud,");
- }
- if (alarms.mechanical_fraud_currently) {
- s.append("mechanical_fraud,");
- }
- if (s.length() > 0) {
- if (alarms.general_alarm) {
- return "general_alarm";
- }
- s.pop_back();
- return s;
- }
- return "no_alarm";
- }
-
- std::string Driver::previousAlarmsText(IzarAlarms &alarms)
- {
- std::string s;
- if (alarms.leakage_previously) {
- s.append("leakage,");
- }
- if (alarms.sensor_fraud_previously) {
- s.append("sensor_fraud,");
- }
- if (alarms.mechanical_fraud_previously) {
- s.append("mechanical_fraud,");
- }
- if (s.length() > 0) {
- s.pop_back();
- return s;
- }
- return "no_alarm";
- }
-
- void Driver::processContent(Telegram *t)
- {
- std::vector frame;
- t->extractFrame(&frame);
- std::vector origin = t->original.empty() ? frame : t->original;
-
- std::vector decoded_content;
- for (auto& key : keys) {
- decoded_content = decodePrios(origin, frame, key);
- if (!decoded_content.empty())
- break;
- }
-
- debug("(izar) Decoded PRIOS data: %s\n", bin2hex(decoded_content).c_str());
-
- if (decoded_content.empty())
- {
- if (t->beingAnalyzed() == false)
- {
- warning("(izar) Decoding PRIOS data failed. Ignoring telegram.\n");
- }
- return;
- }
-
- if (detectDiehlFrameInterpretation(frame) == DiehlFrameInterpretation::SAP_PRIOS)
- {
- std::string digits = std::to_string((origin[7] & 0x03) << 24 | origin[6] << 16 | origin[5] << 8 | origin[4]);
- digits = tostrprintf("%08d", atoi(digits.c_str())); // Make sure we are on 8 digits for 200x years
- // get the manufacture year
- uint8_t yy = atoi(digits.substr(0, 2).c_str());
- int manufacture_year = yy > 70 ? (1900 + yy) : (2000 + yy); // Maybe to adjust in 2070, if this code stills lives :D
- setStringValue("manufacture_year", tostrprintf("%d", manufacture_year), NULL);
-
- // get the serial number
- uint32_t serial_number = atoi(digits.substr(2, digits.size()).c_str());
- setStringValue("serial_number", tostrprintf("%06d", serial_number), NULL);
-
- // get letters
- uchar supplier_code = '@' + (((origin[9] & 0x0F) << 1) | (origin[8] >> 7));
- uchar meter_type = '@' + ((origin[8] & 0x7C) >> 2);
- uchar diameter = '@' + (((origin[8] & 0x03) << 3) | (origin[7] >> 5));
- // build the prefix
- std::string prefix = tostrprintf("%c%02d%c%c", supplier_code, yy, meter_type, diameter);
- setStringValue("prefix", prefix, NULL);
- }
-
- // get the remaining battery life (in year) and transmission period (in seconds)
- double remaining_battery_life = (frame[12] & 0x1F) / 2.0;
- setNumericValue("remaining_battery_life", Unit::Year, remaining_battery_life);
-
- int transmit_period_s = 1 << ((frame[11] & 0x0F) + 2);
- setNumericValue("transmit_period", Unit::Second, transmit_period_s);
-
- double total_water_consumption_l_ = uint32FromBytes(decoded_content, 1, true);
- setNumericValue("total", Unit::L, total_water_consumption_l_);
-
- if (decoded_content.size() > 8) {
- double last_month_total_water_consumption_l_ = uint32FromBytes(decoded_content, 5, true);
- setNumericValue("last_month_total", Unit::L, last_month_total_water_consumption_l_);
- }
-
- // get the date when the second measurement was taken
- if (decoded_content.size() > 10) {
- uint16_t h0_year = ((decoded_content[10] & 0xF0) >> 1) + ((decoded_content[9] & 0xE0) >> 5);
- if (h0_year > 80) {
- h0_year += 1900;
- } else {
- h0_year += 2000;
- }
- uint8_t h0_month = decoded_content[10] & 0xF;
- uint8_t h0_day = decoded_content[9] & 0x1F;
-
- setStringValue("last_month_measure_date", tostrprintf("%d-%02d-%02d", h0_year, h0_month%99, h0_day%99), NULL);
- }
-
- // read the alarms:
- IzarAlarms alarms {};
-
- alarms.general_alarm = frame[11] >> 7;
- alarms.leakage_currently = frame[12] >> 7;
- alarms.leakage_previously = frame[12] >> 6 & 0x1;
- alarms.meter_blocked = frame[12] >> 5 & 0x1;
- alarms.back_flow = frame[13] >> 7;
- alarms.underflow = frame[13] >> 6 & 0x1;
- alarms.overflow = frame[13] >> 5 & 0x1;
- alarms.submarine = frame[13] >> 4 & 0x1;
- alarms.sensor_fraud_currently = frame[13] >> 3 & 0x1;
- alarms.sensor_fraud_previously = frame[13] >> 2 & 0x1;
- alarms.mechanical_fraud_currently = frame[13] >> 1 & 0x1;
- alarms.mechanical_fraud_previously = frame[13] & 0x1;
-
- setStringValue("current_alarms", currentAlarmsText(alarms));
- setStringValue("previous_alarms", previousAlarmsText(alarms));
- }
-
- std::vector Driver::decodePrios(const std::vector &origin, const std::vector &frame, uint32_t key)
- {
- return decodeDiehlLfsr(origin, frame, key, DiehlLfsrCheckMethod::HEADER_1_BYTE, 0x4B);
- }
-}
-
-// Test: IzarWater izar 21242472 NOKEY
-// telegram=|1944304C72242421D401A2_013D4013DD8B46A4999C1293E582CC|
-// {"_":"telegram","media":"water","meter":"izar","name":"IzarWater","id":"21242472","prefix":"C19UA","serial_number":"145842","total_m3":3.488,"last_month_total_m3":3.486,"last_month_measure_date":"2019-09-30","remaining_battery_life_y":14.5,"current_alarms":"meter_blocked,underflow","previous_alarms":"no_alarm","transmit_period_s":8,"manufacture_year":"2019","timestamp":"1111-11-11T11:11:11Z"}
-// |IzarWater;21242472;C19UA;145842;3.488;3.486;2019-09-30;14.5;meter_blocked,underflow;no_alarm;8;2019;1111-11-11 11:11.11
-
-// Test: IzarWater2 izar 66236629 NOKEY
-// telegram=|2944A511780729662366A20118001378D3B3DB8CEDD77731F25832AAF3DA8CADF9774EA673172E8C61F2|
-// {"_":"telegram","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"}
-// |IzarWater2;66236629;null;null;16.76;11.84;2019-11-30;12;no_alarm;no_alarm;8;null;1111-11-11 11:11.11
-
-// Test: IzarWater3 izar 20481979 NOKEY
-// telegram=|1944A511780779194820A1_21170013355F8EDB2D03C6912B1E37
-// {"_":"telegram","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"}
-// |IzarWater3;20481979;null;null;4.366;0;2020-12-31;11.5;no_alarm;no_alarm;8;null;1111-11-11 11:11.11
-
-// Test: IzarWater4 izar 2124589c NOKEY
-// Comment: With mfct specific tpl ci field a3.
-// telegram=|1944304c9c5824210c04a363140013716577ec59e8663ab0d31c|
-// {"_":"telegram","media":"water","meter":"izar","name":"IzarWater4","id":"2124589c","prefix":"H19CA","serial_number":"159196","total_m3":38.944,"last_month_total_m3":38.691,"last_month_measure_date":"2021-02-01","remaining_battery_life_y":10,"current_alarms":"no_alarm","previous_alarms":"no_alarm","transmit_period_s":32,"manufacture_year":"2019","timestamp":"1111-11-11T11:11:11Z"}
-// |IzarWater4;2124589c;H19CA;159196;38.944;38.691;2021-02-01;10;no_alarm;no_alarm;32;2019;1111-11-11 11:11.11
-
-// Test: IzarWater5 izar 20e4ffde NOKEY
-// Comment: Ensure non-regression on manufacture year parsing
-// telegram=|1944304CDEFFE420CC01A2_63120013258F907B0AFF12529AC33B|
-// {"_":"telegram","media":"water","meter":"izar","name":"IzarWater5","id":"20e4ffde","prefix":"C15SA","serial_number":"007710","total_m3":159.832,"last_month_total_m3":157.76,"last_month_measure_date":"2021-02-01","remaining_battery_life_y":9,"current_alarms":"no_alarm","previous_alarms":"no_alarm","transmit_period_s":32,"manufacture_year":"2015","timestamp":"1111-11-11T11:11:11Z"}
-// |IzarWater5;20e4ffde;C15SA;007710;159.832;157.76;2021-02-01;9;no_alarm;no_alarm;32;2015;1111-11-11 11:11.11
-
-// Test: IzarWater6 izar 48500375 NOKEY
-// telegram=|19442423860775035048A251520015BEB6B2E1ED623A18FC74A5|
-// {"_":"telegram","media":"water","meter":"izar","name":"IzarWater6","id":"48500375","total_m3":521.602,"last_month_total_m3":519.147,"last_month_measure_date":"2021-11-15","remaining_battery_life_y":9,"current_alarms":"no_alarm","previous_alarms":"leakage","transmit_period_s":8,"timestamp":"1111-11-11T11:11:11Z"}
-// |IzarWater6;48500375;null;null;521.602;519.147;2021-11-15;9;no_alarm;leakage;8;null;1111-11-11 11:11.11
diff --git a/components/wmbus_common/driver_lansendw.cpp b/components/wmbus_common/driver_lansendw.cpp
deleted file mode 100644
index a0780868..00000000
--- a/components/wmbus_common/driver_lansendw.cpp
+++ /dev/null
@@ -1,99 +0,0 @@
-/*
- Copyright (C) 2020-2022 Fredrik Öhrström (gpl-3.0-or-later)
-
- 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 .
-*/
-
-#include"meters_common_implementation.h"
-
-namespace
-{
- struct Driver : public virtual MeterCommonImplementation
- {
- Driver(MeterInfo &mi, DriverInfo &di);
- };
-
- static bool ok = staticRegisterDriver([](DriverInfo&di)
- {
- di.setName("lansendw");
- di.setDefaultFields("name,id,status,timestamp");
- di.setMeterType(MeterType::DoorWindowDetector);
- di.addLinkMode(LinkMode::T1);
- di.setConstructor([](MeterInfo& mi, DriverInfo& di){ return shared_ptr(new Driver(mi, di)); });
- di.addMVT(MANUFACTURER_LAS, 0x1d, 0x07);
- });
-
- Driver::Driver(MeterInfo &mi, DriverInfo &di) : MeterCommonImplementation(mi, di)
- {
- addStringFieldWithExtractorAndLookup(
- "status",
- "The state (OPEN/CLOSED) for the door/window.",
- DEFAULT_PRINT_PROPERTIES,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::DigitalInput),
- Translate::Lookup()
- .add(Translate::Rule("INPUT_BITS", Translate::MapType::IndexToString)
- .set(MaskBits(0xffff))
- .add(Translate::Map(0x11 ,"CLOSED", TestBit::Set))
- .add(Translate::Map(0x55 ,"OPEN", TestBit::Set))
- ));
-
- addStringFieldWithExtractorAndLookup(
- "error_flags",
- "Error flags.",
- PrintProperty::STATUS | PrintProperty::INCLUDE_TPL_STATUS,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::ErrorFlags),
- Translate::Lookup()
- .add(Translate::Rule("ERROR_FLAGS", Translate::MapType::BitToString)
- .set(MaskBits(0xffff))
- .set(DefaultMessage("OK"))
- ));
-
- addNumericFieldWithExtractor(
- "a",
- "How many times the door/window has been opened or closed.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Dimensionless,
- VifScaling::None, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::Dimensionless)
- );
-
- addNumericFieldWithExtractor(
- "b",
- "The current number of counted pulses from counter b.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Dimensionless,
- VifScaling::None, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::Dimensionless)
- .set(SubUnitNr(1))
- );
-
- }
-}
-
-// Test: Dooro lansendw 00010205 NOKEY
-// telegram=|2e44333005020100071d7ab54800002f2f02fd1b110002fd971d01000efd3a2200000000008e40fd3a000000000000|
-// {"_":"telegram","media":"reserved","meter":"lansendw","name":"Dooro","id":"00010205","status":"CLOSED","a_counter":22,"b_counter":0,"error_flags":"ERROR_FLAGS_1 PERMANENT_ERROR UNKNOWN_40","timestamp":"1111-11-11T11:11:11Z"}
-// |Dooro;00010205;CLOSED;1111-11-11 11:11.11
-
-// telegram=|2e44333005020100071d7ab66800002f2f02fd1b550002fd971d01000efd3a2300000000008e40fd3a000000000000|
-// {"_":"telegram","media":"reserved","meter":"lansendw","name":"Dooro","id":"00010205","status":"OPEN","a_counter":23,"b_counter":0,"error_flags":"ERROR_FLAGS_1 PERMANENT_ERROR UNKNOWN_60","timestamp":"1111-11-11T11:11:11Z"}
-// |Dooro;00010205;OPEN;1111-11-11 11:11.11
diff --git a/components/wmbus_common/driver_lansenpu.cpp b/components/wmbus_common/driver_lansenpu.cpp
deleted file mode 100644
index 8b525727..00000000
--- a/components/wmbus_common/driver_lansenpu.cpp
+++ /dev/null
@@ -1,93 +0,0 @@
-/*
- Copyright (C) 2020-2022 Fredrik Öhrström (gpl-3.0-or-later)
-
- 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 .
-*/
-
-#include"meters_common_implementation.h"
-
-namespace
-{
- struct Driver : public virtual MeterCommonImplementation
- {
- Driver(MeterInfo &mi, DriverInfo &di);
- };
-
- static bool ok = staticRegisterDriver([](DriverInfo&di)
- {
- di.setName("lansenpu");
- di.setDefaultFields("name,id,status,a_counter,b_counter,timestamp");
- di.setMeterType(MeterType::PulseCounter);
- di.addMVT(MANUFACTURER_LAS, 0x00, 0x14);
- di.addMVT(MANUFACTURER_LAS, 0x00, 0x1b);
- di.addMVT(MANUFACTURER_LAS, 0x02, 0x0b);
- di.setConstructor([](MeterInfo& mi, DriverInfo& di){ return shared_ptr(new Driver(mi, di)); });
- });
-
- Driver::Driver(MeterInfo &mi, DriverInfo &di) : MeterCommonImplementation(mi, di)
- {
- setMfctTPLStatusBits(
- Translate::Lookup()
- .add(Translate::Rule("TPL_STS", Translate::MapType::BitToString)
- .set(MaskBits(0xe0))
- .set(DefaultMessage("OK"))
- .add(Translate::Map(0x40 ,"SABOTAGE_ENCLOSURE", TestBit::Set))));
-
- addStringField(
- "status",
- "Meter status from tpl status field.",
- DEFAULT_PRINT_PROPERTIES |
- PrintProperty::STATUS | PrintProperty::INCLUDE_TPL_STATUS);
-
- /* Doubles have a 52 bit significand 11 bit exp and 1 bit sign,
- so double is good for incremental pulses up to 2^52 counts
- which is approx 4.5*10^15
- The values sent by this meter are 12 digit bcd, ie at most 10^13-1 counts.
- so they fit inside a double. */
- addNumericFieldWithExtractor(
- "a",
- "The current number of counted pulses from counter a.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Dimensionless,
- VifScaling::None, DifSignedness::Signed,
- FieldMatcher::build()
- .set(DifVifKey("0EFD3A"))
- );
-
- addNumericFieldWithExtractor(
- "b",
- "The current number of counted pulses from counter b.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Dimensionless,
- VifScaling::None, DifSignedness::Signed,
- FieldMatcher::build()
- .set(DifVifKey("8E40FD3A"))
- );
- }
-}
-
-// Test: COUNTA lansenpu 00010206 NOKEY
-// telegram=|234433300602010014007a8e0400002f2f0efd3a1147000000008e40fd3a341200000000|
-// {"_":"telegram","media":"other","meter":"lansenpu","name":"COUNTA","id":"00010206","status":"POWER_LOW","a_counter":4711,"b_counter":1234,"timestamp":"1111-11-11T11:11:11Z"}
-// |COUNTA;00010206;POWER_LOW;4711;1234;1111-11-11 11:11.11
-
-
-// Test: COUNTB lansenpu 00023750 NOKEY
-// telegram=|1A443330503702000B027AD7000020|2F2F8E40FD3A700800000000|
-// {"_":"telegram","media":"electricity","meter":"lansenpu","name":"COUNTB","id":"00023750","status":"OK","b_counter":870,"timestamp":"1111-11-11T11:11:11Z"}
-// |COUNTB;00023750;OK;null;870;1111-11-11 11:11.11
-
-// telegram=|1A443330503702000B027AD74c0020|2F2F8E40FD3A700800000000|
-// {"_":"telegram","media":"electricity","meter":"lansenpu","name":"COUNTB","id":"00023750","status":"PERMANENT_ERROR POWER_LOW SABOTAGE_ENCLOSURE","b_counter":870,"timestamp":"1111-11-11T11:11:11Z"}
-// |COUNTB;00023750;PERMANENT_ERROR POWER_LOW SABOTAGE_ENCLOSURE;null;870;1111-11-11 11:11.11
diff --git a/components/wmbus_common/driver_lansenrp.cpp b/components/wmbus_common/driver_lansenrp.cpp
deleted file mode 100644
index 276a9bcb..00000000
--- a/components/wmbus_common/driver_lansenrp.cpp
+++ /dev/null
@@ -1,198 +0,0 @@
-/*
- Copyright (C) 2020-2022 Fredrik Öhrström (gpl-3.0-or-later)
-
- 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 .
-
- @see https://www.lansensystems.com/media/1282/mbus_data_format_lan-wmbus-r4_v11_rev_3.pdf
-*/
-
-#include"meters_common_implementation.h"
-
-namespace
-{
- struct Driver : public virtual MeterCommonImplementation
- {
- Driver(MeterInfo &mi, DriverInfo &di);
- };
-
- static bool ok = staticRegisterDriver([](DriverInfo&di)
- {
- di.setName("lansenrp");
- di.setDefaultFields("name,id,status,total_routed_messages_counter,used_router_slots_counter,is_repeater_listening,timestamp");
- di.setMeterType(MeterType::Repeater);
- di.addLinkMode(LinkMode::C1);
- di.addMVT(MANUFACTURER_LAS, 0x32, 0x0b);
- di.setConstructor([](MeterInfo& mi, DriverInfo& di){ return shared_ptr(new Driver(mi, di)); });
- });
-
- Driver::Driver(MeterInfo &mi, DriverInfo &di) : MeterCommonImplementation(mi, di)
- {
- setMfctTPLStatusBits(
- Translate::Lookup()
- .add(Translate::Rule("TPL_STS", Translate::MapType::BitToString)
- .set(MaskBits(0xe0))
- .set(DefaultMessage("OK"))
- .add(Translate::Map(0x04 ,"LOW_BATTERY", TestBit::Set))));
-
- addStringField(
- "status",
- "Meter status from tpl status field.",
- DEFAULT_PRINT_PROPERTIES |
- PrintProperty::STATUS | PrintProperty::INCLUDE_TPL_STATUS);
-
- addNumericFieldWithExtractor(
- "total_routed_messages",
- "Number of total routed messages since power up",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Dimensionless,
- VifScaling::None, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::Dimensionless)
- );
-
- addNumericFieldWithExtractor(
- "used_router_slots",
- "Used router slots (maximum 936)",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Dimensionless,
- VifScaling::None, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::Dimensionless)
- .set(SubUnitNr(1))
- );
-
- addStringFieldWithExtractor(
- "software_version",
- "Software version of repeater",
- DEFAULT_PRINT_PROPERTIES,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::SoftwareVersion)
- );
-
- addStringFieldWithExtractorAndLookup(
- "is_repeater_listening",
- "Is the repeater listening (YES/NO)",
- DEFAULT_PRINT_PROPERTIES,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::Dimensionless)
- .set(SubUnitNr(2)),
- Translate::Lookup()
- .add(Translate::Rule("INPUT_BITS", Translate::MapType::IndexToString)
- .set(MaskBits(0x01))
- .add(Translate::Map(0x00, "NO", TestBit::Set))
- .add(Translate::Map(0x01, "YES", TestBit::Set))
- ));
-
- addNumericFieldWithExtractor(
- "seconds_to_mode_change",
- "Seconds to mode change (Listen -> Sleep or Sleep -> Listen). Maximum 32767 seconds",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Dimensionless,
- VifScaling::None, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::Dimensionless)
- .set(SubUnitNr(3))
- );
-
- addNumericFieldWithExtractor(
- "listen_timer_value",
- "Value on parameter 'Listen timer'",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Dimensionless,
- VifScaling::None, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::Dimensionless)
- .set(StorageNr(1))
- );
-
- addNumericFieldWithExtractor(
- "pause_timer_value",
- "Value on parameter 'Pause timer'",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Dimensionless,
- VifScaling::None, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::Dimensionless)
- .set(StorageNr(2))
- );
-
- addStringFieldWithExtractorAndLookup(
- "repeater_listening_on_weekdays",
- "Shows which weekday(s) repeater is listening (MO/TU/WE/TH/FR/SA/SU)",
- DEFAULT_PRINT_PROPERTIES,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::Dimensionless)
- .set(StorageNr(3)),
- Translate::Lookup()
- .add(Translate::Rule("INPUT_BITS", Translate::MapType::BitToString)
- .set(MaskBits(0xffff))
- .add(Translate::Map(0x01 ,"SU", TestBit::Set))
- .add(Translate::Map(0x02 ,"MO", TestBit::Set))
- .add(Translate::Map(0x04 ,"TU", TestBit::Set))
- .add(Translate::Map(0x08 ,"WE", TestBit::Set))
- .add(Translate::Map(0x10 ,"TH", TestBit::Set))
- .add(Translate::Map(0x20 ,"FR", TestBit::Set))
- .add(Translate::Map(0x40 ,"SA", TestBit::Set))
- ));
-
- addNumericFieldWithExtractor(
- "start_time_value",
- "Value on parameter 'Start time', shown as minusted after midnight (-1=Not used)",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Dimensionless,
- VifScaling::None, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::Dimensionless)
- .set(StorageNr(4))
- );
-
- addStringFieldWithExtractor(
- "meter_datetime",
- "Date and time when the meter sent the telegram.",
- DEFAULT_PRINT_PROPERTIES,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::DateTime)
- );
-
- addNumericFieldWithExtractor(
- "battery",
- "Battery voltage.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Voltage,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::Voltage)
- );
- }
-}
-
-// Test: REPEAT lansenrp 00035946 NOKEY
-// telegram=|54443330465903000B327A2B0000402F2F04FD3A946709008240FD3A600002FD0F9500818040FD3A0084C040FD3A8838000042FD3A28008201FD3A8C05C101FD3A7F8202FD3A3804066D35122EFB2B0002FD46D00C|
-// {"_":"telegram","media":"reserved","meter":"lansenrp","name":"REPEAT","id":"00035946","battery_v":3.28,"listen_timer_value_counter":40,"pause_timer_value_counter":1420,"seconds_to_mode_change_counter":14472,"start_time_value_counter":1080,"total_routed_messages_counter":616340,"used_router_slots_counter":96,"is_repeater_listening":"NO","meter_datetime":"2023-11-27 14:18:53","repeater_listening_on_weekdays":"FR MO SA SU TH TU WE","software_version":"0095","status":"OK","timestamp":"1111-11-11T11:11:11Z"}
-// |REPEAT;00035946;OK;616340;96;NO;1111-11-11 11:11.11
-
-// telegram=|54443330465903000B327A2B0400402F2F04FD3A946709008240FD3A600002FD0F9500818040FD3A0184C040FD3A8838000042FD3A28008201FD3A8C05C101FD3A088202FD3A3804066D35122EFB2B0002FD46D00C|
-// {"_":"telegram","media":"reserved","meter":"lansenrp","name":"REPEAT","id":"00035946","battery_v":3.28,"listen_timer_value_counter":40,"pause_timer_value_counter":1420,"seconds_to_mode_change_counter":14472,"start_time_value_counter":1080,"total_routed_messages_counter":616340,"used_router_slots_counter":96,"is_repeater_listening":"YES","meter_datetime":"2023-11-27 14:18:53","repeater_listening_on_weekdays":"WE","software_version":"0095","status":"POWER_LOW","timestamp":"1111-11-11T11:11:11Z"}
-// |REPEAT;00035946;POWER_LOW;616340;96;YES;1111-11-11 11:11.11
diff --git a/components/wmbus_common/driver_lansensm.cpp b/components/wmbus_common/driver_lansensm.cpp
deleted file mode 100644
index 11883c2c..00000000
--- a/components/wmbus_common/driver_lansensm.cpp
+++ /dev/null
@@ -1,99 +0,0 @@
-/*
- Copyright (C) 2020-2022 Fredrik Öhrström (gpl-3.0-or-later)
-
- 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 .
-*/
-
-#include"meters_common_implementation.h"
-
-namespace
-{
- struct Driver : public virtual MeterCommonImplementation
- {
- Driver(MeterInfo &mi, DriverInfo &di);
- };
-
- static bool ok = staticRegisterDriver([](DriverInfo&di)
- {
- di.setName("lansensm");
- di.setDefaultFields("name,id,status,minutes_since_last_manual_test_counter,timestamp");
- di.setMeterType(MeterType::SmokeDetector);
- di.addLinkMode(LinkMode::T1);
- di.addMVT(MANUFACTURER_LAS, 0x1a, 0x03);
- di.setConstructor([](MeterInfo& mi, DriverInfo& di){ return shared_ptr(new Driver(mi, di)); });
- });
-
- Driver::Driver(MeterInfo &mi, DriverInfo &di) : MeterCommonImplementation(mi, di)
- {
- addStringFieldWithExtractorAndLookup(
- "status",
- "Meter status.",
- DEFAULT_PRINT_PROPERTIES |
- PrintProperty::STATUS | PrintProperty::INCLUDE_TPL_STATUS,
- FieldMatcher::build()
- .set(VIFRange::ErrorFlags)
- .add(VIFCombinable::StandardConformantDataContent),
- Translate::Lookup(
- {
- {
- {
- "ERROR_FLAGS",
- Translate::MapType::BitToString,
- AlwaysTrigger, MaskBits(0xffff),
- "OK",
- {
- { 0x0002, "LOW_BATTERY" },
- { 0x0004, "SMOKE" },
- { 0x0008, "MANUAL_TEST" },
- { 0x0010, "MALFUNCTION" },
- { 0x0020, "NO_CONNECTION_TO_SMOKE_DETECTOR" },
- { 0x0100, "SMOKE_SENSOR_END_OF_LIFE" }
- }
- },
- },
- }));
-
- addNumericFieldWithExtractor(
- "async_msg_id",
- "Unique asynchronous message number.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Dimensionless,
- VifScaling::None, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AccessNumber)
- );
-
- addNumericFieldWithExtractor(
- "minutes_since_last_manual_test",
- "Minutes since last manual test.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Dimensionless,
- VifScaling::None, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::Dimensionless)
- );
-
- }
-}
-
-// Test: SMOKEA lansensm 00010204 NOKEY
-// telegram=|2E44333004020100031A7AC40020052F2F_02FD971D000004FD084C02000004FD3A467500002F2F2F2F2F2F2F2F2F2F|
-// {"_":"telegram","media":"smoke detector","meter":"lansensm","name":"SMOKEA","id":"00010204","status":"OK","async_msg_id_counter":588,"minutes_since_last_manual_test_counter":30022,"timestamp":"1111-11-11T11:11:11Z"}
-// |SMOKEA;00010204;OK;30022;1111-11-11 11:11.11
-
-// telegram=|2E44333004020100031A7ADE0020052F2F_02FD971D040004FD086502000004FD3A010000002F2F2F2F2F2F2F2F2F2F|
-// {"_":"telegram","media":"smoke detector","meter":"lansensm","name":"SMOKEA","id":"00010204","status":"SMOKE","async_msg_id_counter":613,"minutes_since_last_manual_test_counter":1,"timestamp":"1111-11-11T11:11:11Z"}
-// |SMOKEA;00010204;SMOKE;1;1111-11-11 11:11.11
diff --git a/components/wmbus_common/driver_lansenth.cpp b/components/wmbus_common/driver_lansenth.cpp
deleted file mode 100644
index 31f82b9d..00000000
--- a/components/wmbus_common/driver_lansenth.cpp
+++ /dev/null
@@ -1,135 +0,0 @@
-/*
- Copyright (C) 2019-2023 Fredrik Öhrström (gpl-3.0-or-later)
-
- 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 .
-*/
-
-#include"meters_common_implementation.h"
-
-namespace
-{
- struct Driver : public virtual MeterCommonImplementation
- {
- Driver(MeterInfo &mi, DriverInfo &di);
- };
-
- static bool ok = staticRegisterDriver([](DriverInfo&di)
- {
- di.setName("lansenth");
- di.setDefaultFields("name,id,current_temperature_c,current_relative_humidity_rh,timestamp");
- di.setMeterType(MeterType::TempHygroMeter);
- di.addMVT(MANUFACTURER_LAS, 0x1b, 0x07);
- di.addMVT(MANUFACTURER_LAS, 0x1b, 0x09);
- di.setConstructor([](MeterInfo& mi, DriverInfo& di){ return shared_ptr(new Driver(mi, di)); });
- });
-
- Driver::Driver(MeterInfo &mi, DriverInfo &di) : MeterCommonImplementation(mi, di)
- {
- addOptionalLibraryFields("on_time_h");
- setMfctTPLStatusBits(
- Translate::Lookup()
- .add(Translate::Rule("TPL_STS", Translate::MapType::BitToString)
- .set(MaskBits(0xe0))
- .set(DefaultMessage("OK"))
- .add(Translate::Map(0x40 ,"SABOTAGE_ENCLOSURE", TestBit::Set))));
-
- addStringField(
- "status",
- "Meter status from tpl status field.",
- DEFAULT_PRINT_PROPERTIES |
- PrintProperty::STATUS | PrintProperty::INCLUDE_TPL_STATUS);
-
- addNumericFieldWithExtractor(
- "current_temperature",
- "The current temperature.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Temperature,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::ExternalTemperature)
- );
-
- addNumericFieldWithExtractor(
- "current_relative_humidity",
- "The current humidity.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::RelativeHumidity,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::RelativeHumidity)
- );
-
- addNumericFieldWithExtractor(
- "average_temperature_1h",
- "The average temperature over the last hour.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Temperature,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::ExternalTemperature)
- .set(StorageNr(1))
- );
-
- addNumericFieldWithExtractor(
- "average_relative_humidity_1h",
- "The average humidity over the last hour.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::RelativeHumidity,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::RelativeHumidity)
- .set(StorageNr(1))
- );
-
- addNumericFieldWithExtractor(
- "average_temperature_24h",
- "The average temperature over the last 24 hours.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Temperature,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::ExternalTemperature)
- .set(StorageNr(2))
- );
-
- addNumericFieldWithExtractor(
- "average_relative_humidity_24h",
- "The average humidity over the last 24 hours.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::RelativeHumidity,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::RelativeHumidity)
- .set(StorageNr(2))
- );
-
- }
-}
-
-
-// Test: Tempoo lansenth 00010203 NOKEY
-// telegram=|2e44333003020100071b7a634820252f2f0265840842658308820165950802fb1aae0142fb1aae018201fb1aa9012f|
-// {"_":"telegram","media":"room sensor","meter":"lansenth","name":"Tempoo","id":"00010203","status":"PERMANENT_ERROR SABOTAGE_ENCLOSURE","current_temperature_c":21.8,"current_relative_humidity_rh":43,"average_temperature_1h_c":21.79,"average_relative_humidity_1h_rh":43,"average_temperature_24h_c":21.97,"average_relative_humidity_24h_rh":42.5,"timestamp":"1111-11-11T11:11:11Z"}
-// |Tempoo;00010203;21.8;43;1111-11-11 11:11.11
-
-// Test: T2 lansenth 00060041 NOKEY
-// telegram=|2E44333041000600091B7AA70020252F2F_0265DBF94265FC04820165610901FB1B2C41FB1B238101FB1B290223BB00|+0
-// {"_":"telegram","media":"room sensor","meter":"lansenth","name":"T2","id":"00060041","status":"OK","current_temperature_c":-15.73,"current_relative_humidity_rh":44,"average_temperature_1h_c":12.76,"average_relative_humidity_1h_rh":35,"average_temperature_24h_c":24.01,"average_relative_humidity_24h_rh":41,"on_time_h":4488,"timestamp":"1111-11-11T11:11:11Z"}
-// |T2;00060041;-15.73;44;1111-11-11 11:11.11
diff --git a/components/wmbus_common/driver_loader/__init__.py b/components/wmbus_common/driver_loader/__init__.py
deleted file mode 100644
index 5ec5ad55..00000000
--- a/components/wmbus_common/driver_loader/__init__.py
+++ /dev/null
@@ -1,16 +0,0 @@
-from .driver import CppDriver, Driver
-from .driver_manager import DriverManager as _DriverManagerClass
-from .field import FieldType
-from .units import get_human_readable_unit, split_name_unit
-
-DriverManager = _DriverManagerClass()
-DriverManager.load_drivers()
-
-__all__ = [
- "DriverManager",
- "CppDriver",
- "Driver",
- "FieldType",
- "get_human_readable_unit",
- "split_name_unit",
-]
diff --git a/components/wmbus_common/driver_loader/driver.py b/components/wmbus_common/driver_loader/driver.py
deleted file mode 100644
index 7e80559d..00000000
--- a/components/wmbus_common/driver_loader/driver.py
+++ /dev/null
@@ -1,190 +0,0 @@
-import re
-from dataclasses import dataclass, field
-from difflib import get_close_matches
-from functools import cached_property
-from logging import getLogger
-from pathlib import Path
-
-from .field import FieldDefinition, FieldType, MatchResult
-from .xmq_loader import generate as load_xmq
-
-LOGGER = getLogger(__name__)
-
-ADD_FIELD_METHOD_RE = re.compile(
- r'(?P/[*/]\s*)?add(?PNumeric|String)Field\w*?\([^"]*"(?P[^"]+)"[^"]*"(?P[^"]+)".*?\);',
- flags=re.MULTILINE | re.DOTALL,
-)
-
-ADD_LIBRARY_FIELD_METHOD_RE = re.compile(
- r'(?P/[*/]\s*)?addOptionalLibraryFields\("(?P[^"]+)"\);',
- flags=re.MULTILINE | re.DOTALL,
-)
-
-WELL_KNOWN_FIELDS = {
- "rssi_dbm",
- "timestamp",
-}
-
-RELOCATED_CPP_HEADERS = {
- "manufacturer_specificities",
- "meters_common_implementation",
- "wmbus_utils",
-}
-
-
-@dataclass(frozen=True)
-class Driver:
- name: str
- source_path: Path = field(repr=False)
- cpp_source: str = field(repr=False)
-
- def __lt__(self, other):
- if isinstance(other, Driver):
- return self.name < other.name
-
- return NotImplemented
-
- @cached_property
- def fields(self) -> set[FieldDefinition]:
- return {
- FieldDefinition(
- field_type=FieldType(match["field_type"]),
- name=match["name"],
- )
- for match in ADD_FIELD_METHOD_RE.finditer(self.cpp_source)
- if match["comment_mark"] is None
- } | {
- FieldDefinition(
- field_type=FieldType.LIBRARY,
- name=name,
- )
- for match in ADD_LIBRARY_FIELD_METHOD_RE.finditer(self.cpp_source)
- if match["comment_mark"] is None
- for name in match["names"].split(",")
- }
-
- @property
- def available_fields(self):
- return sorted({f.name for f in self.fields} | WELL_KNOWN_FIELDS)
-
- def request_field(self, field_name: str, field_type: FieldType | None = None):
- if field_name in WELL_KNOWN_FIELDS:
- return field_name
-
- matches = [(f, f.match(field_name, field_type)) for f in self.fields]
-
- exact = [f for f, r in matches if r is MatchResult.MATCH]
- if exact:
- for f in exact:
- f.serialize = True
- return field_name
-
- partial = [f for f, r in matches if r is MatchResult.PARTIAL_MATCH]
- if partial:
- for f in partial:
- f.serialize = True
- LOGGER.warning(
- "Requested %s field '%s' from driver '%s' has no exact match, "
- "using %d partial match(es): %s",
- field_type.lower(),
- field_name,
- self.name,
- len(partial),
- ", ".join(f"{f.name} ({f.field_type.value})" for f in partial),
- )
- return field_name
-
- field_names = sorted({f.name for f in self.fields} | WELL_KNOWN_FIELDS)
- suggestions = get_close_matches(field_name, field_names)
-
- LOGGER.warning(
- "Unknown field '%s' requested from driver '%s', %s options are: %s",
- field_name,
- self.name,
- "similar" if suggestions else "valid",
- ", ".join(suggestions or field_names),
- )
- return f"{field_name} [unknown]"
-
- def serialize(self) -> str:
- requested_fields = {f for f in self.fields if f.serialize} or self.fields
- regular_fields_to_remove = {
- f.name
- for f in (self.fields - requested_fields)
- if f.field_type != FieldType.LIBRARY
- }
- library_fields_to_remove = {
- f.name
- for f in (self.fields - requested_fields)
- if f.field_type == FieldType.LIBRARY
- }
-
- source = self.cpp_source
-
- def regular_field_replacer(match: re.Match) -> str:
- if match["comment_mark"] is not None:
- return match[0]
-
- full_call = match[0]
- field_name = match["name"]
-
- if field_name in regular_fields_to_remove:
- return f"/* {full_call} */"
- else:
- help_start, help_end = match.span("info")
-
- call_start = match.start(0)
- rel_start = help_start - call_start - 1
- rel_end = help_end - call_start + 1
-
- return f'{full_call[:rel_start]}"" /* {full_call[rel_start:rel_end]} */{full_call[rel_end:]}'
-
- source = ADD_FIELD_METHOD_RE.sub(regular_field_replacer, self.cpp_source)
-
- def library_field_replacer(match: re.Match) -> str:
- if match["comment_mark"] is not None:
- return match[0]
-
- full_call = match[0]
- old_content = match["names"]
- new_content = ",".join(
- set(old_content.split(",")) - library_fields_to_remove
- )
-
- arg_start, arg_end = match.span("names")
- call_start = match.start(0)
- rel_start = arg_start - call_start - 1
- rel_end = arg_end - call_start + 1
-
- return f'{full_call[:rel_start]}"{new_content}" /* "{old_content}" */{full_call[rel_end:]}'
-
- source = ADD_LIBRARY_FIELD_METHOD_RE.sub(library_field_replacer, source)
-
- source += f'\nextern "C" void register_wmbus_driver_{self.name}() {{}}'
-
- return source
-
-
-@dataclass(frozen=True, order=True)
-class CppDriver(Driver):
- @classmethod
- def from_source(cls, path: Path) -> "CppDriver":
- name = path.stem.removeprefix("driver_")
- cpp_source = path.read_text()
-
- for hdr in RELOCATED_CPP_HEADERS:
- cpp_source = cpp_source.replace(
- f"{hdr}.h",
- f"../esphome/components/wmbus_common/{hdr}.h",
- )
- return cls(name=name, source_path=path, cpp_source=cpp_source)
-
-
-@dataclass(frozen=True, order=True)
-class XmqDriver(Driver):
- @classmethod
- def from_source(cls, path: Path) -> "XmqDriver":
- name = path.stem
- xmq_source = path.read_text()
- cpp_source = load_xmq(xmq_source)
- return cls(name=name, source_path=path, cpp_source=cpp_source)
diff --git a/components/wmbus_common/driver_loader/driver_loader.c.j2 b/components/wmbus_common/driver_loader/driver_loader.c.j2
deleted file mode 100644
index 93e6735c..00000000
--- a/components/wmbus_common/driver_loader/driver_loader.c.j2
+++ /dev/null
@@ -1,12 +0,0 @@
-// Auto-generated from loaded drivers list
-
-{% for n in names %}
-void register_wmbus_driver_{{ n }}();
-{% endfor %}
-
-void register_wmbus_drivers()
-{
-{% for n in names %}
- register_wmbus_driver_{{ n }}();
-{% endfor %}
-}
\ No newline at end of file
diff --git a/components/wmbus_common/driver_loader/driver_manager.py b/components/wmbus_common/driver_loader/driver_manager.py
deleted file mode 100644
index 20f383ab..00000000
--- a/components/wmbus_common/driver_loader/driver_manager.py
+++ /dev/null
@@ -1,74 +0,0 @@
-from pathlib import Path
-
-from jinja2 import Template
-
-from .driver import CppDriver, Driver, XmqDriver
-
-WMBUS_COMMON_PATH = Path(__file__).parents[1]
-LEGACY_DRIVERS_PATH = WMBUS_COMMON_PATH
-XMQ_DRIVERS_PATH = WMBUS_COMMON_PATH / "drivers" / "src"
-
-
-class DriverManager:
- def __init__(self):
- self._all_drivers: dict[str, Driver] = {}
- self._requested_drivers: set[Driver] = set()
-
- def __add_driver(self, driver: Driver) -> None:
- if driver.name in self._all_drivers:
- raise ValueError(f"Driver with name {driver.name} already exists")
- self._all_drivers[driver.name] = driver
-
- def load_drivers(self) -> None:
- for p in LEGACY_DRIVERS_PATH.glob("driver_*.cpp"):
- self.__add_driver(CppDriver.from_source(p))
-
- for p in XMQ_DRIVERS_PATH.glob("*.xmq"):
- self.__add_driver(XmqDriver.from_source(p))
-
- @property
- def available_drivers(self):
- return sorted(self._all_drivers)
-
- def request_driver(self, driver_name: str):
- driver = self._all_drivers[driver_name]
- self._requested_drivers.add(driver)
- return driver
-
- @property
- def loader_content(self) -> str:
- template_path = Path(__file__).parent / "driver_loader.c.j2"
- template: Template = Template(
- template_path.read_text(),
- trim_blocks=True,
- lstrip_blocks=True,
- )
-
- return template.render(names=sorted(d.name for d in self._requested_drivers))
-
- def sync_to_directory(self, target_dir: str | Path) -> None:
- target_dir = Path(target_dir)
- target_dir.mkdir(exist_ok=True, parents=True)
-
- if not self._requested_drivers:
- self.request_driver("unknown")
-
- files_to_write = {target_dir / "driver_loader.c": self.loader_content}
-
- for driver in self._requested_drivers:
- files_to_write[target_dir / f"{driver.name}.cpp"] = driver.serialize()
-
- for path, content in files_to_write.items():
- old_content = path.read_text() if path.exists() else ""
- if old_content != content:
- path.write_text(content)
-
- for root, dirs, files in target_dir.walk(top_down=False):
- for name in files:
- path = Path(root) / name
- if path not in files_to_write:
- path.unlink()
- for name in dirs:
- path = Path(root) / name
- if not any(path.iterdir()):
- path.rmdir()
diff --git a/components/wmbus_common/driver_loader/field.py b/components/wmbus_common/driver_loader/field.py
deleted file mode 100644
index f0aab68c..00000000
--- a/components/wmbus_common/driver_loader/field.py
+++ /dev/null
@@ -1,80 +0,0 @@
-import re
-from dataclasses import dataclass
-from enum import StrEnum, Enum, auto
-from functools import cached_property
-
-from .units import split_name_unit
-
-
-class FieldType(StrEnum):
- NUMERIC = "Numeric"
- STRING = "String"
- LIBRARY = "Library"
-
-
-class MatchResult(Enum):
- MATCH = auto()
- NO_MATCH = auto()
- PARTIAL_MATCH = auto()
-
-
-FORMULA_RE = re.compile(r"\{[^}]+?}")
-
-
-@dataclass
-class FieldDefinition:
- field_type: FieldType
- name: str
- serialize: bool = False
-
- @cached_property
- def _pattern(self) -> re.Pattern:
- # The field name can contain a {number} placeholder, which matches any digits.
- # This allows to match fields like "current_power_consumption_1_kw",
- # "current_power_consumption_2_kw", etc. with a single definition
- # "current_power_consumption_{number}_kw".
- return re.compile(FORMULA_RE.sub(r"\\d+", self.name))
-
- def match(self, field_name: str, type_hint: FieldType | None = None) -> MatchResult:
- # alter formula expressions to number
- field_name = FORMULA_RE.sub('0', field_name)
-
- # and try to match with or without unit suffix
- variants = [field_name]
- if self.field_type == FieldType.NUMERIC:
- name_without_unit, unit = split_name_unit(field_name)
- if unit:
- variants.append(name_without_unit)
-
- matched = any(
- self._pattern.fullmatch(variant) is not None for variant in variants
- )
-
- if (
- type_hint
- and matched
- and self.field_type != FieldType.LIBRARY
- and self.field_type != type_hint
- ):
- return MatchResult.PARTIAL_MATCH
-
- if matched:
- return MatchResult.MATCH
-
- return MatchResult.NO_MATCH
-
- def __hash__(self) -> int:
- return hash((self.name, self.field_type))
-
- def __lt__(self, other) -> bool:
- if isinstance(other, FieldDefinition):
- return self.name < other.name
- if isinstance(other, str):
- return self.name < other
- return NotImplemented
-
- def __gt__(self, other) -> bool:
- return not (self < other or self == other)
-
- def __str__(self) -> str:
- return self.name
diff --git a/components/wmbus_common/driver_loader/units.py b/components/wmbus_common/driver_loader/units.py
deleted file mode 100644
index 602fcbc0..00000000
--- a/components/wmbus_common/driver_loader/units.py
+++ /dev/null
@@ -1,45 +0,0 @@
-import string
-from functools import cache
-from pathlib import Path
-
-
-@cache
-def units_dict():
- """Builds a dictionary of units from the units.h file."""
- units = {}
- with Path(__file__).parent.with_name("units.h").open() as file:
- for line in file:
- if "LIST_OF_UNITS" in line:
- break
-
- for line in file:
- if line.strip() == "":
- break
-
- line = (
- line.strip(string.whitespace + "\\")
- .removeprefix("X(")
- .removesuffix(")")
- )
- if line:
- line = line.split(",")
- if len(line) == 5:
- cname, lcname, hrname, quantity, explanation = line
- units[lcname] = hrname.strip('"')
- else:
- break
-
- return units
-
-
-def split_name_unit(field_with_suffix):
- parts = field_with_suffix.rsplit("_", 1)
- if len(parts) == 1 or parts[-1] not in units_dict():
- return field_with_suffix, ""
- else:
- return parts
-
-
-def get_human_readable_unit(field_unit: str):
- _, unit = split_name_unit(field_unit)
- return units_dict().get(unit.lower(), unit or "?")
diff --git a/components/wmbus_common/driver_loader/xmq_driver.cpp.j2 b/components/wmbus_common/driver_loader/xmq_driver.cpp.j2
deleted file mode 100644
index ba9aeeb1..00000000
--- a/components/wmbus_common/driver_loader/xmq_driver.cpp.j2
+++ /dev/null
@@ -1,26 +0,0 @@
-// Auto-generated from XMQ driver definition
-
-#include"../esphome/components/wmbus_common/meters_common_implementation.h"
-
-namespace
-{
- struct Driver : public virtual MeterCommonImplementation
- {
- Driver(MeterInfo &mi, DriverInfo &di);
- };
-
- static bool ok = staticRegisterDriver([](DriverInfo& di)
- {
-{% for e in driver_info_expressions %}
- {{ e }}
-{% endfor %}
- di.setConstructor([](MeterInfo& mi, DriverInfo& di){ return shared_ptr(new Driver(mi, di)); });
- });
-
- Driver::Driver(MeterInfo &mi, DriverInfo &di) : MeterCommonImplementation(mi, di)
- {
-{% for e in constructor_expressions %}
- {{ e }}
-{% endfor %}
- }
-}
\ No newline at end of file
diff --git a/components/wmbus_common/driver_loader/xmq_loader.py b/components/wmbus_common/driver_loader/xmq_loader.py
deleted file mode 100644
index 4a334b18..00000000
--- a/components/wmbus_common/driver_loader/xmq_loader.py
+++ /dev/null
@@ -1,385 +0,0 @@
-from pathlib import Path
-
-from esphome import codegen as cg
-from esphome.config_validation import ensure_list, hex_int
-from esphome.cpp_generator import ExpressionStatement
-from jinja2 import Template
-from voluptuous import All, Any, Coerce, Optional, Remove, Required, Schema
-
-
-def parse_xmq(content: str):
- result = {}
- stack = [result]
- in_block_comment = False
-
- def add_to_parent(key, value):
- parent = stack[-1]
- if isinstance(parent, list):
- parent.append(value)
- elif key in parent:
- if not isinstance(parent[key], list):
- parent[key] = [parent[key]]
- parent[key].append(value)
- else:
- parent[key] = value
-
- for raw_line in content.split("\n"):
- line = raw_line.strip()
- if "//" in line:
- line = line.split("//", 1)[0].strip()
- if not line:
- continue
- if in_block_comment:
- if "*/" in line:
- in_block_comment = False
- continue
- if line.startswith("/*"):
- if "*/" not in line:
- in_block_comment = True
- continue
-
- if "=" in line:
- key, val = (p.strip().strip("'\"") for p in line.split("=", 1))
- add_to_parent(key, val)
- elif "{" in line:
- key = line.split("{")[0].strip()
- new_node = {}
- add_to_parent(key, new_node)
- stack.append(new_node)
- elif "}" in line:
- if len(stack) > 1:
- stack.pop()
-
- return result
-
-
-ns = cg.global_ns
-Translate = ns.namespace("Translate")
-Quantity = ns.namespace("Quantity")
-FieldMatcher = ns.namespace("FieldMatcher")
-Unit = ns.namespace("Unit")
-
-CSV_LIST = All(str, lambda x: [s.strip() for s in x.split(",")])
-
-
-def frac(s: str):
- num, _, denom = s.partition("/")
- return float(num) / float(denom)
-
-
-MATCHER_SCHEMA = {
- Optional("measurement_type"): str,
- Optional("difvifkey"): str,
- Optional("vif_range"): str,
- Optional("vif_scaling"): str,
- Optional("tariff_nr"): All(CSV_LIST, [Coerce(int)]),
- Optional("subunit_nr"): All(CSV_LIST, [Coerce(int)]),
- Optional("storage_nr"): All(CSV_LIST, [Coerce(int)]),
- Optional("index_nr"): Coerce(int),
- Optional("add_combinable"): str,
-}
-LOOKUP_SCHEMA = Schema(
- {
- Required("name"): str,
- Required("map_type"): str,
- Required("mask_bits"): hex_int,
- Optional("default_message", default=""): str,
- Optional("map", default=[]): ensure_list(
- {
- Required("name"): str,
- Required("value"): hex_int,
- Optional("info"): str,
- Required("test"): str,
- }
- ),
- }
-)
-
-NUMERIC_FIELD_WITH_EXTRACTOR = Schema(
- {
- Required("name"): str,
- Optional("info"): str,
- Optional("attributes", default=""): str,
- Required("quantity"): str,
- Optional("vif_scaling", default="Auto"): str,
- Optional("dif_signedness", default=""): str,
- Required("match"): MATCHER_SCHEMA,
- Optional("display_unit", default=""): str,
- Optional("force_scale", default=1.0): Any(Coerce(float), frac),
- }
-)
-
-NUMERIC_FIELD_WITH_CALCULATOR = Schema(
- {
- Required("name"): str,
- Optional("info"): str,
- Optional("attributes", default=""): str,
- Required("quantity"): str,
- Required("calculate"): str,
- Optional("display_unit", default=""): str,
- }
-)
-
-NUMERIC_FIELD_WITH_CALCULATOR_AND_MATCHER = Schema(
- {
- Required("name"): str,
- Optional("info"): str,
- Optional("attributes", default=""): str,
- Required("quantity"): str,
- Required("formula"): str,
- Required("match"): MATCHER_SCHEMA,
- Optional("display_unit", default=""): str,
- }
-)
-
-NUMERIC_FIELD = Schema(
- {
- Required("name"): str,
- Required("quantity"): str,
- Optional("attributes", default=""): str,
- Optional("info"): str,
- Optional("display_unit", default=""): str,
- }
-)
-
-STRING_FIELD_WITH_EXTRACTOR = Schema(
- {
- Required("name"): str,
- Required("quantity"): "Text",
- Optional("info"): str,
- Optional("attributes", default=""): str,
- Required("match"): MATCHER_SCHEMA,
- }
-)
-
-STRING_FIELD_WITH_EXTRACTOR_AND_LOOKUP = Schema(
- {
- Required("name"): str,
- Required("quantity"): "Text",
- Optional("info"): str,
- Optional("attributes", default=""): str,
- Required("match"): MATCHER_SCHEMA,
- Required("lookup"): LOOKUP_SCHEMA,
- }
-)
-
-STRING_FIELD = Schema(
- {
- Required("name"): str,
- Required("quantity"): "Text",
- Optional("info"): str,
- Optional("attributes", default=""): str,
- }
-)
-
-
-def parse_mvt(v: list[str]):
- m, v, t = v
- if len(m) == 3 and m.isalpha() and m.isupper():
- m = cg.RawExpression(f"MANUFACTURER_{m}")
- else:
- m = hex_int(int(m, 16))
-
- return m, hex_int(int(t, 16)), hex_int(int(v, 16))
-
-
-SCHEMA = Schema(
- {
- Required("driver"): {
- Required("name"): str,
- Required("meter_type"): str,
- Optional("info"): str,
- Remove("manufacturer"): str,
- Remove("model"): str,
- Optional("aliases"): str,
- Required("default_fields"): str,
- Required("detect"): {Required("mvt"): ensure_list(CSV_LIST, parse_mvt)},
- Optional("library"): {
- Required("use"): All(
- ensure_list(CSV_LIST),
- lambda lst: [i for sublst in lst for i in sublst],
- )
- },
- Optional("fields"): {
- Required("field"): ensure_list(
- Any(
- NUMERIC_FIELD_WITH_EXTRACTOR,
- NUMERIC_FIELD_WITH_CALCULATOR_AND_MATCHER,
- NUMERIC_FIELD_WITH_CALCULATOR,
- NUMERIC_FIELD,
- STRING_FIELD_WITH_EXTRACTOR_AND_LOOKUP,
- STRING_FIELD_WITH_EXTRACTOR,
- STRING_FIELD,
- )
- )
- },
- Remove("tests"): object,
- Remove("test"): object,
- }
- }
-)
-
-
-def driver_info_expressions(driver):
- di = ns.di
- yield di.setName(driver["name"])
- if "aliases" in driver:
- yield di.setAliases(driver["aliases"])
- yield di.setMeterType(ns.toMeterType(driver["meter_type"]))
- yield di.setDefaultFields(driver["default_fields"])
- yield from (di.addMVT(*mvt) for mvt in driver["detect"]["mvt"])
-
-
-def build_matcher(match):
- matcher = ns.namespace("FieldMatcher")
- if match is None:
- return matcher()
-
- matcher = matcher.build()
-
- if difvifkey := match.get("difvifkey"):
- conv = ns.DifVifKey(difvifkey)
- matcher = matcher.set(conv)
- return matcher
-
- if measurement_type := match.get("measurement_type"):
- conv = ns.toMeasurementType(measurement_type)
- matcher = matcher.set(conv)
-
- if vif_range := match.get("vif_range"):
- conv = ns.toVIFRange(vif_range)
- matcher = matcher.set(conv)
-
- if index_nr := match.get("index_nr"):
- conv = ns.IndexNr(index_nr)
- matcher = matcher.set(conv)
-
- if storage_nr := match.get("storage_nr"):
- conv = (ns.StorageNr(n) for n in storage_nr)
- matcher = matcher.set(*conv)
-
- if tariff_nr := match.get("tariff_nr"):
- conv = (ns.TariffNr(n) for n in tariff_nr)
- matcher = matcher.set(*conv)
-
- if subunit_nr := match.get("subunit_nr"):
- conv = (ns.SubUnitNr(n) for n in subunit_nr)
- matcher = matcher.set(*conv)
-
- if add_combinable := match.get("add_combinable"):
- conv = ns.toVIFCombinable(add_combinable)
- matcher = matcher.add(conv)
-
- return matcher
-
-
-def build_lookup(lookup):
- tns = ns.namespace("Translate")
- lkp = tns.Lookup()
- rule = tns.Rule(
- lookup["name"],
- ns.toMapType(lookup["map_type"]),
- )
-
- rule = rule.set(ns.MaskBits(lookup["mask_bits"]))
- rule = rule.set(ns.DefaultMessage(lookup["default_message"]))
-
- for m in lookup["map"]:
- rule = rule.add(tns.Map(m["value"], m["name"], ns.toTestBit(m["test"])))
-
- lkp = lkp.add(rule)
-
- return lkp
-
-
-def constructor_expressions(driver):
- if lib := driver.get("library"):
- for field in lib["use"]:
- yield ns.addOptionalLibraryFields(field.split("|", 1)[0])
-
- if fields := driver.get("fields"):
- for field in fields["field"]:
- name = field["name"]
- info = field.get("info", "")
- attributes = ns.toPrintProperties(field["attributes"])
- quantity = ns.toQuantity(field["quantity"])
- calculate = field.get("calculate")
- display_unit = ns.toUnit(field.get("display_unit"))
-
- match = field.get("match")
- matcher = build_matcher(match)
-
- if lookup := field.get("lookup"):
- lookup = build_lookup(lookup)
-
- is_numeric = field["quantity"].casefold() != "text"
-
- match (is_numeric, bool(calculate), bool(match), bool(lookup)):
- case (True, False, _, _):
- yield ns.addNumericFieldWithExtractor(
- name,
- info,
- attributes,
- quantity,
- ns.toVifScaling(field["vif_scaling"]),
- ns.toDifSignedness(field["dif_signedness"]),
- matcher,
- display_unit,
- field["force_scale"],
- )
- case (True, True, False, _):
- yield ns.addNumericFieldWithCalculator(
- name,
- info,
- attributes,
- quantity,
- calculate,
- display_unit,
- )
- case (True, True, True, _):
- yield ns.addNumericFieldWithCalculatorAndMatcher(
- name,
- info,
- attributes,
- quantity,
- calculate,
- matcher,
- display_unit,
- )
- case (False, _, _, True):
- yield ns.addStringFieldWithExtractorAndLookup(
- name,
- info,
- attributes,
- matcher,
- lookup,
- )
- case (False, _, _, False):
- yield ns.addStringFieldWithExtractor(
- name,
- info,
- attributes,
- matcher,
- )
-
-
-def generate(xmq_content: str):
- raw = parse_xmq(xmq_content)
-
- data = SCHEMA(raw)
-
- template_path = Path(__file__).parent / "xmq_driver.cpp.j2"
- template: Template = Template(
- template_path.read_text(),
- trim_blocks=True,
- lstrip_blocks=True,
- )
-
- return template.render(
- constructor_expressions=(
- str(ExpressionStatement(e)) for e in constructor_expressions(data["driver"])
- ),
- driver_info_expressions=(
- str(ExpressionStatement(e)) for e in driver_info_expressions(data["driver"])
- ),
- )
diff --git a/components/wmbus_common/driver_lse_07_17.cpp b/components/wmbus_common/driver_lse_07_17.cpp
deleted file mode 100644
index 9f72ede4..00000000
--- a/components/wmbus_common/driver_lse_07_17.cpp
+++ /dev/null
@@ -1,215 +0,0 @@
-/*
- Copyright (C) 2021-2024 Fredrik Öhrström (gpl-3.0-or-later)
-
- 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 .
-*/
-
-#include"meters_common_implementation.h"
-
-namespace
-{
- struct Driver : public virtual MeterCommonImplementation
- {
- Driver(MeterInfo &mi, DriverInfo &di);
- };
-
- static bool ok = staticRegisterDriver([](DriverInfo&di)
- {
- di.setName("lse_07_17");
- di.setDefaultFields("name,id,total_m3,due_date_m3,due_date,error_code,error_date,device_date_time,timestamp");
- di.setMeterType(MeterType::WaterMeter);
- di.addLinkMode(LinkMode::S1);
- di.addMVT(MANUFACTURER_LSE, 0x06, 0x18);
- di.addMVT(MANUFACTURER_LSE, 0x07, 0x18);
- di.addMVT(MANUFACTURER_LSE, 0x07, 0x16);
- di.addMVT(MANUFACTURER_LSE, 0x07, 0x17);
- di.addMVT(MANUFACTURER_LSE, 0x07, 0xd8);
- di.setConstructor([](MeterInfo& mi, DriverInfo& di){ return shared_ptr(new Driver(mi, di)); });
- });
-
- Driver::Driver(MeterInfo &mi, DriverInfo &di) : MeterCommonImplementation(mi, di)
- {
- addNumericFieldWithExtractor(
- "total",
- "The total water consumption recorded by this meter.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Volume,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::Volume)
- );
-
- addNumericFieldWithExtractor(
- "due_date",
- "The water consumption at the due date.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Volume,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::Volume)
- .set(StorageNr(1))
- );
-
- addStringFieldWithExtractor(
- "due_date",
- "The due date configured on the meter.",
- DEFAULT_PRINT_PROPERTIES,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::Date)
- .set(StorageNr(1))
- );
-
- addNumericFieldWithExtractor(
- "what_date",
- "The water consumption at the what date?",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Volume,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::Volume)
- .set(StorageNr(8))
- );
-
- addStringFieldWithExtractor(
- "what_date",
- "The what date?",
- DEFAULT_PRINT_PROPERTIES,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::Date)
- .set(StorageNr(8))
- );
-
- addStringFieldWithExtractorAndLookup(
- "error_code",
- "Error code of the Meter, 0 means no error.",
- DEFAULT_PRINT_PROPERTIES,
- FieldMatcher::build()
- .set(DifVifKey("02BB56")
- ),
- {
- {
- {
- "ERROR_FLAGS",
- Translate::MapType::BitToString,
- AlwaysTrigger, MaskBits(0xffff),
- "OK",
- {
- // { 0x01, "?" },
- }
- },
- },
- });
-
- addStringFieldWithExtractor(
- "error_date",
- "The date the error occurred at. If no error, reads 2127-15-31 (FFFF).",
- DEFAULT_PRINT_PROPERTIES,
- FieldMatcher::build()
- .set(MeasurementType::AtError)
- .set(VIFRange::Date)
- );
-
- addStringFieldWithExtractor(
- "device_date_time",
- "Date when measurement was recorded.",
- DEFAULT_PRINT_PROPERTIES,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::DateTime)
- );
-
- addStringFieldWithExtractor(
- "meter_version",
- "Meter model/version.",
- DEFAULT_PRINT_PROPERTIES,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::ModelVersion)
- );
- }
-}
-
- /*
- The following telegram corresponds to the Qundis QWater5.5 cold water meters I have here.
- From the device display it states that it is set to S-mode operation, sending a telegram
- every 4 h.
- Another option of this device is the C mode operation, sending telegrams every
- 7.5 s.
-
- Even though my meters are definitely Qundis QWater5.5, the meters do not identify with
- manufacturer code QDS but with LSE.
-
- (lse_07_17) 0f: 0C dif (8 digit BCD Instantaneous value)
- (lse_07_17) 10: 13 vif (Volume l)
- (lse_07_17) 11: * 04400100 total consumption (14.004000 m3)
- (lse_07_17) 15: 4C dif (8 digit BCD Instantaneous value storagenr=1)
- (lse_07_17) 16: 13 vif (Volume l)
- (lse_07_17) 17: * 40620000 due date consumption (6.240000 m3)
- (lse_07_17) 1b: 42 dif (16 Bit Integer/Binary Instantaneous value storagenr=1)
- (lse_07_17) 1c: 6C vif (Date type G)
- (lse_07_17) 1d: * 9F2C due date (2020-12-31)
- (lse_07_17) 1f: 02 dif (16 Bit Integer/Binary Instantaneous value)
- (lse_07_17) 20: BB vif (Volume flow l/h)
- (lse_07_17) 21: 56 vife (duration of limit exceed last lower is 2)
- (lse_07_17) 22: * 0000 error code (0)
- (lse_07_17) 24: 32 dif (16 Bit Integer/Binary Value during error state)
- (lse_07_17) 25: 6C vif (Date type G)
- (lse_07_17) 26: * FFFF error date (2127-15-31)
- (lse_07_17) 28: 04 dif (32 Bit Integer/Binary Instantaneous value)
- (lse_07_17) 29: 6D vif (Date and time type)
- (lse_07_17) 2a: * 180DA924 device datetime (2021-04-09 13:24)
-
- if (findKey(MeasurementType::Instantaneous, VIFRange::DateTime, 0, 0, &key, &t->dv_entries)) {
- struct tm datetime;
- extractDVdate(&t->dv_entries, key, &offset, &datetime);
- device_date_time_ = strdatetime(&datetime);
- t->addMoreExplanation(offset, " device datetime (%s)", device_date_time_.c_str());
- }
- // How do the following error codes on the display map to the code in the telegram?
- // According to the datasheet, these errors can appear on the display:
- // LEAC Leak in the system (no associated error code)
- // 0 Negative direction of flow.
- // 2 Operating hours expired.
- // 3 Hardware error.
- // 4 Permanently stored error.
- // b Communication via OPTO too often per month.
- // c Communication via M-Bus too often per month.
- // d Flow too high.
- // f Device was without voltage supply briefly. All parameter settings are lost.
- */
-
-
-// Test: Water lse_07_17 13963399 NOKEY
-
-// telegram=|244465329933961318067AE1000000_8C04130070000082046CBE2B01FD0C11046D010CA22C|
-// {"_":"telegram","media":"warm water","meter":"lse_07_17","name":"Water","id":"13963399","what_date_m3":7,"what_date":"2021-11-30","device_date_time":"2021-12-02 12:01","meter_version":"11","timestamp":"1111-11-11T11:11:11Z"}
-// |Water;13963399;null;null;null;null;null;2021-12-02 12:01;1111-11-11 11:11.11
-
-// telegram=|2A4465329933961318067AD8800000_8C04130070000082046CBE2B01FD0C11046D1800A12C02FDAC7E9B2E|
-// {"_":"telegram","media":"warm water","meter":"lse_07_17","name":"Water","id":"13963399","what_date_m3":7,"what_date":"2021-11-30","device_date_time":"2021-12-01 00:24","meter_version":"11","timestamp":"1111-11-11T11:11:11Z"}
-// |Water;13963399;null;null;null;null;null;2021-12-01 00:24;1111-11-11 11:11.11
-
-// telegram=|2D4465329933961318067ADA000000_0C13567100004C1300000000426CFFFF02BB560000326CFFFF046D2307A12C|
-// {"_":"telegram","media":"warm water","meter":"lse_07_17","name":"Water","id":"13963399","total_m3":7.156,"due_date_m3":0,"due_date":"2127-15-31","what_date_m3":7,"what_date":"2021-11-30","error_code":"OK","error_date":"2127-15-31","device_date_time":"2021-12-01 07:35","meter_version":"11","timestamp":"1111-11-11T11:11:11Z"}
-// |Water;13963399;7.156;0;2127-15-31;OK;2127-15-31;2021-12-01 07:35;1111-11-11 11:11.11
-
-// Test: Water2 lse_07_17 09993623 NOKEY
-// telegram=|2d44653223369909d8077a80000000046d130aed2B0c13233332004c1351762700426cdf2c326cffff02BB560000|
-// {"_":"telegram","device_date_time": "2023-11-13 10:19","due_date": "2022-12-31","due_date_m3": 277.651,"error_code": "OK","error_date": "2127-15-31","id": "09993623","media": "water","meter": "lse_07_17","name": "Water2","timestamp": "1111-11-11T11:11:11Z","total_m3": 323.323}
-// |Water2;09993623;323.323;277.651;2022-12-31;OK;2127-15-31;2023-11-13 10:19;1111-11-11 11:11.11
diff --git a/components/wmbus_common/driver_lse_08.cpp b/components/wmbus_common/driver_lse_08.cpp
deleted file mode 100644
index 4b001f6a..00000000
--- a/components/wmbus_common/driver_lse_08.cpp
+++ /dev/null
@@ -1,132 +0,0 @@
-/*
- Copyright (C) 2019-2022 Fredrik Öhrström (gpl-3.0-or-later)
-
- 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 .
-*/
-
-#include"meters_common_implementation.h"
-
-namespace
-{
- struct Driver : public virtual MeterCommonImplementation
- {
- Driver(MeterInfo &mi, DriverInfo &di);
- };
-
- static bool ok = staticRegisterDriver([](DriverInfo&di)
- {
- di.setName("lse_08");
- di.setDefaultFields("name,id,set_date,consumption_at_set_date_hca,timestamp");
- di.setMeterType(MeterType::HeatCostAllocationMeter);
- di.addLinkMode(LinkMode::C1);
- di.addLinkMode(LinkMode::T1);
- di.addLinkMode(LinkMode::S1);
- di.addMVT(MANUFACTURER_LSE, 0x08, 0x01);
-
- di.setConstructor([](MeterInfo& mi, DriverInfo& di){ return shared_ptr(new Driver(mi, di)); });
- });
-
- Driver::Driver(MeterInfo &mi, DriverInfo &di) : MeterCommonImplementation(mi, di)
- {
- addStringFieldWithExtractorAndLookup(
- "status",
- "Meter status from tpl status field.",
- DEFAULT_PRINT_PROPERTIES |
- PrintProperty::STATUS | PrintProperty::INCLUDE_TPL_STATUS,
- FieldMatcher::build()
- .set(DifVifKey("01FD73")),
- Translate::Lookup(
- {
- {
- {
- "ERROR_FLAGS",
- Translate::MapType::BitToString,
- AlwaysTrigger, MaskBits(0xff),
- "OK",
- {
- // Bits unknown
- }
- },
- },
- }));
-
- addNumericFieldWithExtractor(
- "current_consumption",
- "The current heat cost allocation.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::HCA,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::HeatCostAllocation)
- );
-
- addStringFieldWithExtractor(
- "set_date",
- "The most recent billing period date.",
- DEFAULT_PRINT_PROPERTIES,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::Date)
- .set(StorageNr(8))
- );
-
- addNumericFieldWithExtractor(
- "consumption_at_set_date",
- "Heat cost allocation at the most recent billing period date.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::HCA,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::HeatCostAllocation)
- .set(StorageNr(8))
- );
-
- addStringFieldWithExtractor(
- "device_date_time",
- "Date and time when the meter sent the telegram.",
- DEFAULT_PRINT_PROPERTIES,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::DateTime)
- );
-
- addNumericFieldWithExtractor(
- "duration_since_readout",
- "Duration since last measurement.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Time,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::DurationSinceReadout)
- .add(VIFCombinable::FutureValue)
- );
-
- addStringFieldWithExtractor(
- "model_version",
- "model version.",
- DEFAULT_PRINT_PROPERTIES,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::ModelVersion)
- );
- }
-}
-
-// Test: HCA2 lse_08 04998541 NOKEY
-// telegram=|294465324185990401087a0080000082046c7f018b046e210300046d1a0e6f0202fdac7e012301fd0c01|
-// {"_":"telegram","media":"heat cost allocation","meter":"lse_08","name":"HCA2","id":"04998541","status":"UNKNOWN_80","set_date":"2003-01-31","consumption_at_set_date_hca":321,"device_date_time":"2003-02-15 14:26","duration_since_readout_h":2.489167,"model_version":"01","timestamp":"1111-11-11T11:11:11Z"}
-// |HCA2;04998541;2003-01-31;321;1111-11-11 11:11.11
diff --git a/components/wmbus_common/driver_microclima.cpp b/components/wmbus_common/driver_microclima.cpp
deleted file mode 100644
index 09a9df46..00000000
--- a/components/wmbus_common/driver_microclima.cpp
+++ /dev/null
@@ -1,173 +0,0 @@
-/*
- Copyright (C) 2022 Fredrik Öhrström (gpl-3.0-or-later)
-
- 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 .
-*/
-
-#include"meters_common_implementation.h"
-
-namespace
-{
- struct Driver : public virtual MeterCommonImplementation
- {
- Driver(MeterInfo &mi, DriverInfo &di);
- };
-
- static bool ok = staticRegisterDriver([](DriverInfo&di)
- {
- di.setName("microclima");
- di.setDefaultFields("name,id,status,total_energy_consumption_kwh,total_volume_m3,timestamp");
- di.setMeterType(MeterType::HeatMeter);
- di.addLinkMode(LinkMode::T1);
- di.addMVT(MANUFACTURER_MAD, 0x04, 0x00);
- di.setConstructor([](MeterInfo& mi, DriverInfo& di){ return shared_ptr(new Driver(mi, di)); });
- });
-
- Driver::Driver(MeterInfo &mi, DriverInfo &di) : MeterCommonImplementation(mi, di)
- {
- addOptionalLibraryFields("meter_datetime,model_version,parameter_set");
- addOptionalLibraryFields("flow_temperature_c,return_temperature_c");
-
- addStringFieldWithExtractorAndLookup(
- "status",
- "Meter status from error flags and tpl status field.",
- DEFAULT_PRINT_PROPERTIES |
- PrintProperty::STATUS | PrintProperty::INCLUDE_TPL_STATUS,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::ErrorFlags),
- Translate::Lookup(
- {
- {
- {
- "ERROR_FLAGS",
- Translate::MapType::BitToString,
- AlwaysTrigger, MaskBits(0xffff),
- "OK",
- {
- }
- },
- },
- }));
-
- addNumericFieldWithExtractor(
- "total_energy_consumption",
- "The total heat energy consumption recorded by this meter.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Energy,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyEnergyVIF)
- );
-
- addNumericFieldWithExtractor(
- "total_volume",
- "The total heating media volume recorded by this meter.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Volume,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::Volume)
- );
-
- addNumericFieldWithExtractor(
- "volume_flow",
- "The current heat media volume flow.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Flow,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::VolumeFlow)
- );
-
- addNumericFieldWithExtractor(
- "power",
- "The current power consumption.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Power,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::PowerW)
- );
-
- addNumericFieldWithExtractor(
- "temperature_difference",
- "The difference between flow and return media temperatures.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Temperature,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::TemperatureDifference)
- );
-
- addNumericFieldWithExtractor(
- "set",
- "The most recent billing period date.",
- DEFAULT_PRINT_PROPERTIES | PrintProperty::HIDE,
- Quantity::PointInTime,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::Date)
- .set(StorageNr(1)),
- Unit::DateLT
- );
-
- addNumericFieldWithExtractor(
- "consumption_at_set_date_{storage_counter}",
- "The total water consumption at the historic date.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Energy,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyEnergyVIF)
- .set(StorageNr(1),StorageNr(31))
- );
-
- addNumericFieldWithCalculatorAndMatcher(
- "set_date_{storage_counter}",
- "Unclear! What is the date really?",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::PointInTime,
- "set_date - ((storage_counter-1counter) * 1 month)",
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyEnergyVIF)
- .set(StorageNr(1),StorageNr(31)),
- Unit::DateLT
- );
-
- }
-}
-
-// The meter sends two types of telegrams a shorter with current values.
-
-// Test: Heat microclima 93572431 NOKEY
-// telegram=|494424343124579300047a5a0000202f2f046d2720b62c04060d07000001fd170004130a8c0400043b00000000042b00000000025b1500025f15000261d0ff03fd0c05000002fd0b1011|
-// {"_":"telegram","flow_temperature_c":21,"id":"93572431","media":"heat","meter":"microclima","meter_datetime":"2021-12-22 00:39","model_version":"000005","name":"Heat","parameter_set":"1110","power_kw":0,"return_temperature_c":21,"status":"OK","temperature_difference_c":-0.48,"timestamp":"1111-11-11T11:11:11Z","total_energy_consumption_kwh":1805,"total_volume_m3":297.994,"volume_flow_m3h":0}
-// |Heat;93572431;OK;1805;297.994;1111-11-11 11:11.11
-
-
-// And a longer with historical values. This telegram is not yet properly decoded.
-
-// Test: Heat microclima 93573086 NOKEY
-// telegram=|A44424348630579300047ADD0000202F2F046D0721B62C04064708000004135DB0030001FD1700426C9F2C4406C6040000C40106C0070000C4020637070000C4030611070000C404060B070000C405060B070000C406060B070000C407060B070000C40806A5060000C40906F7050000C40A067A050000C40B060F050000C40C06C6040000C40D063F040000C40E06BB030000C40F06A502000003FD0C05000002FD0B1111|
-// {"_":"telegram","consumption_at_set_date_11_kwh":1803,"consumption_at_set_date_13_kwh":1803,"consumption_at_set_date_15_kwh":1803,"consumption_at_set_date_17_kwh":1701,"consumption_at_set_date_19_kwh":1527,"consumption_at_set_date_1_kwh":1222,"consumption_at_set_date_21_kwh":1402,"consumption_at_set_date_23_kwh":1295,"consumption_at_set_date_25_kwh":1222,"consumption_at_set_date_27_kwh":1087,"consumption_at_set_date_29_kwh":955,"consumption_at_set_date_31_kwh":677,"consumption_at_set_date_3_kwh":1984,"consumption_at_set_date_5_kwh":1847,"consumption_at_set_date_7_kwh":1809,"consumption_at_set_date_9_kwh":1803,"id":"93573086","media":"heat","meter":"microclima","meter_datetime":"2021-12-22 01:07","model_version":"000005","name":"Heat","parameter_set":"1111","set_date_11_date":"2020-02-29","set_date_13_date":"2019-12-31","set_date_15_date":"2019-10-31","set_date_17_date":"2019-08-31","set_date_19_date":"2019-06-30","set_date_1_date":"2020-12-31","set_date_21_date":"2019-04-30","set_date_23_date":"2019-02-28","set_date_25_date":"2018-12-31","set_date_27_date":"2018-10-31","set_date_29_date":"2018-08-31","set_date_31_date":"2018-06-30","set_date_3_date":"2020-10-31","set_date_5_date":"2020-08-31","set_date_7_date":"2020-06-30","set_date_9_date":"2020-04-30","status":"OK","timestamp":"1111-11-11T11:11:11Z","total_energy_consumption_kwh":2119,"total_volume_m3":241.757}
-// |Heat;93573086;OK;2119;241.757;1111-11-11 11:11.11
diff --git a/components/wmbus_common/driver_minomess.cpp b/components/wmbus_common/driver_minomess.cpp
deleted file mode 100644
index 1219657f..00000000
--- a/components/wmbus_common/driver_minomess.cpp
+++ /dev/null
@@ -1,226 +0,0 @@
-/*
- Copyright (C) 2021 Olli Salonen (gpl-3.0-or-later)
- Copyright (C) 2022-2023 Fredrik Öhrström (gpl-3.0-or-later)
-
- 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 .
-*/
-
-#include"meters_common_implementation.h"
-
-namespace
-{
- struct Driver : public virtual MeterCommonImplementation
- {
- Driver(MeterInfo &mi, DriverInfo &di);
- };
-
- static bool ok = staticRegisterDriver([](DriverInfo&di)
- {
- di.setName("minomess");
- di.setDefaultFields("name,id,total_m3,target_m3,status,timestamp");
- di.setMeterType(MeterType::WaterMeter);
- di.addLinkMode(LinkMode::C1);
- di.addMVT(MANUFACTURER_ZRI, 0x07, 0x00);
- di.addMVT(MANUFACTURER_ZRI, 0x16, 0x01);
- di.addMVT(MANUFACTURER_ZRI, 0x06, 0x01);
- di.setConstructor([](MeterInfo& mi, DriverInfo& di){ return shared_ptr(new Driver(mi, di)); });
- });
-
- Driver::Driver(MeterInfo &mi, DriverInfo &di) : MeterCommonImplementation(mi, di)
- {
- addOptionalLibraryFields("meter_date,fabrication_no,operating_time_h,on_time_h,on_time_at_error_h,meter_datetime");
- addOptionalLibraryFields("total_m3,total_backward_m3,volume_flow_m3h");
-
- /* If the meter is recently commissioned, the target water consumption value is bogus.
- The bits store 0xffffffff. Should we deal with this? Now a very large value is printed in the json.
-
- The wmbus telegram contains only storage 8 for target_date and total. */
- addNumericFieldWithExtractor(
- "target",
- "The total water consumption recorded at the beginning of this month.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Volume,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::Volume)
- .set(StorageNr(8))
- );
-
- addStringFieldWithExtractor(
- "target_date",
- "Date when target water consumption was recorded.",
- DEFAULT_PRINT_PROPERTIES,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::Date)
- .set(StorageNr(8))
- );
-
- // The wire mbus telegram contains 4 totals and dates. For the moment we only print nr 1 which is the latest.
- addNumericFieldWithExtractor(
- "target",
- "The total water consumption recorded at the beginning of this month.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Volume,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::Volume)
- .set(StorageNr(1))
- );
-
- addStringFieldWithExtractor(
- "target_date",
- "Date when target water consumption was recorded.",
- DEFAULT_PRINT_PROPERTIES,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::Date)
- .set(StorageNr(1))
- );
-
- /*
- According to data sheet, there are two status/info bytes, byte A and byte B.
- Unfortunately we do not now is byte A is the first or second byte. Oh well.
- Now we guess that A is the hi byte. I.e. 0x8000 is byte A bit 7.
- In the telegram the byte order is: lo byte first followed by the hi byte.
- So the defacto telegram bytes would be 0x0080 for byte A bit 7.
-
- Byte A:
- bit 7 removal active in the past
- bit 6 tamper active in the past
- bit 5 leak active in the past
- bit 4 temporary error (in connection with smart functions)
- bit 3 permanent error (meter value might be lost)
- bit 2 battery EOL (measured)
- bit 1 abnormal error
- bit 0 unused
-
- Byte B:
- bit 7 burst
- bit 6 removal
- bit 5 leak
- bit 4 backflow in the past
- bit 3 backflow
- bit 2 meter blocked in the past
- bit 1 meter undersized
- bit 0 meter oversized
- */
-
- addStringFieldWithExtractorAndLookup(
- "status",
- "Status and error flags.",
- DEFAULT_PRINT_PROPERTIES,
- FieldMatcher::build()
- .set(DifVifKey("02FD17"))
- ,
- {
- {
- {
- "ERROR_FLAGS",
- Translate::MapType::BitToString,
- AlwaysTrigger, MaskBits(0xffff),
- "OK",
- {
- { 0x8000, "WAS_REMOVED" },
- { 0x4000, "WAS_TAMPERED" },
- { 0x2000, "WAS_LEAKING" },
- { 0x1000, "TEMPORARY_ERROR" },
- { 0x0800, "PERMANENT_ERROR" },
- { 0x0400, "BATTERY_EOL" },
- { 0x0200, "ABNORMAL_ERROR" },
- // 0x0100 not used
- { 0x0080, "BURSTING" },
- { 0x0040, "REMOVED" },
- { 0x0020, "LEAKING" },
- { 0x0010, "WAS_BACKFLOWING" },
- { 0x0008, "BACKFLOWING" },
- { 0x0004, "WAS_BLOCKED" },
- { 0x0002, "UNDERSIZED" },
- { 0x0001, "OVERSIZED" }
- }
- },
- },
- });
-
- }
-}
-
- // 00: 66 length (102 bytes)
- // 01: 44 dll-c (from meter SND_NR)
- // 02: 496a dll-mfct (ZRI)
- // 04: 10640355 dll-id (55036410)
- // 08: 14 dll-version
- // 09: 37 dll-type (Radio converter (meter side))
- // 0a: 72 tpl-ci-field (EN 13757-3 Application Layer (long tplh))
- // 0b: 51345015 tpl-id (15503451)
- // 0f: 496a tpl-mfct (ZRI)
- // 11: 00 tpl-version
- // 12: 07 tpl-type (Water meter)
- // 13: 76 tpl-acc-field
- // 14: 00 tpl-sts-field (OK)
- // 15: 5005 tpl-cfg 0550 (AES_CBC_IV nb=5 cntn=0 ra=0 hc=0 )
- // 17: 2f2f decrypt check bytes
-
- // 19: 0C dif (8 digit BCD Instantaneous value)
- // 1a: 13 vif (Volume l)
- // 1b: * 55140000 total consumption (1.455000 m3)
- // 1f: 02 dif (16 Bit Integer/Binary Instantaneous value)
- // 20: 6C vif (Date type G)
- // 21: * A92B meter date (2021-11-09)
- // 23: 82 dif (16 Bit Integer/Binary Instantaneous value)
- // 24: 04 dife (subunit=0 tariff=0 storagenr=8)
- // 25: 6C vif (Date type G)
- // 26: * A12B target consumption reading date (2021-11-01)
- // 28: 8C dif (8 digit BCD Instantaneous value)
- // 29: 04 dife (subunit=0 tariff=0 storagenr=8)
- // 2a: 13 vif (Volume l)
- // 2b: * 71000000 target consumption (0.071000 m3)
- //
- // 2f: 8D dif (variable length Instantaneous value)
- // 30: 04 dife (subunit=0 tariff=0 storagenr=8)
- // 31: 93 vif (Volume l)
- // 32: 13 vife (Reverse compact profile without register)
- // 33: 2C varlen=44
- // This register has 24-bit integers for the consumption of the past months n-2 until n-15.
- // If the meter is commissioned less than 15 months ago, you will see FFFFFF as the value.
- // n-2 n-3 n-4 n-5 n-6 n-7 n-8 n-9 n-10 n-11 n-12 n-13 n-14 n-15
- // 34: FBFE 000000 FFFFFF FFFFFF FFFFFF FFFFFF FFFFFF FFFFFF FFFFFF FFFFFF FFFFFF FFFFFF FFFFFF FFFFFF FFFFFF
- //
- // 60: 02 dif (16 Bit Integer/Binary Instantaneous value)
- // 61: FD vif (Second extension FD of VIF-codes)
- // 62: 17 vife (Error flags (binary))
- // 63: * 0000 info codes (OK)
-
-
-// Test: Mino minomess 15503451 NOKEY
-// telegram=|6644496A1064035514377251345015496A0007EE0050052F2F#0C1359000000026CBE2B82046CA12B8C0413FFFFFFFF8D0493132CFBFEFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF02FD1700002F2F|
-// {"_":"telegram","media":"water","meter":"minomess","name":"Mino","id":"15503451","meter_date":"2021-11-30","total_m3":0.059,"target_date":"2021-11-01","status":"OK","timestamp":"1111-11-11T11:11:11Z"}
-// |Mino;15503451;0.059;null;OK;1111-11-11 11:11.11
-
-// Test: Minowired minomess 57575757 NOKEY
-// telegram=|6874746808007257575757496A000712000000_0C7857575757046D2414DE280413000000000C943C000000004413FFFFFFFF426CFFFF840113FFFFFFFF82016CFFFFC40113FFFFFFFFC2016CFFFF840213FFFFFFFF82026CFFFF043B000000000422E62F000004260000000034220000000002FD1700001F5716|
-// {"_":"telegram","media":"water","meter":"minomess","name":"Minowired","id":"57575757","fabrication_no":"57575757","operating_time_h":0,"on_time_h":12262,"on_time_at_error_h":0,"meter_datetime":"2022-08-30 20:36","total_m3":0,"total_backward_m3":0,"volume_flow_m3h":0,"target_m3":-0.001,"target_date":"2127-15-31","status":"OK","timestamp":"1111-11-11T11:11:11Z"}
-// |Minowired;57575757;0;-0.001;OK;1111-11-11 11:11.11
-
-// Test: Zenner_cold minomess 21314151 NOKEY
-// telegram=|6644496A4425155518377251413121496A0116360050052F2F_0C1355000000026CEC2182046CE1218C0413000000808D0493132C33FE00008000008000008000008000008000008000008000008000008000008000008000008000008000008002FD1700002F2F|
-// {"_":"telegram","media":"cold water","meter":"minomess","name":"Zenner_cold","id":"21314151","meter_date":"2023-01-12","total_m3":0.055,"target_m3":80000,"target_date":"2023-01-01","status":"OK","timestamp":"1111-11-11T11:11:11Z"}
-// |Zenner_cold;21314151;0.055;80000;OK;1111-11-11 11:11.11
-
-// Test: Zenner_warm minomess 51413121 NOKEY
-// telegram=|6644496A8753155518377221314151496A0106300050052F2F_0C1357000000026CEC2182046CE1218C0413000000808D0493132C33FE00008000008000008000008000008000008000008000008000008000008000008000008000008000008002FD1700002F2F|
-// {"_":"telegram","media":"warm water","meter":"minomess","name":"Zenner_warm","id":"51413121","meter_date":"2023-01-12","total_m3":0.057,"target_m3":80000,"target_date":"2023-01-01","status":"OK","timestamp":"1111-11-11T11:11:11Z"}
-// |Zenner_warm;51413121;0.057;80000;OK;1111-11-11 11:11.11
diff --git a/components/wmbus_common/driver_mkradio3.cpp b/components/wmbus_common/driver_mkradio3.cpp
deleted file mode 100644
index 4d4e6968..00000000
--- a/components/wmbus_common/driver_mkradio3.cpp
+++ /dev/null
@@ -1,111 +0,0 @@
-/*
- Copyright (C) 2019-2023 Fredrik Öhrström (gpl-3.0-or-later)
-
- 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 .
-*/
-
-#include"meters_common_implementation.h"
-
-namespace
-{
- struct Driver : public virtual MeterCommonImplementation
- {
- Driver(MeterInfo &mi, DriverInfo &di);
-
- void processContent(Telegram *t);
- };
-
- static bool ok = staticRegisterDriver([](DriverInfo&di)
- {
- di.setName("mkradio3");
- di.setDefaultFields("name,id,total_m3,target_m3,current_date,prev_date,timestamp");
- di.setMeterType(MeterType::WaterMeter);
- di.addLinkMode(LinkMode::T1);
- di.addMVT(MANUFACTURER_TCH, 0x62, 0x74);
- di.addMVT(MANUFACTURER_TCH, 0x72, 0x74);
- di.usesProcessContent();
- di.setConstructor([](MeterInfo& mi, DriverInfo& di){ return shared_ptr(new Driver(mi, di)); });
- });
-
- Driver::Driver(MeterInfo &mi, DriverInfo &di) : MeterCommonImplementation(mi, di)
- {
- addNumericField("total",
- Quantity::Volume,
- DEFAULT_PRINT_PROPERTIES,
- "The total water consumption recorded by this meter.");
-
- addNumericField("target",
- Quantity::Volume,
- DEFAULT_PRINT_PROPERTIES,
- "The total water consumption recorded at the beginning of this month.");
-
- addStringField("current_date",
- "Date of current billing period.",
- DEFAULT_PRINT_PROPERTIES);
-
- addStringField("prev_date",
- "Date of previous billing period.",
- DEFAULT_PRINT_PROPERTIES);
- }
-
- void Driver::processContent(Telegram *t)
- {
- // Unfortunately, the MK Radio 3 is mostly a proprieatary protocol
- // simple wrapped inside a wmbus telegram since the ci-field is 0xa2.
- // Which means that the entire payload is manufacturer specific.
-
- std::map> vendor_values;
- std::vector content;
-
- t->extractPayload(&content);
-
- // Previous date
- uint16_t prev_date = (content[2] << 8) | content[1];
- uint prev_date_day = (prev_date >> 0) & 0x1F;
- uint prev_date_month = (prev_date >> 5) & 0x0F;
- uint prev_date_year = (prev_date >> 9) & 0x3F;
- setStringValue("prev_date",
- tostrprintf("%d-%02d-%02dT02:00:00Z", prev_date_year+2000, prev_date_month, prev_date_day));
-
- // Previous consumption
- uchar prev_lo = content[3];
- uchar prev_hi = content[4];
- double prev = (256.0*prev_hi+prev_lo)/10.0;
-
- // Current date
- uint16_t current_date = (content[6] << 8) | content[5];
- uint current_date_day = (current_date >> 4) & 0x1F;
- uint current_date_month = (current_date >> 9) & 0x0F;
- setStringValue("current_date",
- tostrprintf("%s-%02d-%02dT02:00:00Z",
- currentYear().c_str(), current_date_month, current_date_day));
-
- // Current consumption
- uchar curr_lo = content[7];
- uchar curr_hi = content[8];
- double curr = (256.0*curr_hi+curr_lo)/10.0;
-
- double total_water_consumption_m3 = prev+curr;
- setNumericValue("total", Unit::M3, total_water_consumption_m3);
- double target_water_consumption_m3 = prev;
- setNumericValue("target", Unit::M3, target_water_consumption_m3);
- }
-}
-
-// Test: Duschen mkradio3 34333231 NOKEY
-// Comment: There is a problem in the decoding here, the data stored inside the telegram does not seem to properly encode/decode the year....
-// We should not report a current_date with a full year, if the year is actually not part of the telegram.
-// telegram=|2F446850313233347462A2_069F255900B029310000000306060906030609070606050509050505050407040605070500|
-// {"_":"telegram","media":"warm water","meter":"mkradio3","name":"Duschen","id":"34333231","total_m3":13.8,"target_m3":8.9,"current_date":"2026-04-27T02:00:00Z","prev_date":"2018-12-31T02:00:00Z","timestamp":"1111-11-11T11:11:11Z"}
-// |Duschen;34333231;13.8;8.9;2026-04-27T02:00:00Z;2018-12-31T02:00:00Z;1111-11-11 11:11.11
diff --git a/components/wmbus_common/driver_mkradio3a.cpp b/components/wmbus_common/driver_mkradio3a.cpp
deleted file mode 100644
index e64e855f..00000000
--- a/components/wmbus_common/driver_mkradio3a.cpp
+++ /dev/null
@@ -1,252 +0,0 @@
-/*
- Copyright (C) 2019-2023 Fredrik Öhrström (gpl-3.0-or-later)
-
- 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 .
-*/
-
-#include"meters_common_implementation.h"
-
-namespace
-{
- struct Driver : public virtual MeterCommonImplementation
- {
- Driver(MeterInfo &mi, DriverInfo &di);
-
- void processContent(Telegram *t);
- };
-
- static bool ok = staticRegisterDriver([](DriverInfo&di)
- {
- di.setName("mkradio3a");
- di.setDefaultFields("name,id,total_m3,target_m3,timestamp");
- di.setMeterType(MeterType::WaterMeter);
- di.addLinkMode(LinkMode::T1);
- di.addMVT(MANUFACTURER_TCH, 0x72, 0x50);
- di.usesProcessContent();
- di.setConstructor([](MeterInfo& mi, DriverInfo& di){ return shared_ptr(new Driver(mi, di)); });
- });
-
- Driver::Driver(MeterInfo &mi, DriverInfo &di) : MeterCommonImplementation(mi, di)
- {
- addNumericField("total",
- Quantity::Volume,
- DEFAULT_PRINT_PROPERTIES,
- "The total water consumption recorded by this meter.");
-
- addNumericField("target",
- Quantity::Volume,
- DEFAULT_PRINT_PROPERTIES,
- "The total water consumption recorded at the beginning of this month.");
-
- addNumericField("last_jan",
- Quantity::Volume,
- DEFAULT_PRINT_PROPERTIES,
- "The total water consumption recorded at the beginning of this month.");
-
- addNumericField("last_feb",
- Quantity::Volume,
- DEFAULT_PRINT_PROPERTIES,
- "The total water consumption recorded at the beginning of this month.");
-
- addNumericField("last_mar",
- Quantity::Volume,
- DEFAULT_PRINT_PROPERTIES,
- "The total water consumption recorded at the beginning of this month.");
-
- addNumericField("last_apr",
- Quantity::Volume,
- DEFAULT_PRINT_PROPERTIES,
- "The total water consumption recorded at the beginning of this month.");
-
- addNumericField("last_may",
- Quantity::Volume,
- DEFAULT_PRINT_PROPERTIES,
- "The total water consumption recorded at the beginning of this month.");
-
- addNumericField("last_jun",
- Quantity::Volume,
- DEFAULT_PRINT_PROPERTIES,
- "The total water consumption recorded at the beginning of this month.");
-
- addNumericField("last_jul",
- Quantity::Volume,
- DEFAULT_PRINT_PROPERTIES,
- "The total water consumption recorded at the beginning of this month.");
-
- addNumericField("last_aug",
- Quantity::Volume,
- DEFAULT_PRINT_PROPERTIES,
- "The total water consumption recorded at the beginning of this month.");
-
- addNumericField("last_sep",
- Quantity::Volume,
- DEFAULT_PRINT_PROPERTIES,
- "The total water consumption recorded at the beginning of this month.");
-
- addNumericField("last_oct",
- Quantity::Volume,
- DEFAULT_PRINT_PROPERTIES,
- "The total water consumption recorded at the beginning of this month.");
-
- addNumericField("last_nov",
- Quantity::Volume,
- DEFAULT_PRINT_PROPERTIES,
- "The total water consumption recorded at the beginning of this month.");
-
- addNumericField("last_dec",
- Quantity::Volume,
- DEFAULT_PRINT_PROPERTIES,
- "The total water consumption recorded at the beginning of this month.");
-
- addStringField("target_date",
- "Date of current billing period.",
- DEFAULT_PRINT_PROPERTIES);
- }
-
- void Driver::processContent(Telegram *t)
- {
- // Unfortunately, the MK Radio 3 is mostly a proprieatary protocol
- // simple wrapped inside a wmbus telegram since the ci-field is 0xa2.
- // Which means that the entire payload is manufacturer specific.
-
- std::map> vendor_values;
- std::vector content;
-
- t->extractPayload(&content);
-
- // Current date
- uint16_t curr_date = (content[3] << 8) | content[2];
- uint curr_date_day = (curr_date >> 0) & 0x1F;
- uint curr_date_month = (curr_date >> 5) & 0x0F;
- uint curr_date_year = (curr_date >> 9) & 0xFF;
- setStringValue("target_date", tostrprintf("%d-%02d-%02dT02:00:00Z", curr_date_year+2000, curr_date_month, curr_date_day));
-
- // Previous consumption
- // uchar prev_lo = content[8];
- // uchar prev_hi = content[9];
- // double prev = (256.0*prev_hi+prev_lo)/10.0;
-
- // Total consumption
- uchar total_lo = content[4];
- uchar total_me = content[5];
- uchar total_hi = content[6];
- double total_water_consumption_m3 = (256.0*256.0*total_hi+256.0*total_me+total_lo)/10.0;
- setNumericValue("total", Unit::M3, total_water_consumption_m3);
-
-
- double prev_month[20];
- double curr_month_m3;
-
- // Current & Prev Month consumption
- if (curr_date_day <= 15) {
- uchar prev_lo = content[8];
- curr_month_m3 = prev_lo/10.0;
- prev_month[ (curr_date_month + 12) % 12 ] = curr_month_m3;
- prev_lo = content[9];
- uchar prev_hi = content[11];
- prev_month[ (curr_date_month + 12 - 1) % 12 ] = (prev_hi+prev_lo)/10.0;
- prev_lo = content[12];
- prev_hi = content[14];
- prev_month[ (curr_date_month + 12 - 2) % 12 ] = (prev_hi+prev_lo)/10.0;
- prev_lo = content[15];
- prev_hi = content[17];
- prev_month[ (curr_date_month + 12 - 3) % 12 ] = (prev_hi+prev_lo)/10.0;
- prev_lo = content[18];
- prev_hi = content[20];
- prev_month[ (curr_date_month + 12 - 4) % 12 ] = (prev_hi+prev_lo)/10.0;
- prev_lo = content[21];
- prev_hi = content[23];
- prev_month[ (curr_date_month + 12 - 5) % 12 ] = (prev_hi+prev_lo)/10.0;
- prev_lo = content[24];
- prev_hi = content[26];
- prev_month[ (curr_date_month + 12 - 6) % 12 ] = (prev_hi+prev_lo)/10.0;
- prev_lo = content[27];
- prev_hi = content[29];
- prev_month[ (curr_date_month + 12 - 7) % 12 ] = (prev_hi+prev_lo)/10.0;
- prev_lo = content[30];
- prev_hi = content[32];
- prev_month[ (curr_date_month + 12 - 8) % 12 ] = (prev_hi+prev_lo)/10.0;
- prev_lo = content[33];
- prev_hi = content[35];
- prev_month[ (curr_date_month + 12 - 9) % 12 ] = (prev_hi+prev_lo)/10.0;
- prev_lo = content[36];
- prev_hi = content[38];
- prev_month[ (curr_date_month + 12 - 10) % 12 ] = (prev_hi+prev_lo)/10.0;
- prev_lo = content[39];
- prev_hi = content[41];
- prev_month[ (curr_date_month + 12 - 11) % 12 ] = (prev_hi+prev_lo)/10.0;
- //prev_lo = content[42];
- //prev_month[ (curr_date_month + 12 - 11) % 12 ] = 0.0;
- }else{
- uchar prev_lo = content[8];
- uchar prev_hi = content[9];
- curr_month_m3 = (prev_hi+prev_lo)/10.0;
- prev_month[ (curr_date_month + 12) % 12 ] = curr_month_m3;
- prev_lo = content[11];
- prev_hi = content[12];
- prev_month[ (curr_date_month + 12 - 1) % 12 ] = (prev_hi+prev_lo)/10.0;
- prev_lo = content[14];
- prev_hi = content[15];
- prev_month[ (curr_date_month + 12 - 2) % 12 ] = (prev_hi+prev_lo)/10.0;
- prev_lo = content[17];
- prev_hi = content[18];
- prev_month[ (curr_date_month + 12 - 3) % 12 ] = (prev_hi+prev_lo)/10.0;
- prev_lo = content[20];
- prev_hi = content[21];
- prev_month[ (curr_date_month + 12 - 4) % 12 ] = (prev_hi+prev_lo)/10.0;
- prev_lo = content[23];
- prev_hi = content[24];
- prev_month[ (curr_date_month + 12 - 5) % 12 ] = (prev_hi+prev_lo)/10.0;
- prev_lo = content[26];
- prev_hi = content[27];
- prev_month[ (curr_date_month + 12 - 6) % 12 ] = (prev_hi+prev_lo)/10.0;
- prev_lo = content[29];
- prev_hi = content[30];
- prev_month[ (curr_date_month + 12 - 7) % 12 ] = (prev_hi+prev_lo)/10.0;
- prev_lo = content[32];
- prev_hi = content[33];
- prev_month[ (curr_date_month + 12 - 8) % 12 ] = (prev_hi+prev_lo)/10.0;
- prev_lo = content[35];
- prev_hi = content[36];
- prev_month[ (curr_date_month + 12 - 9) % 12 ] = (prev_hi+prev_lo)/10.0;
- prev_lo = content[38];
- prev_hi = content[39];
- prev_month[ (curr_date_month + 12 - 10) % 12 ] = (prev_hi+prev_lo)/10.0;
- prev_lo = content[41];
- prev_hi = content[42];
- prev_month[ (curr_date_month + 12 - 11) % 12 ] = (prev_hi+prev_lo)/10.0;
- }
- setNumericValue("target", Unit::M3, curr_month_m3);
- setNumericValue("last_jan", Unit::M3, prev_month[1]);
- setNumericValue("last_feb", Unit::M3, prev_month[2]);
- setNumericValue("last_mar", Unit::M3, prev_month[3]);
- setNumericValue("last_apr", Unit::M3, prev_month[4]);
- setNumericValue("last_may", Unit::M3, prev_month[5]);
- setNumericValue("last_jun", Unit::M3, prev_month[6]);
- setNumericValue("last_jul", Unit::M3, prev_month[7]);
- setNumericValue("last_aug", Unit::M3, prev_month[8]);
- setNumericValue("last_sep", Unit::M3, prev_month[9]);
- setNumericValue("last_oct", Unit::M3, prev_month[10]);
- setNumericValue("last_nov", Unit::M3, prev_month[11]);
- setNumericValue("last_dec", Unit::M3, prev_month[0]);
- }
-}
-
-// Test: TCH mkradio3a 62560642 NOKEY
-// 7C31 => 317C : Date
-// 102500 => 002510 : water meter value (same as digital one)
-// 2934 => 3429 : current month
-// telegram=|36446850420656625072A2_0C007C3110250000293400373A002E38000E15002F37003A39003835002F24003930001D2500312500162900|
-// { "_":"telegram", "media":"cold water", "meter":"mkradio3a", "name":"TCH", "id":"62560642", "target_m3":9.3, "last_apr_m3":8.3, "last_aug_m3":3.5, "last_dec_m3":6.3, "last_feb_m3":6.6, "last_jan_m3":8.6, "last_jul_m3":10.2, "last_jun_m3":11.5, "last_mar_m3":10.5, "last_may_m3":10.9, "last_nov_m3":9.3, "last_oct_m3":11.3, "last_sep_m3":10.2, "total_m3":948.8, "target_date":"2024-11-28T02:00:00Z", "timestamp":"1111-11-11T11:11:11Z" }
-// |TCH;62560642;948.8;9.3;1111-11-11 11:11.11
diff --git a/components/wmbus_common/driver_mkradio4.cpp b/components/wmbus_common/driver_mkradio4.cpp
deleted file mode 100644
index 00b1ccd2..00000000
--- a/components/wmbus_common/driver_mkradio4.cpp
+++ /dev/null
@@ -1,103 +0,0 @@
-/*
- Copyright (C) 2019-2023 Fredrik Öhrström (gpl-3.0-or-later)
-
- 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 .
-*/
-
-#include"meters_common_implementation.h"
-
-namespace
-{
- struct Driver : public virtual MeterCommonImplementation
- {
- Driver(MeterInfo &mi, DriverInfo &di);
-
- void processContent(Telegram *t);
- };
-
- static bool ok = staticRegisterDriver([](DriverInfo&di)
- {
- di.setName("mkradio4");
- di.setDefaultFields("name,id,total_m3,target_m3,timestamp");
- di.setMeterType(MeterType::WaterMeter);
- di.addLinkMode(LinkMode::T1);
- di.addMVT(MANUFACTURER_TCH, 0x62, 0x95);
- di.addMVT(MANUFACTURER_TCH, 0x62, 0x70);
- di.addMVT(MANUFACTURER_TCH, 0x72, 0x95);
- di.addMVT(MANUFACTURER_TCH, 0x72, 0x70);
- di.usesProcessContent();
- di.setConstructor([](MeterInfo& mi, DriverInfo& di){ return shared_ptr(new Driver(mi, di)); });
- });
-
- Driver::Driver(MeterInfo &mi, DriverInfo &di) : MeterCommonImplementation(mi, di)
- {
- addNumericField("total",
- Quantity::Volume,
- DEFAULT_PRINT_PROPERTIES,
- "The total water consumption recorded by this meter.");
-
- addNumericField("target",
- Quantity::Volume,
- DEFAULT_PRINT_PROPERTIES,
- "The total water consumption recorded at the beginning of this month.");
- }
-
- void Driver::processContent(Telegram *t)
- {
- // The MK Radio 4 is mostly a proprietary protocol
- // simple wrapped inside a wmbus telegram since the ci-field is 0xa2.
- // Which means that the entire payload is manufacturer specific.
-
- std::map> vendor_values;
- std::vector content;
-
- t->extractPayload(&content);
-
- uchar prev_lo = content[3];
- uchar prev_hi = content[4];
- double prev = (256.0*prev_hi+prev_lo)/10.0;
-
- /*
- std::string prevs;
- strprintf(&prevs, "%02x%02x", prev_lo, prev_hi);
- int offset = t->parsed.size()+3;
- vendor_values["0215"] = { offset, DVEntry(offset, DifVifKey("0215"), MeasurementType::Instantaneous, 0x15, {}, 0, 0, 0, prevs) };
- t->explanations.push_back(Explanation(offset, 2, prevs, KindOfData::CONTENT, Understanding::FULL));
- t->addMoreExplanation(offset, " prev consumption (%f m3)", prev);
- */
-
- uchar curr_lo = content[7];
- uchar curr_hi = content[8];
- double curr = (256.0*curr_hi+curr_lo)/10.0;
-
- /*
- std::string currs;
- strprintf(&currs, "%02x%02x", curr_lo, curr_hi);
- offset = t->parsed.size()+7;
- vendor_values["0215"] = { offset, DVEntry(offset, DifVifKey("0215"), MeasurementType::Instantaneous, 0x15, {}, 0, 0, 0, currs) };
- t->explanations.push_back(Explanation(offset, 2, currs, KindOfData::CONTENT, Understanding::FULL));
- t->addMoreExplanation(offset, " curr consumption (%f m3)", curr);
- */
-
- double total_water_consumption_m3 = prev+curr;
- setNumericValue("total", Unit::M3, total_water_consumption_m3);
- double target_water_consumption_m3 = prev;
- setNumericValue("target", Unit::M3, target_water_consumption_m3);
- }
-}
-
-// Test: Duschagain mkradio4 02410120 NOKEY
-// telegram=|2F446850200141029562A2_06702901006017030004000300000000000000000000000000000000000000000000000000|
-// {"_":"telegram","media":"warm water","meter":"mkradio4","name":"Duschagain","id":"02410120","total_m3":0.4,"target_m3":0.1,"timestamp":"1111-11-11T11:11:11Z"}
-// |Duschagain;02410120;0.4;0.1;1111-11-11 11:11.11
diff --git a/components/wmbus_common/driver_mkradio4a.cpp b/components/wmbus_common/driver_mkradio4a.cpp
deleted file mode 100644
index c2ea98ca..00000000
--- a/components/wmbus_common/driver_mkradio4a.cpp
+++ /dev/null
@@ -1,80 +0,0 @@
-/*
- Copyright (C) 2017-2022 Fredrik Öhrström (gpl-3.0-or-later)
-
- 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 .
-*/
-
-#include"meters_common_implementation.h"
-
-namespace
-{
- struct Driver : public virtual MeterCommonImplementation
- {
- Driver(MeterInfo &mi, DriverInfo &di);
- };
-
- static bool ok = staticRegisterDriver([](DriverInfo&di)
-
- {
- di.setName("mkradio4a");
- di.setDefaultFields("name,id,target_m3,target_date,timestamp");
- di.setMeterType(MeterType::WaterMeter);
- di.addLinkMode(LinkMode::T1);
- di.addLinkMode(LinkMode::C1);
- di.addMVT(MANUFACTURER_HYD, 0x06, 0xfe);
- di.addMVT(MANUFACTURER_TCH, 0x37, 0x95);
- di.setConstructor([](MeterInfo& mi, DriverInfo& di){ return shared_ptr(new Driver(mi, di)); });
- });
-
- Driver::Driver(MeterInfo &mi, DriverInfo &di) : MeterCommonImplementation(mi, di)
- {
- setExpectedTPLSecurityMode(TPLSecurityMode::AES_CBC_IV);
-
- addNumericFieldWithExtractor(
- "target",
- "The total water consumption recorded at the end of previous year.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Volume,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::Volume)
- .set(StorageNr(1))
- );
-
- addNumericFieldWithExtractor(
- "target",
- "Date when previous year ended.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::PointInTime,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::Date)
- .set(StorageNr(1)),
- Unit::DateLT
- );
- }
-}
-// Test: FOO mkradio4a 66953825 NOKEY
-// Comment: Warm water
-// telegram=|4B44685036494600953772253895662423FE064E0030052F2F_4315A10000426CBF2C0F542CF2DD8BEC869511B2DB8301C3ABA390FB4FDB6F1144DA1F3897DD55F2AD0D194F68510FF8FADFB9|
-// {"_":"telegram","media":"warm water","meter":"mkradio4a","name":"FOO","id":"66953825","target_m3":16.1,"target_date":"2021-12-31","timestamp":"1111-11-11T11:11:11Z"}
-// |FOO;66953825;16.1;2021-12-31;1111-11-11 11:11.11
-
-// Test: BAR mkradio4a 01770002 NOKEY
-// Comment: Cold water
-// telegram=|4B4468508644710095377202007701A85CFE078A0030052F2F_4315F00200426CBF2C0FEE456BF6F802216503E25EB73E9377D54F672681B76C469696E4C7BCCC9072CC79F712360FC3F57D85|
-// {"_":"telegram","media":"water","meter":"mkradio4a","name":"BAR","id":"01770002","target_m3":75.2,"target_date":"2021-12-31","timestamp":"1111-11-11T11:11:11Z"}
-// |BAR;01770002;75.2;2021-12-31;1111-11-11 11:11.11
diff --git a/components/wmbus_common/driver_multical21.cpp b/components/wmbus_common/driver_multical21.cpp
deleted file mode 100644
index b63fd59b..00000000
--- a/components/wmbus_common/driver_multical21.cpp
+++ /dev/null
@@ -1,280 +0,0 @@
-/*
- Copyright (C) 2017-2022 Fredrik Öhrström (gpl-3.0-or-later)
-
- 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 .
-*/
-
-#include"meters_common_implementation.h"
-
-namespace
-{
- struct Driver : public virtual MeterCommonImplementation {
- Driver(MeterInfo &mi, DriverInfo &di);
- };
-
- static bool ok = staticRegisterDriver([](DriverInfo&di)
- {
- di.setName("multical21");
- di.setDefaultFields("name,id,total_m3,target_m3,max_flow_m3h,flow_temperature_c,external_temperature_c,status,timestamp");
-
- di.setMeterType(MeterType::WaterMeter);
- di.addLinkMode(LinkMode::C1);
- // Multical21
- di.addMVT(MANUFACTURER_KAM, 0x06, 0x1b);
- di.addMVT(MANUFACTURER_KAM, 0x16, 0x1b);
-
- di.setConstructor([](MeterInfo& mi, DriverInfo& di){ return shared_ptr(new Driver(mi, di)); });
- });
-
- Driver::Driver(MeterInfo &mi, DriverInfo &di) : MeterCommonImplementation(mi, di)
- {
- addStringFieldWithExtractorAndLookup(
- "status",
- "Status of meter.",
- DEFAULT_PRINT_PROPERTIES | PrintProperty::STATUS,
- FieldMatcher::build()
- .set(DifVifKey("02FF20")),
- Translate::Lookup()
- .add(Translate::Rule("ERROR_FLAGS", Translate::MapType::BitToString)
- .set(MaskBits(0x000f))
- .set(DefaultMessage("OK"))
- .add(Translate::Map(0x01 ,"DRY", TestBit::Set))
- .add(Translate::Map(0x02 ,"REVERSE", TestBit::Set))
- .add(Translate::Map(0x04 ,"LEAK", TestBit::Set))
- .add(Translate::Map(0x08 ,"BURST", TestBit::Set))
- ));
-
- addNumericFieldWithExtractor(
- "total",
- "The total water consumption recorded by this meter.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Volume,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::Volume)
- );
-
- addNumericFieldWithExtractor(
- "target",
- "The total water consumption recorded at the beginning of this month.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Volume,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::Volume)
- .set(StorageNr(1))
- );
-
- addNumericFieldWithExtractor(
- "flow_temperature",
- "The water temperature.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Temperature,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Minimum)
- .set(VIFRange::FlowTemperature)
- .set(AnyStorageNr)
- );
-
- addNumericFieldWithExtractor(
- "external_temperature",
- "The external temperature outside of the meter.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Temperature,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Any)
- .set(VIFRange::ExternalTemperature)
- .set(AnyStorageNr)
- .add(VIFCombinable::Any)
- );
-
- addNumericFieldWithExtractor(
- "min_external_temperature",
- "The lowest external temperature outside of the meter.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Temperature,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Minimum)
- .set(VIFRange::ExternalTemperature)
- );
-
- addNumericFieldWithExtractor(
- "max_flow",
- "The maximum flow recorded during previous period.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Flow,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Maximum)
- .set(VIFRange::VolumeFlow)
- .set(AnyStorageNr)
- );
-
- addStringFieldWithExtractorAndLookup(
- "current_status",
- "Status of meter. This field will go away use status instead.",
- DEFAULT_PRINT_PROPERTIES | PrintProperty::DEPRECATED,
- FieldMatcher::build()
- .set(DifVifKey("02FF20")),
- {
- {
- {
- "ERROR_FLAGS",
- Translate::MapType::BitToString,
- AlwaysTrigger, MaskBits(0x000f),
- "",
- {
- { 0x01 , "DRY" },
- { 0x02 , "REVERSE" },
- { 0x04 , "LEAK" },
- { 0x08 , "BURST" },
- }
- },
- },
- });
-
- addStringFieldWithExtractorAndLookup(
- "time_dry",
- "Amount of time the meter has been dry.",
- DEFAULT_PRINT_PROPERTIES,
- FieldMatcher::build()
- .set(DifVifKey("02FF20")),
- {
- {
- {
- "DRY",
- Translate::MapType::IndexToString,
- AlwaysTrigger, MaskBits(0x0070),
- "",
- {
- { 0x0000, "" },
- { 0x0010, "1-8 hours" },
- { 0x0020, "9-24 hours" },
- { 0x0030, "2-3 days" },
- { 0x0040, "4-7 days" },
- { 0x0050, "8-14 days" },
- { 0x0060, "15-21 days" },
- { 0x0070, "22-31 days" },
- },
- },
- },
- });
-
- addStringFieldWithExtractorAndLookup(
- "time_reversed",
- "Amount of time the meter has been reversed.",
- DEFAULT_PRINT_PROPERTIES,
- FieldMatcher::build()
- .set(DifVifKey("02FF20")),
- {
- {
- {
- "REVERSED",
- Translate::MapType::IndexToString,
- AlwaysTrigger, MaskBits(0x0380),
- "",
- {
- { 0x0000, "" },
- { 0x0080, "1-8 hours" },
- { 0x0100, "9-24 hours" },
- { 0x0180, "2-3 days" },
- { 0x0200, "4-7 days" },
- { 0x0280, "8-14 days" },
- { 0x0300, "15-21 days" },
- { 0x0380, "22-31 days" },
- },
- },
- },
- });
-
- addStringFieldWithExtractorAndLookup(
- "time_leaking",
- "Amount of time the meter has been leaking.",
- DEFAULT_PRINT_PROPERTIES,
- FieldMatcher::build()
- .set(DifVifKey("02FF20")),
- {
- {
- {
- "LEAKING",
- Translate::MapType::IndexToString,
- AlwaysTrigger, MaskBits(0x1c00),
- "",
- {
- { 0x0000, "" },
- { 0x0400, "1-8 hours" },
- { 0x0800, "9-24 hours" },
- { 0x0c00, "2-3 days" },
- { 0x1000, "4-7 days" },
- { 0x1400, "8-14 days" },
- { 0x1800, "15-21 days" },
- { 0x1c00, "22-31 days" },
- },
- },
- },
- });
-
- addStringFieldWithExtractorAndLookup(
- "time_bursting",
- "Amount of time the meter has been bursting.",
- DEFAULT_PRINT_PROPERTIES,
- FieldMatcher::build()
- .set(DifVifKey("02FF20")),
- {
- {
- {
- "BURSTING",
- Translate::MapType::IndexToString,
- AlwaysTrigger, MaskBits(0xe000),
- "",
- {
- { 0x0000, "" },
- { 0x2000, "1-8 hours" },
- { 0x4000, "9-24 hours" },
- { 0x6000, "2-3 days" },
- { 0x8000, "4-7 days" },
- { 0xa000, "8-14 days" },
- { 0xc000, "15-21 days" },
- { 0xe000, "22-31 days" },
- },
- },
- },
- });
-
- }
-}
-
-// Test: MyTapWater multical21 76348799 NOKEY
-// Comment:
-// telegram=|2A442D2C998734761B168D2091D37CAC21576C78_02FF207100041308190000441308190000615B7F616713|
-// {"_":"telegram","media":"cold water","meter":"multical21","name":"MyTapWater","id":"76348799","status":"DRY","total_m3":6.408,"target_m3":6.408,"flow_temperature_c":127,"external_temperature_c":19,"current_status":"DRY","time_dry":"22-31 days","time_reversed":"","time_leaking":"","time_bursting":"","timestamp":"1111-11-11T11:11:11Z"}
-// |MyTapWater;76348799;6.408;6.408;null;127;19;DRY;1111-11-11 11:11.11
-
-// telegram=|23442D2C998734761B168D2087D19EAD217F1779EDA86AB6_710008190000081900007F13|
-// {"_":"telegram","media":"cold water","meter":"multical21","name":"MyTapWater","id":"76348799","status":"DRY","total_m3":6.408,"target_m3":6.408,"flow_temperature_c":127,"external_temperature_c":19,"current_status":"DRY","time_dry":"22-31 days","time_reversed":"","time_leaking":"","time_bursting":"","timestamp":"1111-11-11T11:11:11Z"}
-// |MyTapWater;76348799;6.408;6.408;null;127;19;DRY;1111-11-11 11:11.11
-
-// Test: Vadden multical21 44556677 NOKEY
-// telegram=|2D442D2C776655441B168D2083B48D3A20_46887802FF20000004132F4E000092013B3D01A1015B028101E7FF0F03|
-// {"_":"telegram","media":"cold water","meter":"multical21","name":"Vadden","id":"44556677","status":"OK","total_m3":20.015,"flow_temperature_c":2,"external_temperature_c":3,"max_flow_m3h":0.317,"current_status":"","time_dry":"","time_reversed":"","time_leaking":"","time_bursting":"","timestamp":"1111-11-11T11:11:11Z"}
-// |Vadden;44556677;20.015;null;0.317;2;3;OK;1111-11-11 11:11.11
-
-// telegram=|21442D2C776655441B168D2079CC8C3A20_F4307912C40DFF00002F4E00003D010203|
-// {"_":"telegram","media":"cold water","meter":"multical21","name":"Vadden","id":"44556677","status":"OK","total_m3":20.015,"flow_temperature_c":2,"external_temperature_c":3,"max_flow_m3h":0.317,"current_status":"","time_dry":"","time_reversed":"","time_leaking":"","time_bursting":"","timestamp":"1111-11-11T11:11:11Z"}
-// |Vadden;44556677;20.015;null;0.317;2;3;OK;1111-11-11 11:11.11
diff --git a/components/wmbus_common/driver_munia.cpp b/components/wmbus_common/driver_munia.cpp
deleted file mode 100644
index 5780e351..00000000
--- a/components/wmbus_common/driver_munia.cpp
+++ /dev/null
@@ -1,94 +0,0 @@
-/*
- Copyright (C) 2019-2022 Fredrik Öhrström (gpl-3.0-or-later)
-
- 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 .
-*/
-
-#include"meters_common_implementation.h"
-
-namespace
-{
- struct Driver : public virtual MeterCommonImplementation
- {
- Driver(MeterInfo &mi, DriverInfo &di);
- };
-
- static bool ok = staticRegisterDriver([](DriverInfo&di)
- {
- di.setName("munia");
- di.setDefaultFields("name,id,current_temperature_c,current_relative_humidity_rh,timestamp");
- di.setMeterType(MeterType::TempHygroMeter);
- di.setConstructor([](MeterInfo& mi, DriverInfo& di){ return shared_ptr(new Driver(mi, di)); });
- di.addMVT(MANUFACTURER_WEP, 0x1b, 0x02);
- di.addMVT(MANUFACTURER_WEP, 0x1b, 0x04);
- });
-
- Driver::Driver(MeterInfo &mi, DriverInfo &di) : MeterCommonImplementation(mi, di)
- {
- addStringFieldWithExtractorAndLookup(
- "status",
- "Meter status. Reports OK if neither tpl sts nor error flags have bits set.",
- DEFAULT_PRINT_PROPERTIES |
- PrintProperty::STATUS | PrintProperty::INCLUDE_TPL_STATUS,
- FieldMatcher::build()
- .set(DifVifKey("02FD971D")),
- Translate::Lookup({
- {
- {
- "ERROR_FLAGS",
- Translate::MapType::BitToString,
- AlwaysTrigger, MaskBits(0xffff),
- "OK",
- {
- // What does these bits mean?
- }
- },
- },
- }));
-
- addNumericFieldWithExtractor(
- "current_temperature",
- "The current temperature.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Temperature,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::ExternalTemperature)
- );
-
- addNumericFieldWithExtractor(
- "current_relative_humidity",
- "The current relative humidity.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::RelativeHumidity,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::RelativeHumidity)
- );
- }
-}
-
-// Munia
-// Test: TempoHygro munia 00013482 NOKEY
-// telegram=|2E44B05C82340100021B7A460000002F2F0A6601020AFB1A570602FD971D00002F2F2F2F2F2F2F2F2F2F2F2F2F2F2F|
-// {"_":"telegram","media":"room sensor","meter":"munia","name":"TempoHygro","id":"00013482","status":"OK","current_temperature_c":20.1,"current_relative_humidity_rh":65.7,"timestamp":"1111-11-11T11:11:11Z"}
-// |TempoHygro;00013482;20.1;65.7;1111-11-11 11:11.11
-
-// Robin
-// Test: TempoHygro munia 00220111 NOKEY
-// telegram=|2E44B05C11012200041B7A2B0000002F2F0A6617020AFB1A100602FD971D00002F2F2F2F2F2F2F2F2F2F2F2F2F2F2F|
-// {"_":"telegram","media":"room sensor","meter":"munia","name":"TempoHygro","id":"00220111","status":"OK","current_temperature_c":21.7,"current_relative_humidity_rh":61,"timestamp":"1111-11-11T11:11:11Z"}
-// |TempoHygro;00220111;21.7;61;1111-11-11 11:11.11
diff --git a/components/wmbus_common/driver_nemo.cpp b/components/wmbus_common/driver_nemo.cpp
deleted file mode 100644
index 079d6fa0..00000000
--- a/components/wmbus_common/driver_nemo.cpp
+++ /dev/null
@@ -1,459 +0,0 @@
-/*
- Copyright (C) 2023 Fredrik Öhrström (gpl-3.0-or-later)
-
- 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 .
-*/
-
-#include"meters_common_implementation.h"
-
-namespace
-{
- struct Driver : public virtual MeterCommonImplementation
- {
- Driver(MeterInfo &mi, DriverInfo &di);
- };
-
- static bool ok = staticRegisterDriver([](DriverInfo&di)
- {
- di.setName("nemo");
- di.setDefaultFields(
- "name,id,status,"
- "total_active_positive_3phase_kwh,"
- "active_positive_3phase_kw,"
- "timestamp");
- di.setMeterType(MeterType::ElectricityMeter);
- di.addLinkMode(LinkMode::MBUS);
- di.addMVT(MANUFACTURER_IME, 0x02, 0x1d);
- di.setConstructor([](MeterInfo& mi, DriverInfo& di){ return shared_ptr(new Driver(mi, di)); });
- });
-
- // Telegram 1 /////////////////////////////////////////////////////////////////////
-
- Driver::Driver(MeterInfo &mi, DriverInfo &di) : MeterCommonImplementation(mi, di)
- {
- addNumericFieldWithExtractor(
- "total_active_positive_3phase",
- "Et+ the total 3-phase active positive energy.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Energy,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyEnergyVIF)
- .set(StorageNr(0))
- .set(TariffNr(1))
- .set(SubUnitNr(1))
- );
-
- addNumericFieldWithExtractor(
- "active_positive_3phase",
- "P+ the 3-phase active positive power.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Power,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyPowerVIF)
- .set(StorageNr(0))
- .set(TariffNr(1))
- .set(SubUnitNr(1))
- );
-
- addNumericFieldWithExtractor(
- "total_reactive_positive_3phase",
- "Er+ the total 3-phase reactive positive energy.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Energy,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyEnergyVIF)
- .set(StorageNr(0))
- .set(TariffNr(1))
- .set(SubUnitNr(2))
- );
-
- addNumericFieldWithExtractor(
- "reactive_positive_3phase",
- "Q+ the 3-phase reactive positive power.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Power,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyPowerVIF)
- .set(StorageNr(0))
- .set(TariffNr(1))
- .set(SubUnitNr(2))
- );
-
- addNumericFieldWithExtractor(
- "total_active_partial_3phase",
- "Part Et+ the total 3-phase active partial energy.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Energy,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyEnergyVIF)
- .set(StorageNr(0))
- .set(TariffNr(2))
- .set(SubUnitNr(1))
- );
-
- addNumericFieldWithExtractor(
- "active_negative_3phase",
- "P- the 3-phase active negative power.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Power,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyPowerVIF)
- .set(StorageNr(0))
- .set(TariffNr(2))
- .set(SubUnitNr(1))
- );
-
- addNumericFieldWithExtractor(
- "total_reactive_partial_3phase",
- "Part Er+ the total 3-phase reactive partial energy.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Energy,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyEnergyVIF)
- .set(StorageNr(0))
- .set(TariffNr(2))
- .set(SubUnitNr(2))
- );
-
- addNumericFieldWithExtractor(
- "reactive_negative_3phase",
- "Q- the 3-phase reactive negative power.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Power,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyPowerVIF)
- .set(StorageNr(0))
- .set(TariffNr(2))
- .set(SubUnitNr(2))
- );
-
- addNumericFieldWithExtractor(
- "power",
- "PF the power factor.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Dimensionless,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::Dimensionless),
- Unit::FACTOR
- );
-
- addStringFieldWithExtractorAndLookup(
- "status",
- "Status. OK if no error flags are set.",
- PrintProperty::STATUS | PrintProperty::INCLUDE_TPL_STATUS,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::ErrorFlags),
- Translate::Lookup()
- .add(Translate::Rule("ERROR_FLAGS", Translate::MapType::BitToString)
- .set(MaskBits(0xff))
- .set(DefaultMessage("OK"))
- ));
-
- // Telegram 2 /////////////////////////////////////////////////////////////
-
- addNumericFieldWithExtractor(
- "current_at_phase_1",
- "I1 Amperage for L1 phase.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Amperage,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::Amperage)
- .add(VIFCombinable::Mfct01)
- );
-
- addNumericFieldWithExtractor(
- "current_at_phase_2",
- "I2 Amperage for L2 phase.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Amperage,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::Amperage)
- .add(VIFCombinable::Mfct02)
- );
-
- addNumericFieldWithExtractor(
- "current_at_phase_3",
- "I3 Amperage for L3 phase.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Amperage,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::Amperage)
- .add(VIFCombinable::Mfct03)
- );
-
- addNumericFieldWithExtractor(
- "voltage_at_phase_1",
- "L1-N Voltage for L1 phase.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Voltage,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::Voltage)
- .add(VIFCombinable::Mfct01)
- );
-
- addNumericFieldWithExtractor(
- "voltage_at_phase_2",
- "L2-N Voltagefor L2 phase.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Voltage,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::Voltage)
- .add(VIFCombinable::Mfct02)
- );
-
- addNumericFieldWithExtractor(
- "voltage_at_phase_3",
- "L3-N Voltage for L3 phase.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Voltage,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::Voltage)
- .add(VIFCombinable::Mfct03)
- );
-
- // Telegram 3 /////////////////////////////////////////////////////////////
-
- addNumericFieldWithExtractor(
- "active_power_at_phase_1",
- "P1 Power for L1 phase.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Power,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyPowerVIF)
- .set(SubUnitNr(1))
- .add(VIFCombinable::Mfct01)
- );
-
- addNumericFieldWithExtractor(
- "active_power_at_phase_2",
- "P2 Power for L2 phase.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Power,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyPowerVIF)
- .set(SubUnitNr(1))
- .add(VIFCombinable::Mfct02)
- );
-
- addNumericFieldWithExtractor(
- "active_power_at_phase_3",
- "P3 Power for L3 phase.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Power,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyPowerVIF)
- .set(SubUnitNr(1))
- .add(VIFCombinable::Mfct03)
- );
-
- addNumericFieldWithExtractor(
- "reactive_power_at_phase_1",
- "Q1 Power for L1 phase.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Power,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyPowerVIF)
- .set(SubUnitNr(2))
- .add(VIFCombinable::Mfct01)
- );
-
- addNumericFieldWithExtractor(
- "reactive_power_at_phase_2",
- "Q2 Power for L2 phase.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Power,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyPowerVIF)
- .set(SubUnitNr(2))
- .add(VIFCombinable::Mfct02)
- );
-
- addNumericFieldWithExtractor(
- "reactive_power_at_phase_3",
- "Q3 Power for L3 phase.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Power,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyPowerVIF)
- .set(SubUnitNr(2))
- .add(VIFCombinable::Mfct03)
- );
-
- addNumericFieldWithExtractor(
- "at_phase_1_power",
- "PF1 the power factor for L1 phase.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Dimensionless,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::Dimensionless)
- .add(VIFCombinable::Mfct01),
- Unit::FACTOR
- );
-
- addNumericFieldWithExtractor(
- "at_phase_2_power",
- "PF2 the power factor for L2 phase.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Dimensionless,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::Dimensionless)
- .add(VIFCombinable::Mfct02),
- Unit::FACTOR
- );
-
- addNumericFieldWithExtractor(
- "at_phase_3_power",
- "PF3 the power factor for L3 phase.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Dimensionless,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::Dimensionless)
- .add(VIFCombinable::Mfct03),
- Unit::FACTOR
- );
-
- addNumericFieldWithExtractor(
- "voltage_l1_l2",
- "L1-L2 Voltage between phases.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Voltage,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::Voltage)
- .add(VIFCombinable::Mfct04)
- );
-
- addNumericFieldWithExtractor(
- "voltage_l2_l3",
- "L2-L3 Voltage between phases.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Voltage,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::Voltage)
- .add(VIFCombinable::Mfct05)
- );
-
- addNumericFieldWithExtractor(
- "voltage_l3_l1",
- "L3-L1 Voltage between phases.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Voltage,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::Voltage)
- .add(VIFCombinable::Mfct06)
- );
-
- addNumericFieldWithExtractor(
- "current_in_neutral",
- "I Neutral amperage.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Amperage,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::Amperage)
- .add(VIFCombinable::Mfct04)
- );
-
-
- addNumericFieldWithExtractor(
- "raw_frequency",
- "Frequency in 0.1 Hz",
- DEFAULT_PRINT_PROPERTIES | PrintProperty::HIDE,
- Quantity::Frequency,
- VifScaling::None, DifSignedness::Signed,
- FieldMatcher::build()
- .set(DifVifKey("05FF5A"))
- );
-
- addNumericFieldWithCalculator(
- "frequency",
- "Frequency of AC.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Frequency,
- "raw_frequency_hz / 10 counter");
-
-
- }
-}
-
-// Test: Elen nemo 00067609 NOKEY
-// comment: Telegram 1
-// telegram=|6864646808657209760600A5251D02000000008E500400355873060085502B0013BF478E9040040029452501008590402B00F800468E600400000000000085602B000000008EA0400400890000000085A0402B0000000005FD3ADCF97E3F01FD17001F00000000009116|
-// {"_":"telegram","media":"electricity", "meter":"nemo", "name":"Elen", "id":"00067609", "total_active_positive_3phase_kwh":6735835, "active_positive_3phase_kw":97.83, "total_reactive_positive_3phase_kwh":1254529, "reactive_positive_3phase_kw":8.254, "total_active_partial_3phase_kwh":0, "active_negative_3phase_kw":0, "total_reactive_partial_3phase_kwh":89, "reactive_negative_3phase_kw":0, "power_factor":-0.996, "status":"OK", "frequency_hz":null, "timestamp":"1111-11-11T11:11:11Z"}
-// |Elen;00067609;OK;6735835;97.83;1111-11-11 11:11.11
-
-// comment: Telegram 2
-// telegram=|684B4B6808657209760600A5251D020100000005FDD9FF0100F4174805FDD9FF0200B8084805FDD9FF0300B0014805FDC8FF0100C0104505FDC8FF020040114505FDC8FF03005011451F00000000000716|
-// {"_":"telegram","media":"electricity", "meter":"nemo", "name":"Elen", "id":"00067609", "total_active_positive_3phase_kwh":6735835, "active_positive_3phase_kw":97.83, "total_reactive_positive_3phase_kwh":1254529, "reactive_positive_3phase_kw":8.254, "total_active_partial_3phase_kwh":0, "active_negative_3phase_kw":0, "total_reactive_partial_3phase_kwh":89, "reactive_negative_3phase_kw":0, "power_factor":-0.996, "status":"OK", "current_at_phase_1_a":155.6, "current_at_phase_2_a":140, "current_at_phase_3_a":132.8, "voltage_at_phase_1_v":231.6, "voltage_at_phase_2_v":232.4, "voltage_at_phase_3_v":232.5, "frequency_hz":null, "timestamp":"1111-11-11T11:11:11Z"}
-// |Elen;00067609;OK;6735835;97.83;1111-11-11 11:11.11
-
-// comment: Telegram 3
-// telegram=|689E9E6808657209760600A5251D02020000008540ABFF0100360B478540ABFF02002CFA468540ABFF030074ED46858040ABFF0100C0E244858040ABFF0200405A45858040ABFF030060364505FDBAFF0178BE7F3F05FDBAFF0240357E3F05FDBAFF0353B87E3F05FDC8FF0400907A4505FDC8FF0500707B4505FDC8FF0600807B4505FDD9FF0400502A4705FF5A0000FA4302FD3AC80002FD3A0A000F00000000008B16|
-// {"_":"telegram","media":"electricity", "meter":"nemo", "name":"Elen", "id":"00067609", "total_active_positive_3phase_kwh":6735835, "active_positive_3phase_kw":97.83, "total_reactive_positive_3phase_kwh":1254529, "reactive_positive_3phase_kw":8.254, "total_active_partial_3phase_kwh":0, "active_negative_3phase_kw":0, "total_reactive_partial_3phase_kwh":89, "reactive_negative_3phase_kw":0, "power_factor":-200, "status":"OK", "current_at_phase_1_a":155.6, "current_at_phase_2_a":140, "current_at_phase_3_a":132.8, "voltage_at_phase_1_v":231.6, "voltage_at_phase_2_v":232.4, "voltage_at_phase_3_v":232.5, "active_power_at_phase_1_kw":35.638, "active_power_at_phase_2_kw":32.022, "active_power_at_phase_3_kw":30.394, "reactive_power_at_phase_1_kw":1.814, "reactive_power_at_phase_2_kw":3.492, "reactive_power_at_phase_3_kw":2.918, "at_phase_1_power_factor":-0.999, "at_phase_2_power_factor":-0.993, "at_phase_3_power_factor":-0.995, "voltage_l1_l2_v":400.9, "voltage_l2_l3_v":402.3, "voltage_l3_l1_v":402.4, "current_in_neutral_a":43.6, "frequency_hz":50, "timestamp":"1111-11-11T11:11:11Z"}
-// |Elen;00067609;OK;6735835;97.83;1111-11-11 11:11.11
diff --git a/components/wmbus_common/driver_omnipower.cpp b/components/wmbus_common/driver_omnipower.cpp
deleted file mode 100644
index ea169eb8..00000000
--- a/components/wmbus_common/driver_omnipower.cpp
+++ /dev/null
@@ -1,116 +0,0 @@
-/*
- Copyright (C) 2018-2022 Fredrik Öhrström (gpl-3.0-or-later)
-
- 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 .
-*/
-
-#include"meters_common_implementation.h"
-
-/*
-Implemented October 2020 Janus Bo Andersen:
-Implements Kamstrup OmniPower, energy meter.
-This C1 WM-Bus meter broadcasts:
-- Accumulated energy consumption (A+, kWh)
-- Accumulated energy production (A-, kWh)
-- Current power consumption (P+, kW)
-- Current power production (P-, kW)
-
-According to Kamstrup doc. 58101496_C1_GB_05.2018
-(Wireless M-Bus Module for OMNIPOWER), the single-phase,
-three-phase and CT meters send the same datagram.
-
-Meter version. Implementation tested against meter:
-Kamstrup one-phase with firmware version 0x30.
-
-Meter uses AES-128 in CTR mode, which is the only mode supported by
-the extended link layer (wm-bus), see EN 13757-4:2019.
-
-*/
-namespace
-{
- struct Driver : public virtual MeterCommonImplementation
- {
- Driver(MeterInfo &mi, DriverInfo &di);
- };
-
- static bool ok = staticRegisterDriver([](DriverInfo&di)
- {
- di.setName("omnipower");
- di.setDefaultFields("name,id,total_energy_consumption_kwh,total_energy_production_kwh,"
- "current_power_consumption_kw,current_power_production_kw,timestamp");
- di.setMeterType(MeterType::ElectricityMeter);
- di.addLinkMode(LinkMode::C1);
-
- di.addMVT(MANUFACTURER_KAM, 0x02, 0x30);
-
- di.setConstructor([](MeterInfo& mi, DriverInfo& di){ return shared_ptr(new Driver(mi, di)); });
- });
-
- Driver::Driver(MeterInfo &mi, DriverInfo &di) : MeterCommonImplementation(mi, di)
- {
- addNumericFieldWithExtractor(
- "total_energy_consumption",
- "The total energy consumption recorded by this meter.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Energy,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyEnergyVIF)
- );
-
- addNumericFieldWithExtractor(
- "total_energy_production",
- "The total energy backward (production) recorded by this meter.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Energy,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyEnergyVIF)
- .add(VIFCombinable::BackwardFlow)
- );
-
- addNumericFieldWithExtractor(
- "current_power_consumption",
- "The current power consumption.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Power,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyPowerVIF)
- );
-
- addNumericFieldWithExtractor(
- "current_power_production",
- "The current power production.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Power,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyPowerVIF)
- .add(VIFCombinable::BackwardFlow)
- );
- }
-}
-// Test: myomnipower omnipower 32666857 NOKEY
-// Comment:
-// telegram=|2D442D2C5768663230028D20E4E2C81C20878C78_04041A03000004843C00000000042B0300000004AB3C00000000|
-// {"_":"telegram","media":"electricity","meter":"omnipower","name":"myomnipower","id":"32666857","total_energy_consumption_kwh":7.94,"total_energy_production_kwh":0,"current_power_consumption_kw":0.003,"current_power_production_kw":0,"timestamp":"1111-11-11T11:11:11Z"}
-// |myomnipower;32666857;7.94;0;0.003;0;1111-11-11 11:11.11
-// telegram=|27442D2C5768663230028D20E900C91C2011BA79138CCCFB_1A030000000000000300000000000000|
-// {"_":"telegram","media":"electricity","meter":"omnipower","name":"myomnipower","id":"32666857","total_energy_consumption_kwh":7.94,"total_energy_production_kwh":0,"current_power_consumption_kw":0.003,"current_power_production_kw":0,"timestamp":"1111-11-11T11:11:11Z"}
-// |myomnipower;32666857;7.94;0;0.003;0;1111-11-11 11:11.11
diff --git a/components/wmbus_common/driver_piigth.cpp b/components/wmbus_common/driver_piigth.cpp
deleted file mode 100644
index 5d59c7f0..00000000
--- a/components/wmbus_common/driver_piigth.cpp
+++ /dev/null
@@ -1,126 +0,0 @@
-/*
- Copyright (C) 2021-2022 Fredrik Öhrström (gpl-3.0-or-later)
-
- 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 .
-*/
-
-#include"meters_common_implementation.h"
-
-namespace
-{
- struct Driver : public virtual MeterCommonImplementation
- {
- Driver(MeterInfo &mi, DriverInfo &di);
- };
-
- static bool ok = staticRegisterDriver([](DriverInfo&di)
- {
- di.setName("piigth");
- di.setDefaultFields("name,id,status,temperature_c,relative_humidity_rh,timestamp");
- di.setMeterType(MeterType::TempHygroMeter);
- di.addLinkMode(LinkMode::MBUS);
- di.setConstructor([](MeterInfo& mi, DriverInfo& di){ return shared_ptr(new Driver(mi, di)); });
- di.addMVT(MANUFACTURER_PII, 0x1b, 0x01);
- });
-
- Driver::Driver(MeterInfo &mi, DriverInfo &di) : MeterCommonImplementation(mi, di)
- {
- addOptionalLibraryFields("fabrication_no,software_version");
-
- addStringField(
- "status",
- "Meter status from tpl status field.",
- PrintProperty::STATUS | PrintProperty::INCLUDE_TPL_STATUS);
-
- addNumericFieldWithExtractor(
- "temperature",
- "The current temperature.",
- PrintProperty::REQUIRED,
- Quantity::Temperature,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::ExternalTemperature)
- );
-
- addNumericFieldWithExtractor(
- "average_temperature_1h",
- "The average temperature over the last hour.",
- PrintProperty::REQUIRED,
- Quantity::Temperature,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::ExternalTemperature)
- .set(StorageNr(1))
- );
-
- addNumericFieldWithExtractor(
- "average_temperature_24h",
- "The average temperature over the last 24 hours.",
- PrintProperty::REQUIRED,
- Quantity::Temperature,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::ExternalTemperature)
- .set(StorageNr(2))
- );
-
- addNumericFieldWithExtractor(
- "relative_humidity",
- "The current relative humidity.",
- PrintProperty::REQUIRED,
- Quantity::RelativeHumidity,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::RelativeHumidity)
- );
-
- addNumericFieldWithExtractor(
- "relative_humidity_1h",
- "The average relative humidity over the last hour.",
- PrintProperty::REQUIRED,
- Quantity::RelativeHumidity,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::RelativeHumidity)
- .set(StorageNr(1))
- );
-
- addNumericFieldWithExtractor(
- "relative_humidity_24h",
- "The average relative humidity over the last 24 hours.",
- PrintProperty::REQUIRED,
- Quantity::RelativeHumidity,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::RelativeHumidity)
- .set(StorageNr(2))
- );
- }
-}
-
-
-// Test: Tempo piigth 10000284 NOKEY
-// telegram=|68383868080072840200102941011B04000000_0265C0094265A509B20165000002FB1A900142FB1A6901B201FB1A00000C788402001002FD0F21000FC016|
-// {"_":"telegram","media":"room sensor","meter":"piigth","name":"Tempo","id":"10000284","fabrication_no":"10000284","software_version":"0021","status":"OK","temperature_c":24.96,"average_temperature_1h_c":24.69,"relative_humidity_rh":40,"relative_humidity_1h_rh":36.1,"timestamp":"1111-11-11T11:11:11Z"}
-// |Tempo;10000284;OK;24.96;40;1111-11-11 11:11.11
-
-// telegram=|68383868080072840200102941011B06000000_02653F0A4265000A820165CA0902FB1A4F0142FB1A53018201FB1A5E010C788402001002FD0F21000F1916|
-// {"_":"telegram","media":"room sensor","meter":"piigth","name":"Tempo","id":"10000284","fabrication_no":"10000284","software_version":"0021","status":"OK","temperature_c":26.23,"average_temperature_1h_c":25.6,"average_temperature_24h_c":25.06,"relative_humidity_rh":33.5,"relative_humidity_1h_rh":33.9,"relative_humidity_24h_rh":35,"timestamp":"1111-11-11T11:11:11Z"}
-// |Tempo;10000284;OK;26.23;33.5;1111-11-11 11:11.11
diff --git a/components/wmbus_common/driver_pollucomf.cpp b/components/wmbus_common/driver_pollucomf.cpp
deleted file mode 100644
index 9f7b1212..00000000
--- a/components/wmbus_common/driver_pollucomf.cpp
+++ /dev/null
@@ -1,190 +0,0 @@
-/*
- Copyright (C) 2022 Fredrik Öhrström (gpl-3.0-or-later)
-
- 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 .
-*/
-
-#include"meters_common_implementation.h"
-
-namespace
-{
- struct Driver : public virtual MeterCommonImplementation {
- Driver(MeterInfo &mi, DriverInfo &di);
- };
-
- static bool ok = staticRegisterDriver([](DriverInfo&di)
- {
- di.setName("pollucomf");
- di.setDefaultFields("name,id,status,total_kwh,total_m3,target_kwh,target_m3,timestamp");
- di.setMeterType(MeterType::HeatMeter);
- di.addLinkMode(LinkMode::T1); // default
- di.addLinkMode(LinkMode::C1);
- di.addLinkMode(LinkMode::MBUS); // optional hw module
- di.addMVT(MANUFACTURER_SEN, 0x04, 0x1d);
- di.setConstructor([](MeterInfo& mi, DriverInfo& di){ return shared_ptr(new Driver(mi, di)); });
- });
-
- Driver::Driver(MeterInfo &mi, DriverInfo &di) : MeterCommonImplementation(mi, di)
- {
- addStringField(
- "status",
- "Meter status.",
- DEFAULT_PRINT_PROPERTIES |
- PrintProperty::STATUS | PrintProperty::INCLUDE_TPL_STATUS);
-
- addNumericFieldWithExtractor(
- "total",
- "The total energy consumption recorded by this meter.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Energy,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyEnergyVIF)
- );
-
- addNumericFieldWithExtractor(
- "total",
- "The total amount of water that has passed through this meter.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Volume,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::Volume)
- );
-
- addNumericFieldWithExtractor(
- "power",
- "The active power consumption.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Power,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyPowerVIF)
- );
-
- addNumericFieldWithExtractor(
- "flow",
- "The flow of water.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Flow,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::VolumeFlow)
- );
-
- addNumericFieldWithExtractor(
- "forward",
- "The forward temperature of the water.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Temperature,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::FlowTemperature)
- );
-
- addNumericFieldWithExtractor(
- "return",
- "The return temperature of the water.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Temperature,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::ReturnTemperature)
- );
-
- addStringFieldWithExtractor(
- "target_date",
- "The most recent billing period date.",
- DEFAULT_PRINT_PROPERTIES,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::Date)
- .set(StorageNr(1))
- );
-
- addNumericFieldWithExtractor(
- "target",
- "The energy consumption recorded by this meter at the set date.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Energy,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyEnergyVIF)
- .set(StorageNr(1))
- );
-
- addNumericFieldWithExtractor(
- "target",
- "The amount of water that had passed through this meter at the set date.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Volume,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::Volume)
- .set(StorageNr(1))
- );
-
- addNumericFieldWithExtractor(
- "forward_max",
- "The maximum forward temperature of the water.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Temperature,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Maximum)
- .set(VIFRange::FlowTemperature)
- .set(StorageNr(1))
- );
-
- addNumericFieldWithExtractor(
- "return_max",
- "The maximum return temperature of the water.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Temperature,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Maximum)
- .set(VIFRange::ReturnTemperature)
- .set(StorageNr(1))
- );
-
- addNumericFieldWithExtractor(
- "flow_max",
- "The maximum forward flow of water through this meter.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Flow,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Maximum)
- .set(VIFRange::VolumeFlow)
- .set(StorageNr(1))
- );
-
- addOptionalLibraryFields("operating_time_h,on_time_h,on_time_at_error_h,meter_datetime");
- }
-}
-
-// Test: Heat pollucomf 14175439 NOKEY
-// Comment:
-// telegram=|5e44ae4c395417141d047a9f0050252f2f046d2d32d92c0223B701040600000000041331000000032B000000033B000000025a3201025ef2003222000002fd170000426cBf2c440600000000441301000000525a0000525e0000533B000000|
-// {"_":"telegram","media":"heat","meter":"pollucomf","name":"Heat","id":"14175439","status":"OK","total_kwh":0,"total_m3":0.049,"power_kw":0,"flow_m3h":0,"forward_c":30.6,"return_c":24.2,"target_date":"2021-12-31","target_kwh":0,"target_m3":0.001,"forward_max_c":0,"return_max_c":0,"flow_max_m3h":0,"on_time_h":10536,"on_time_at_error_h":0,"meter_datetime":"2022-12-25 18:45","timestamp":"1111-11-11T11:11:11Z"}
-// |Heat;14175439;OK;0;0.049;0;0.001;1111-11-11 11:11.11
diff --git a/components/wmbus_common/driver_q400.cpp b/components/wmbus_common/driver_q400.cpp
deleted file mode 100644
index 25d98008..00000000
--- a/components/wmbus_common/driver_q400.cpp
+++ /dev/null
@@ -1,128 +0,0 @@
-/*
- Copyright (C) 2020-2022 Fredrik Öhrström (gpl-3.0-or-later)
-
- 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 .
-*/
-
-#include"meters_common_implementation.h"
-
-namespace
-{
- struct Driver : public virtual MeterCommonImplementation
- {
- Driver(MeterInfo &mi, DriverInfo &di);
- };
-
- static bool ok = staticRegisterDriver([](DriverInfo&di)
- {
- di.setName("q400");
- di.setDefaultFields("name,id,total_m3,timestamp");
- di.setMeterType(MeterType::WaterMeter);
- di.addLinkMode(LinkMode::T1);
- di.addMVT(MANUFACTURER_AXI, 0x07, 0x01);
- di.addMVT(MANUFACTURER_AXI, 0x07, 0x10);
- di.setConstructor([](MeterInfo& mi, DriverInfo& di){ return shared_ptr(new Driver(mi, di)); });
- });
-
- Driver::Driver(MeterInfo &mi, DriverInfo &di) : MeterCommonImplementation(mi, di)
- {
- addOptionalLibraryFields("meter_datetime,on_time_h");
- addOptionalLibraryFields("total_m3,total_forward_m3,total_backward_m3,flow_temperature_c,volume_flow_m3h");
-
- addStringField(
- "status",
- "Status and error flags.",
- DEFAULT_PRINT_PROPERTIES | PrintProperty::STATUS | PrintProperty::INCLUDE_TPL_STATUS);
-
- addStringFieldWithExtractor(
- "set_datetime",
- "Date and time when the previous billing period ended.",
- DEFAULT_PRINT_PROPERTIES,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::DateTime)
- .set(StorageNr(1))
- );
-
- addNumericFieldWithExtractor(
- "consumption_at_set_date",
- "The total water consumption at the end of the previous billing period.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Volume,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::Volume)
- .set(StorageNr(1))
- );
-
- addNumericFieldWithExtractor(
- "forward_at_set_date",
- "The total media volume flowing forward at the end of previous billing period.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Volume,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::Volume)
- .set(StorageNr(1))
- .add(VIFCombinable::ForwardFlow)
- );
-
- addNumericFieldWithExtractor(
- "backward_at_set_date",
- "The total media volume flowing backward at the end of the previous billing period.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Volume,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::Volume)
- .set(StorageNr(1))
- .add(VIFCombinable::BackwardFlow)
- );
-
- addNumericFieldWithExtractor(
- "battery",
- "Percentage of battery remaining.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Dimensionless,
- VifScaling::None, DifSignedness::Signed,
- FieldMatcher::build()
- .set(DifVifKey("01FD74")),
- Unit::PERCENTAGE
- );
- }
-}
-
-// Test: Q400Water q400 72727272 NOKEY
-// telegram=|2E4409077272727210077AD71020052F2F_046D040D742C041377000000446D0000612C4413000000002F2F2F2F2F2F|
-// {"_":"telegram","media":"water","meter":"q400","name":"Q400Water","id":"72727272","meter_datetime":"2019-12-20 13:04","total_m3":0.119,"status":"TEMPORARY_ERROR","set_datetime":"2019-12-01 00:00","consumption_at_set_date_m3":0,"timestamp":"1111-11-11T11:11:11Z"}
-// |Q400Water;72727272;0.119;1111-11-11 11:11.11
-
-// Test: AxiomaWater q400 72727273 NOKEY
-// Comment: Test Axioma W1 telegram with additional fields compared to the older q400 meter.
-// telegram=|5E4409077372727210077A710050052F2F_046D0110A92704130022000004933B0000000004933C00000000023B000002592A0A446D0000A12744130000000044933B0000000044933C0000000001FD74622F2F2F2F2F2F2F2F2F2F2F2F2F2F|
-// {"_":"telegram","media":"water","meter":"q400","name":"AxiomaWater","id":"72727273","meter_datetime":"2021-07-09 16:01","total_m3":8.704,"total_forward_m3":0,"total_backward_m3":0,"flow_temperature_c":26.02,"volume_flow_m3h":0,"status":"OK","set_datetime":"2021-07-01 00:00","consumption_at_set_date_m3":0,"forward_at_set_date_m3":0,"backward_at_set_date_m3":0,"battery_pct":98,"timestamp":"1111-11-11T11:11:11Z"}
-// |AxiomaWater;72727273;8.704;1111-11-11 11:11.11
-
-// Test: M q400 05829163 NOKEY
-// telegram=|544409076391820510077ABF100000046D2A0DC62C0420E80F430104130000000004933B0000000004933C00000000023B00000259F0D8446D0000C12C44130000000044933B0000000044933C0000000001FD7461|
-// {"_":"telegram","backward_at_set_date_m3": 0,"battery_pct": 97,"consumption_at_set_date_m3": 0,"flow_temperature_c": -100,"forward_at_set_date_m3": 0,"id": "05829163","media": "water","meter": "q400","meter_datetime": "2022-12-06 13:42","name": "M","on_time_h": 5881.166667,"set_datetime": "2022-12-01 00:00","status": "TEMPORARY_ERROR","timestamp": "1111-11-11T11:11:11Z","total_backward_m3": 0,"total_forward_m3": 0,"total_m3": 0,"volume_flow_m3h": 0}
-// |M;05829163;0;1111-11-11 11:11.11
-
-// Test: MM q400 06289748 NOKEY
-// telegram=|614409074897280601077AD1000000046D2C173B370420580193040413FB94000004933BFB94000004933C01000000023B02000259E306446D000021374413C08A000044933BC08A000044933C0100000034FD17000000000424BA01930401FD745B|
-// {"_": "telegram","backward_at_set_date_m3": 0.001,"battery_pct": 91,"consumption_at_set_date_m3": 35.52,"flow_temperature_c": 17.63,"forward_at_set_date_m3": 35.52,"id": "06289748","media": "water","meter": "q400","meter_datetime": "2025-07-27 23:44","name": "MM","on_time_h": 21317.5,"set_datetime": "2025-07-01 00:00","status": "OK","timestamp": "1111-11-11T11:11:11Z","total_backward_m3": 0.001,"total_forward_m3": 38.139,"total_m3": 38.139,"volume_flow_m3h": 0.002}
-// |MM;06289748;38.139;1111-11-11 11:11.11
diff --git a/components/wmbus_common/driver_qcaloric.cpp b/components/wmbus_common/driver_qcaloric.cpp
deleted file mode 100644
index 931057f9..00000000
--- a/components/wmbus_common/driver_qcaloric.cpp
+++ /dev/null
@@ -1,275 +0,0 @@
-/*
- Copyright (C) 2019-2023 Fredrik Öhrström (gpl-3.0-or-later)
-
- 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 .
-*/
-
-#include"meters_common_implementation.h"
-
-namespace
-{
- struct Driver : public virtual MeterCommonImplementation
- {
- Driver(MeterInfo &mi, DriverInfo &di);
- };
-
- static bool ok = staticRegisterDriver([](DriverInfo&di)
- {
- di.setName("qcaloric");
- di.addNameAlias("whe5x");
- di.addNameAlias("whe46x");
- di.setDefaultFields("name,id,current_consumption_hca,set_date,consumption_at_set_date_hca,timestamp");
- di.setMeterType(MeterType::HeatCostAllocationMeter);
- di.addLinkMode(LinkMode::C1);
- di.addLinkMode(LinkMode::T1);
- di.addLinkMode(LinkMode::S1);
- di.addMVT(MANUFACTURER_LSE, 0x08, 0x34);
- di.addMVT(MANUFACTURER_LSE, 0x08, 0x35);
- di.addMVT(MANUFACTURER_QDS, 0x08, 0x35);
- di.addMVT(MANUFACTURER_QDS, 0x08, 0x34);
- di.addMVT(MANUFACTURER_QDS, 0x08, 0x36);
- di.addMVT(MANUFACTURER_LSE, 0x08, 0x18); // whe4
- di.addMVT(MANUFACTURER_ZRI, 0x08, 0xfd);
-
- di.setConstructor([](MeterInfo& mi, DriverInfo& di){ return shared_ptr(new Driver(mi, di)); });
- });
-
- Driver::Driver(MeterInfo &mi, DriverInfo &di) : MeterCommonImplementation(mi, di)
- {
- addStringFieldWithExtractorAndLookup(
- "status",
- "Meter status from tpl status field.",
- DEFAULT_PRINT_PROPERTIES |
- PrintProperty::STATUS | PrintProperty::INCLUDE_TPL_STATUS,
- FieldMatcher::build()
- .set(DifVifKey("01FD73")),
- Translate::Lookup(
- {
- {
- {
- "ERROR_FLAGS",
- Translate::MapType::BitToString,
- AlwaysTrigger, MaskBits(0xff),
- "OK",
- {
- // Bits unknown
- }
- },
- },
- }));
-
- addNumericFieldWithExtractor(
- "current_consumption",
- "The current heat cost allocation.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::HCA,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::HeatCostAllocation)
- );
-
- addStringFieldWithExtractor(
- "set_date",
- "The most recent billing period date.",
- DEFAULT_PRINT_PROPERTIES,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::Date)
- .set(StorageNr(1))
- );
-
- addNumericFieldWithExtractor(
- "consumption_at_set_date",
- "Heat cost allocation at the most recent billing period date.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::HCA,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::HeatCostAllocation)
- .set(StorageNr(1))
- );
-
- addStringFieldWithExtractor(
- "set_date_1",
- "The most recent billing period date.",
- DEFAULT_PRINT_PROPERTIES,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::Date)
- .set(StorageNr(1))
- );
-
- addNumericFieldWithExtractor(
- "consumption_at_set_date_1",
- "Heat cost allocation at the most recent billing period date.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::HCA,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::HeatCostAllocation)
- .set(StorageNr(1))
- );
-
- addStringFieldWithExtractor(
- "set_date_8",
- "The 8 billing period date.",
- DEFAULT_PRINT_PROPERTIES,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::Date)
- .set(StorageNr(8))
- );
-
- addNumericFieldWithExtractor(
- "consumption_at_set_date_8",
- "Heat cost allocation at the 8 billing period date.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::HCA,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::HeatCostAllocation)
- .set(StorageNr(8))
- );
-
- addStringFieldWithExtractor(
- "set_date_17",
- "The 17 billing period date.",
- DEFAULT_PRINT_PROPERTIES,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::Date)
- .set(StorageNr(17))
- );
-
- addNumericFieldWithExtractor(
- "consumption_at_set_date_17",
- "Heat cost allocation at the 17 billing period date.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::HCA,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::HeatCostAllocation)
- .set(StorageNr(17))
- );
-
- addStringFieldWithExtractor(
- "error_date",
- "Date when the meter entered an error state.",
- DEFAULT_PRINT_PROPERTIES,
- FieldMatcher::build()
- .set(MeasurementType::AtError)
- .set(VIFRange::Date)
- );
-
- addStringFieldWithExtractor(
- "device_date_time",
- "Date and time when the meter sent the telegram.",
- DEFAULT_PRINT_PROPERTIES,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::DateTime)
- );
-
- addStringFieldWithExtractor(
- "model_version",
- "model version.",
- DEFAULT_PRINT_PROPERTIES,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::ModelVersion)
- );
-
- addNumericFieldWithExtractor(
- "flow_temperature",
- "Forward media temperature.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Temperature,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::FlowTemperature)
- );
-
- }
-}
-
-// Test: MyElement qcaloric 78563412 NOKEY
-// telegram=|314493441234567835087a740000200b6e2701004b6e450100426c5f2ccb086e790000c2086c7f21326cffff046d200b7422|
-// {"_":"telegram","media":"heat cost allocation","meter":"qcaloric","name":"MyElement","id":"78563412","status":"OK","current_consumption_hca":127,"set_date":"2018-12-31","consumption_at_set_date_hca":145,"set_date_1":"2018-12-31","consumption_at_set_date_1_hca":145,"set_date_17":"2019-01-31","consumption_at_set_date_17_hca":79,"error_date":"2127-15-31","device_date_time":"2019-02-20 11:32","timestamp":"1111-11-11T11:11:11Z"}
-// |MyElement;78563412;127;2018-12-31;145;1111-11-11 11:11.11
-
-// Test: MyElement2 qcaloric 90919293 NOKEY
-// Comment: Test mostly proprietary telegram without values
-// telegram=|49449344939291903408780DFF5F350082180000800007B06EFFFF970000009F2C70020000BE26970000000000010018002E001F002E0023FF210008000500020000002F046D220FA227|
-// {"_":"telegram","media":"heat cost allocation","meter":"qcaloric","name":"MyElement2","id":"90919293","status":"OK","device_date_time":"2021-07-02 15:34","timestamp":"1111-11-11T11:11:11Z"}
-// |MyElement2;90919293;null;null;null;1111-11-11 11:11.11
-
-// Test: zenner_heat qcaloric 25932395 NOKEY
-// telegram=|5E44496A95239325FD087A2CC050052F2F_0B6E030100426CDF2C4B6EFFFFFF82046CE1228B046E6200008D04EE132C3BFEFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2F2F2F2F|
-// {"_":"telegram","media":"heat cost allocation","meter":"qcaloric","name":"zenner_heat","id":"25932395","status":"UNKNOWN_C0","current_consumption_hca":103,"set_date":"2022-12-31","set_date_1":"2022-12-31","set_date_8":"2023-02-01","consumption_at_set_date_8_hca":62,"timestamp":"1111-11-11T11:11:11Z"}
-// |zenner_heat;25932395;103;2022-12-31;null;1111-11-11 11:11.11
-
-// Comment: Normal telegram that fills in values.
-// telegram=|314493449392919034087a520000200b6e9700004b6e700200426c9f2ccb086e970000c2086cbe26326cffff046d2d16a227|
-// {"_":"telegram","media":"heat cost allocation","meter":"qcaloric","name":"MyElement2","id":"90919293","status":"OK","current_consumption_hca":97,"set_date":"2020-12-31","consumption_at_set_date_hca":270,"set_date_1":"2020-12-31","consumption_at_set_date_1_hca":270,"set_date_17":"2021-06-30","consumption_at_set_date_17_hca":97,"error_date":"2127-15-31","device_date_time":"2021-07-02 22:45","timestamp":"1111-11-11T11:11:11Z"}
-// |MyElement2;90919293;97;2020-12-31;270;1111-11-11 11:11.11
-
-// Comment: Another mostly empty telegram, but values are now valid.
-// telegram=|49449344939291903408780DFF5F350082180000800007B06EFFFF970000009F2C70020000BE26970000000000010018002E001F002E0023FF210008000500020000002F046D220FA228|
-// {"_":"telegram","media":"heat cost allocation","meter":"qcaloric","name":"MyElement2","id":"90919293","status":"OK","current_consumption_hca":97,"set_date":"2020-12-31","consumption_at_set_date_hca":270,"set_date_1":"2020-12-31","consumption_at_set_date_1_hca":270,"set_date_17":"2021-06-30","consumption_at_set_date_17_hca":97,"error_date":"2127-15-31","device_date_time":"2021-08-02 15:34","timestamp":"1111-11-11T11:11:11Z"}
-// |MyElement2;90919293;97;2020-12-31;270;1111-11-11 11:11.11
-
-// Comment: Another version of the heat cost allocator. Was known as whe5x, so a name alias exist that maps to qcaloric.
-// Test: HCA whe5x 91835132 NOKEY
-// telegram=|244465323251839134087a4f0000000b6e0403004b6e660300426c9e29326cffff046d1416b921dd2f|
-// {"_":"telegram","media":"heat cost allocation","meter":"qcaloric","name":"HCA","id":"91835132","status":"OK","current_consumption_hca":304,"set_date":"2020-09-30","consumption_at_set_date_hca":366,"set_date_1":"2020-09-30","consumption_at_set_date_1_hca":366,"error_date":"2127-15-31","device_date_time":"2021-01-25 22:20","timestamp":"1111-11-11T11:11:11Z"}
-// |HCA;91835132;304;2020-09-30;366;1111-11-11 11:11.11
-
-// Comment: Another version of the heat cost allocator. Was known as whe46x, which now is a name alias mapped to qcaloric.
-// Test: HCA2 whe46x 60366655 NOKEY
-// telegram=|344465325566366018087A90040000046D1311962C01FD0C03326CFFFF01FD7300025AC2000DFF5F0C0008003030810613080BFFFC|
-// {"_":"telegram","media":"heat cost allocation","meter":"qcaloric","name":"HCA2","id":"60366655","status":"POWER_LOW","error_date":"2127-15-31","device_date_time":"2020-12-22 17:19","model_version":"03","flow_temperature_c":19.4,"timestamp":"1111-11-11T11:11:11Z"}
-// |HCA2;60366655;null;null;null;1111-11-11 11:11.11
-
-// telegram=|2a4465325566366018087ac3040000046d1617Ba210B6e890000426c9f2c4B6e520600326cffff01fd7300|
-// {"_":"telegram","media":"heat cost allocation","meter":"qcaloric","name":"HCA2","id":"60366655","status":"POWER_LOW","current_consumption_hca":89,"set_date":"2020-12-31","consumption_at_set_date_hca":652,"set_date_1":"2020-12-31","consumption_at_set_date_1_hca":652,"error_date":"2127-15-31","device_date_time":"2021-01-26 23:22","model_version":"03","flow_temperature_c":19.4,"timestamp":"1111-11-11T11:11:11Z"}
-// |HCA2;60366655;89;2020-12-31;652;1111-11-11 11:11.11
-
-// Test: HCA55 qcaloric 30535282 NOKEY
-// Comment: QCaloric 5.5 encrypted but with some not-encrypted bytes at the end. We should print these, but we do not right now....
-// Since it is encrypted and we do not have the key, wmbusmeters currently ignores it and every following telegram.
-// The readable bytes are: 326cffff046d230dda2c which decodes to
-// 32 dif (16 Bit Integer/Binary Value during error state)
-// 6C vif (Date type G)
-// FFFF
-// 04 dif (32 Bit Integer/Binary Instantaneous value)
-// 6D vif (Date and time type)
-// 230DDA2C
-// 32 dif (16 Bit Integer/Binary Value during error state)
-// 6C vif (Date type G)
-// FFFF ("error_date":"2127-15-31")
-// 04 dif (32 Bit Integer/Binary Instantaneous value)
-// 6D vif (Date and time type)
-// 230DDA2C ("device_date_time":"2022-12-26 13:35")
-// NOTYET telegram=|384493448252533036087A430020253F59515BD90F76E8576AF36C988EEA9B398EC5C205E5DBBE3F2698408947CB8E326CFFFF046D230DDA2C|
-
-// Comment: Mostly mfct specific data. Not yet decoded.
-// telegram=|49449344825253303608780DFF5F350082430035E3DFC4EAC97A58B8610713D93549E2601258D617D267E7515C764B002A88CD341A9F9DF3C6034DE5B6D1FAB3619CBA9F046D250DDA2C|
-// {"_":"telegram","device_date_time": "2022-12-26 13:37","id": "30535282","media": "heat cost allocation","meter": "qcaloric","name": "HCA55","status": "OK","timestamp": "1111-11-11T11:11:11Z"}
-// |HCA55;30535282;null;null;null;1111-11-11 11:11.11
diff --git a/components/wmbus_common/driver_qheat.cpp b/components/wmbus_common/driver_qheat.cpp
deleted file mode 100644
index 5e03212a..00000000
--- a/components/wmbus_common/driver_qheat.cpp
+++ /dev/null
@@ -1,239 +0,0 @@
-/*
- Copyright (C) 2021-2025 Fredrik Öhrström (gpl-3.0-or-later)
-
- 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 .
-*/
-
-#include"meters_common_implementation.h"
-#include"manufacturer_specificities.h"
-
-namespace
-{
- struct Driver : public virtual MeterCommonImplementation
- {
- Driver(MeterInfo &mi, DriverInfo &di);
-
- protected:
- void processContent(Telegram *t) override;
- };
-
- static bool ok = staticRegisterDriver([](DriverInfo&di)
- {
- di.setName("qheat");
- di.setDefaultFields("name,id,total_energy_consumption_kwh,last_month_date,last_month_energy_consumption_kwh,timestamp");
- di.setMeterType(MeterType::HeatMeter);
- di.addLinkMode(LinkMode::C1);
- di.addMVT(MANUFACTURER_QDS, 0x04, 0x23);
- di.addMVT(MANUFACTURER_QDS, 0x04, 0x46);
- di.addMVT(MANUFACTURER_QDS, 0x37, 0x23);
- di.addMVT(MANUFACTURER_QDS, 0x37, 0x47);
- di.usesProcessContent();
- di.setConstructor([](MeterInfo& mi, DriverInfo& di){ return shared_ptr(new Driver(mi, di)); });
- });
-
- Driver::Driver(MeterInfo &mi, DriverInfo &di) : MeterCommonImplementation(mi, di)
- {
- addStringFieldWithExtractorAndLookup(
- "status",
- "Meter status.",
- DEFAULT_PRINT_PROPERTIES | PrintProperty::STATUS | PrintProperty::INCLUDE_TPL_STATUS,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::ErrorFlags),
- {
- {
- {
- "ERROR_FLAGS",
- Translate::MapType::BitToString,
- AlwaysTrigger, MaskBits(0xffff),
- "OK",
- {
- // !!! Uncertain !!!
- // The documentation links to some intranet site, Qundis Error Codes Specification v1.7
- // at https://base/svn/sys/Meter/Gen/G5-5/PUBL/Gen55_SysSpec_Error-Codes_EN_v1.6_any.pdf
-
- // Here is a table that might apply:
- // https://www.manualslib.com/manual/2046543/Qundis-Q-Heat-5-5-Us.html?page=5
-
- { 0x01, "NO_FLOW" }, // F0
- { 0x02, "SUPPLY_SENSOR_INTERRUPTED" }, // F1
- { 0x04, "RETURN_SENSOR_INTERRUPTED" }, // F2
- { 0x08, "TEMPERATURE_ELECTRONICS_ERROR" }, // F3
- { 0x10, "BATTERY_VOLTAGE_ERROR" }, // F4
- { 0x20, "SHORT_CIRCUIT_SUPPLY_SENSOR" }, // F5
- { 0x40, "SHORT_CIRCUIT_RETURN_SENSOR" }, // F6
- { 0x80, "MEMORY_ERROR" }, // F7
- { 0x100, "SABOTAGE" }, // F8 - F1,2,3,5,6 longer than 8 hours, latching error, no more measurements performed.
- { 0x200, "ELECTRONICS_ERROR" }, // F9
- }
- },
- },
- });
-
- addNumericFieldWithExtractor(
- "total_energy_consumption",
- "The total energy consumption recorded by this meter.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Energy,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyEnergyVIF)
- );
-
- addStringFieldWithExtractor(
- "last_month_date",
- "Last day previous month when total energy consumption was recorded.",
- DEFAULT_PRINT_PROPERTIES,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(StorageNr(17))
- .set(VIFRange::Date)
- );
-
- addNumericFieldWithExtractor(
- "last_month_energy_consumption",
- "The total energy consumption recorded at the last day of the previous month.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Energy,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(StorageNr(17))
- .set(VIFRange::AnyEnergyVIF)
- );
-
- addStringFieldWithExtractor(
- "last_year_date",
- "Last day previous year when total energy consumption was recorded.",
- DEFAULT_PRINT_PROPERTIES,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(StorageNr(1))
- .set(VIFRange::Date)
- );
-
- addNumericFieldWithExtractor(
- "last_year_energy_consumption",
- "The total energy consumption recorded at the last day of the previous year.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Energy,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(StorageNr(1))
- .set(VIFRange::AnyEnergyVIF)
- );
-
- addStringFieldWithExtractor(
- "device_date_time",
- "Device date time.",
- DEFAULT_PRINT_PROPERTIES,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::DateTime)
- );
-
- addStringFieldWithExtractor(
- "device_error_date",
- "Device error date.",
- DEFAULT_PRINT_PROPERTIES,
- FieldMatcher::build()
- .set(MeasurementType::AtError)
- .set(VIFRange::Date)
- );
- }
-
- void Driver::processContent(Telegram *t) {
- auto it = t->dv_entries.find("0779");
- if (it != t->dv_entries.end()) {
- std::vector v;
- auto entry = it->second.second;
- hex2bin(entry.value.substr(0, 8), &v);
- // FIXME PROBLEM
- Address a;
- a.id = tostrprintf("%02x%02x%02x%02x", v[3], v[2], v[1], v[0]);
- t->addresses.push_back(a);
- std::string info = "*** " + entry.value.substr(0, 8) + " tpl-id (" + t->addresses.back().id + ")";
- t->addSpecialExplanation(entry.offset, 4, KindOfData::CONTENT, Understanding::FULL, info.c_str());
-
- v.clear();
- hex2bin(entry.value.substr(8, 4), &v);
- uint16_t tpl_mfct = *(uint16_t *) (&v[0]);
- info = "*** " + entry.value.substr(8, 4) + " tpl-mfct (" + manufacturerFlag(tpl_mfct) + ")";
- t->addSpecialExplanation(entry.offset + 4, 2, KindOfData::PROTOCOL, Understanding::FULL, info.c_str());
-
- v.clear();
- hex2bin(entry.value.substr(12, 2), &v);
- uint8_t tpl_version = v[0];
- info = "*** " + entry.value.substr(12, 2) + " tpl-version";
- t->addSpecialExplanation(entry.offset + 6, 1, KindOfData::PROTOCOL, Understanding::FULL, info.c_str());
-
- v.clear();
- hex2bin(entry.value.substr(14, 2), &v);
- uint8_t tpl_type = v[0];
- info = "*** " + entry.value.substr(14, 2) + " tpl-type (" + mediaType(v[0], tpl_mfct) + ")";
- t->addSpecialExplanation(entry.offset + 7, 1, KindOfData::PROTOCOL, Understanding::FULL, info.c_str());
-
- t->tpl_id_found = true;
- t->tpl_mfct = tpl_mfct;
- t->tpl_version = tpl_version;
- t->tpl_type = tpl_type;
- }
-
- it = t->dv_entries.find("0DFF5F");
- if (it != t->dv_entries.end()) {
- DVEntry entry = it->second.second;
- if (entry.value.length() == 53 * 2) {
- qdsExtractWalkByField(t, this, entry, 24, 8, "0C05", "total_energy_consumption", Quantity::Energy);
- qdsExtractWalkByField(t, this, entry, 32, 4, "426C", "last_year_date", Quantity::Text);
- qdsExtractWalkByField(t, this, entry, 36, 8, "4C05", "last_year_energy_consumption", Quantity::Energy);
- qdsExtractWalkByField(t, this, entry, 44, 4, "C2086C", "last_month_date", Quantity::Text);
- qdsExtractWalkByField(t, this, entry, 48, 8, "CC0805", "last_month_energy_consumption", Quantity::Energy);
- }
- }
- }
-}
-
-// Test: QHeato qheat 67228058 NOKEY
-// telegram=|3C449344957002372337725880226793442304DC0000200C05043900004C0500000000426C9F2CCC080551070000C2086CBE29326CFFFF046D280DB62A|
-// {"_":"telegram","media":"heat","meter":"qheat","name":"QHeato","id":"67228058","status":"OK","total_energy_consumption_kwh":390.4,"last_month_date":"2021-09-30","last_month_energy_consumption_kwh":75.1,"last_year_date":"2020-12-31","last_year_energy_consumption_kwh":0,"device_date_time":"2021-10-22 13:40","device_error_date":"2127-15-31","timestamp":"1111-11-11T11:11:11Z"}
-// |QHeato;67228058;390.4;2021-09-30;75.1;1111-11-11 11:11.11
-
-
-// Test: Qheatoo qheat 67506579 NOKEY
-// telegram=|41449344796550674637727965506793444604dc0000200c0d000000004c0d00000000426cffffcc080d00000000c2086cdf2802fd170000326cffff046d3a0ddb29|
-// {"_":"telegram","media":"heat","meter":"qheat","name":"Qheatoo","id":"67506579","status":"OK","total_energy_consumption_kwh":0,"last_month_date":"2022-08-31","last_month_energy_consumption_kwh":0,"last_year_date":"2127-15-31","last_year_energy_consumption_kwh":0,"device_date_time":"2022-09-27 13:58","device_error_date":"2127-15-31","timestamp":"1111-11-11T11:11:11Z"}
-// |Qheatoo;67506579;0;2022-08-31;0;1111-11-11 11:11.11
-
-// Test: Qheatoo qheat 78563412 NOKEY
-// telegram=|3C449344123456782337729876543293442304FE0000200C05682235004C0580253200426CDF2CCC080525153500C2086CFE24326CFFFF046D1811F225|
-// {"_":"telegram","media":"heat","meter":"qheat","name":"Qheatoo","id":"32547698","status":"OK","total_energy_consumption_kwh":35226.8,"last_month_date":"2023-04-30","last_month_energy_consumption_kwh":35152.5,"last_year_date":"2022-12-31","last_year_energy_consumption_kwh":32258,"device_date_time":"2023-05-18 17:24","device_error_date":"2127-15-31","timestamp":"1111-11-11T11:11:11Z"}
-// |Qheatoo;32547698;35226.8;2023-04-30;35152.5;1111-11-11 11:11.11
-
-// Test: Qheatoo qheat 78563411 NOKEY
-// Comment: Proprietary Q walk-by message, these telegrams currently can be matched only by the first id
-// telegram=|5344934411345678233778077998765431934423040dff5f350082fe00005f0107c005ffff68223500df2c80253200fe24251535005c03030000000000af03f508e91e1d2efc236e1fa218fe142f046d1911f225|
-// {"_":"telegram","media":"heat","meter":"qheat","name":"Qheatoo","id":"31547698","status":"OK","total_energy_consumption_kwh":35226.8,"last_month_date":"2023-04-30","last_month_energy_consumption_kwh":35152.5,"last_year_date":"2022-12-31","last_year_energy_consumption_kwh":32258,"device_date_time":"2023-05-18 17:25","timestamp":"1111-11-11T11:11:11Z"}
-// |Qheatoo;31547698;35226.8;2023-04-30;35152.5;1111-11-11 11:11.11
-
-// Test: QQ1 qheat 68204641 NOKEY
-// Comment:
-// telegram=|_58449344414620684737780779414620689344470C0DFF5F3500825A00000E0007C00DFFFF310803001F3C036800003E34310803000080008000800080008000800000931A92293128190C00002F02FD170000046D000F3235|
-// {"_": "telegram","device_date_time": "2025-05-18 15:00","id": "68204641","last_month_date": "2025-04-30","last_month_energy_consumption_kwh": 3083.1,"last_year_date": "2024-12-31","last_year_energy_consumption_kwh": 680.3,"media": "heat volume at inlet","meter": "qheat","name": "QQ1","status": "OK","timestamp": "1111-11-11T11:11:11Z","total_energy_consumption_kwh": 3083.1}
-// |QQ1;68204641;3083.1;2025-04-30;3083.1;1111-11-11 11:11.11
-
-// telegram=|_4144934441462068473772414620689344470C5B0000200C0D310803004C0D03680000426C1F3CCC080D31080300C2086C3E3402FD170000326CFFFF046D000F3235|
-// {"_": "telegram","device_date_time": "2025-05-18 15:00","device_error_date": "2127-15-31","id": "68204641","last_month_date": "2025-04-30","last_month_energy_consumption_kwh": 856.416667,"last_year_date": "2024-12-31","last_year_energy_consumption_kwh": 188.972222,"media": "heat volume at inlet","meter": "qheat","name": "QQ1","status": "OK","timestamp": "1111-11-11T11:11:11Z","total_energy_consumption_kwh": 856.416667}
-// |QQ1;68204641;856.416667;2025-04-30;856.416667;1111-11-11 11:11.11
diff --git a/components/wmbus_common/driver_qheat_55_us.cpp b/components/wmbus_common/driver_qheat_55_us.cpp
deleted file mode 100644
index 61a17b00..00000000
--- a/components/wmbus_common/driver_qheat_55_us.cpp
+++ /dev/null
@@ -1,166 +0,0 @@
-/*
- Copyright (C) 2022 Fredrik Öhrström (gpl-3.0-or-later)
-
- 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 .
-*/
-
-#include "meters_common_implementation.h"
-
-namespace
-{
- struct Driver : public virtual MeterCommonImplementation
- {
- Driver(MeterInfo &mi, DriverInfo &di);
-
- // Telegrams contain the following values:
-
- // device_date_time - current device date and time
- // total_energy_consumption_kwh - current reading
-
- // key_date - billing date, storagenr 1
- // key_date_kwh - billing date reading, storagenr 1
-
- // prev_month - date of the previous month in storagenr 2
- // prev_month_kwh[13] - 13 past month readings, storagenr 2 to 14
- };
-
- static bool ok = staticRegisterDriver([](DriverInfo &di)
- {
- di.setName("qheat_55_us");
- di.setDefaultFields("name,id,total_energy_consumption_kwh,key_date_kwh,timestamp");
- di.setMeterType(MeterType::HeatMeter);
-
- di.addLinkMode(LinkMode::C1);
- di.addMVT(MANUFACTURER_LUG, 0x04, 0x07);
- di.addMVT(MANUFACTURER_LUG, 0x04, 0x0a);
- di.setConstructor([](MeterInfo& mi, DriverInfo& di){ return shared_ptr(new Driver(mi, di)); });
- });
-
- Driver::Driver(MeterInfo &mi, DriverInfo &di) : MeterCommonImplementation(mi, di)
- {
- addStringFieldWithExtractor(
- "device_date_time",
- "Device date time.",
- DEFAULT_PRINT_PROPERTIES,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::DateTime)
- );
-
- addNumericFieldWithExtractor(
- "total_energy_consumption",
- "The total energy consumption recorded by this meter.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Energy,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyEnergyVIF)
- .set(StorageNr(0))
- .set(TariffNr(0))
- .set(IndexNr(1))
- );
-
- addStringFieldWithExtractor(
- "key_date",
- "The key (billing) date",
- DEFAULT_PRINT_PROPERTIES,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::DateTime)
- .set(StorageNr(1))
- .set(TariffNr(0))
- .set(IndexNr(1))
- );
-
- addNumericFieldWithExtractor(
- "key_date",
- "The total energy consumption recorded at key (billing) date",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Energy,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyEnergyVIF)
- .set(StorageNr(1))
- .set(TariffNr(0))
- .set(IndexNr(1))
- );
-
- addStringFieldWithExtractor(
- "prev_month",
- "The date of end of last month.",
- DEFAULT_PRINT_PROPERTIES,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::DateTime)
- .set(StorageNr(2))
- .set(TariffNr(0))
- .set(IndexNr(1))
- );
-
- for (int i = 1; i <= 13; ++i)
- {
- std::string key, info;
- strprintf(&key, "prev_%d_month", i);
- strprintf(&info, "Energy consumption %d months back.", i);
-
- addNumericFieldWithExtractor(
- key,
- info,
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Energy,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyEnergyVIF)
- .set(StorageNr(i + 1))
- .set(TariffNr(0))
- .set(IndexNr(1))
- );
- }
-
- addNumericFieldWithExtractor(
- "actuality_duration",
- "The time between the measurement and the sending of this telegram.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Time,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::ActualityDuration)
- );
-
- addNumericFieldWithExtractor(
- "time_without_measurement",
- "How long the meter has been in an error state and unable to measure values, while powered up.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Time,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::AtError)
- .set(VIFRange::OnTime)
- );
-
- }
-}
-// Test: Heat qheat_55_us 70835484 NOKEY
-// telegram=9644a732845483700a047ae70000200274fc00046d230Bd3250c0605920000446d3B17Bf2c4c068251000084016d3B17de248c010605900000cc0106988500008c020629770000cc0206226600008c030682510000cc0306933600008c040602260000cc0406691800008c050633140000cc0506900900008c060618020000cc0606000000008c0706000000003c22000000000f001000
-// {"_":"telegram","media":"heat","meter":"qheat_55_us","name":"Heat","id":"70835484","device_date_time":"2022-05-19 11:35","total_energy_consumption_kwh":9205,"key_date":"2021-12-31 23:59","key_date_kwh":5182,"prev_month":"2022-04-30 23:59","prev_1_month_kwh":9005,"prev_2_month_kwh":8598,"prev_3_month_kwh":7729,"prev_4_month_kwh":6622,"prev_5_month_kwh":5182,"prev_6_month_kwh":3693,"prev_7_month_kwh":2602,"prev_8_month_kwh":1869,"prev_9_month_kwh":1433,"prev_10_month_kwh":990,"prev_11_month_kwh":218,"prev_12_month_kwh":0,"prev_13_month_kwh":0,"actuality_duration_h":0.07,"time_without_measurement_h":0,"timestamp":"1111-11-11T11:11:11Z"}
-// |Heat;70835484;9205;5182;1111-11-11 11:11.11
-
-// Test: Heat qheat_55_us 70639638 NOKEY
-// telegram=9644a7323896637007047a5B00002002740400046d2a0fe2210c0600080600446d3B17df2c4c069106060084016d3B17df2c8c010691060600cc0106227005008c020667430500cc0206122505008c030612180500cc0306821305008c040636090500cc0406080505008c050660960400cc0506597304008c060600410400cc0606790504008c0706936303003c22000000000f001000
-// {"_":"telegram","media":"heat","meter":"qheat_55_us","name":"Heat","id":"70639638","device_date_time":"2023-01-02 15:42","total_energy_consumption_kwh":60800,"key_date":"2022-12-31 23:59","key_date_kwh":60691,"prev_month":"2022-12-31 23:59","prev_1_month_kwh":60691,"prev_2_month_kwh":57022,"prev_3_month_kwh":54367,"prev_4_month_kwh":52512,"prev_5_month_kwh":51812,"prev_6_month_kwh":51382,"prev_7_month_kwh":50936,"prev_8_month_kwh":50508,"prev_9_month_kwh":49660,"prev_10_month_kwh":47359,"prev_11_month_kwh":44100,"prev_12_month_kwh":40579,"prev_13_month_kwh":36393,"actuality_duration_h":0.001111,"time_without_measurement_h":0,"timestamp":"1111-11-11T11:11:11Z"}
-// |Heat;70639638;60800;60691;1111-11-11 11:11.11
\ No newline at end of file
diff --git a/components/wmbus_common/driver_qsmoke.cpp b/components/wmbus_common/driver_qsmoke.cpp
deleted file mode 100644
index c3e70811..00000000
--- a/components/wmbus_common/driver_qsmoke.cpp
+++ /dev/null
@@ -1,151 +0,0 @@
-/*
- Copyright (C) 2021 Fredrik Öhrström (gpl-3.0-or-later)
-
- 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 .
-*/
-
-#include"meters_common_implementation.h"
-
-namespace
-{
- struct Driver : public virtual MeterCommonImplementation
- {
- Driver(MeterInfo &mi, DriverInfo &di);
- };
-
- static bool ok = staticRegisterDriver([](DriverInfo&di)
- {
- di.setName("qsmoke");
- di.setDefaultFields("name,id,status,last_alarm_date,alarm_counter,timestamp");
- di.setMeterType(MeterType::SmokeDetector);
- di.addLinkMode(LinkMode::T1);
- di.addMVT(MANUFACTURER_QDS, 0x1a, 0x21);
- di.addMVT(MANUFACTURER_QDS, 0x1a, 0x23);
- di.setConstructor([](MeterInfo& mi, DriverInfo& di){ return shared_ptr(new Driver(mi, di)); });
- });
-
- Driver::Driver(MeterInfo &mi, DriverInfo &di) : MeterCommonImplementation(mi, di)
- {
- addStringFieldWithExtractorAndLookup(
- "status",
- "Meter error flags. IMPORTANT! Smoke alarm is probably NOT reported here! You MUST check last alarm date and counter!",
- DEFAULT_PRINT_PROPERTIES |
- PrintProperty::STATUS | PrintProperty::INCLUDE_TPL_STATUS,
- FieldMatcher::build()
- .set(VIFRange::ErrorFlags),
- Translate::Lookup(
- {
- {
- {
- "ERROR_FLAGS",
- Translate::MapType::BitToString,
- AlwaysTrigger, MaskBits(0xffff),
- "OK",
- {
- }
- },
- },
- }));
-
- addStringFieldWithExtractor(
- "last_alarm_date",
- "Date when the smoke alarm last triggered.",
- DEFAULT_PRINT_PROPERTIES,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(StorageNr(6))
- .set(VIFRange::Date)
- );
-
- addNumericFieldWithExtractor(
- "alarm",
- "Number of times the smoke alarm has triggered.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Dimensionless,
- VifScaling::None, DifSignedness::Unsigned,
- FieldMatcher::build()
- .set(DifVifKey("81037C034C4123"))
- );
-
- addStringFieldWithExtractor(
- "message_datetime",
- "Device date time.",
- DEFAULT_PRINT_PROPERTIES,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::DateTime)
- );
-
- addStringFieldWithExtractor(
- "test_button_last_date",
- "Date when test button was last pressed.",
- DEFAULT_PRINT_PROPERTIES,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(StorageNr(4))
- .set(VIFRange::Date)
- );
-
- addNumericFieldWithExtractor(
- "test_button",
- "Number of times the test button has been pressed.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Dimensionless,
- VifScaling::None, DifSignedness::Signed,
- FieldMatcher::build()
- .set(DifVifKey("81027C03495523"))
- );
-
- addNumericFieldWithExtractor(
- "transmission",
- "Transmission counter?",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Dimensionless,
- VifScaling::None, DifSignedness::Unsigned,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AccessNumber)
- );
-
- addStringFieldWithExtractor(
- "at_error_date",
- "Date when the device entered an error state.",
- DEFAULT_PRINT_PROPERTIES,
- FieldMatcher::build()
- .set(MeasurementType::AtError)
- .set(VIFRange::Date)
- );
-
- addNumericFieldWithExtractor(
- "some_sort_of_duration",
- "What does this mean?",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Time,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(DifVifKey("02FDAC7E"))
- );
-
- }
-}
-
-// Test: QSmokeo qsmoke 45797086 NOKEY
-// telegram=|3E44934486707945211A7801FD08F081027C034955230082026CFFFF81037C034C41230082036CFFFF03FD17000000326CFFFF046D0F0ABC2B02FDAC7E1100|
-// {"_":"telegram","media":"smoke detector","meter":"qsmoke","name":"QSmokeo","id":"45797086","status":"OK","last_alarm_date":"2127-15-31","alarm_counter":0,"message_datetime":"2021-11-28 10:15","test_button_last_date":"2127-15-31","test_button_counter":0,"transmission_counter":240,"at_error_date":"2127-15-31","some_sort_of_duration_h":0.004722,"timestamp":"1111-11-11T11:11:11Z"}
-// |QSmokeo;45797086;OK;2127-15-31;0;1111-11-11 11:11.11
-
-// Test: QSmokep qsmoke 48128850 NOKEY
-// telegram=|3744934450881248231A7A5C00002081027C034955230082026CFFFF81037C034C41230082036CFFFF02FD170000326CFFFF046D2514BC2B|
-// {"_":"telegram","media":"smoke detector","meter":"qsmoke","name":"QSmokep","id":"48128850","status":"OK","last_alarm_date":"2127-15-31","alarm_counter":0,"message_datetime":"2021-11-28 20:37","test_button_last_date":"2127-15-31","test_button_counter":0,"at_error_date":"2127-15-31","timestamp":"1111-11-11T11:11:11Z"}
-// |QSmokep;48128850;OK;2127-15-31;0;1111-11-11 11:11.11
diff --git a/components/wmbus_common/driver_qualcosonic.cpp b/components/wmbus_common/driver_qualcosonic.cpp
deleted file mode 100644
index 936ec7f9..00000000
--- a/components/wmbus_common/driver_qualcosonic.cpp
+++ /dev/null
@@ -1,153 +0,0 @@
-/*
- Copyright (C) 2022-2024 Fredrik Öhrström (gpl-3.0-or-later)
-
- 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 .
-*/
-
-#include"meters_common_implementation.h"
-
-namespace
-{
- struct Driver : public virtual MeterCommonImplementation
- {
- Driver(MeterInfo &mi, DriverInfo &di);
- };
-
- static bool ok = staticRegisterDriver([](DriverInfo&di)
- {
- di.setName("qualcosonic");
- di.setDefaultFields("name,id,status,total_heat_energy_kwh,total_cooling_energy_kwh,"
- "power_kw,target_datetime,target_heat_energy_kwh,target_cooling_energy_kwh,timestamp");
- di.setMeterType(MeterType::HeatCoolingMeter);
- di.addLinkMode(LinkMode::C1);
- di.addMVT(MANUFACTURER_AXI, 0x0d, 0x0b);
- di.addMVT(MANUFACTURER_AXI, 0x0d, 0x0c);
- di.setConstructor([](MeterInfo& mi, DriverInfo& di){ return shared_ptr(new Driver(mi, di)); });
- });
-
- Driver::Driver(MeterInfo &mi, DriverInfo &di) : MeterCommonImplementation(mi, di)
- {
- addOptionalLibraryFields("fabrication_no,operating_time_h,on_time_h,meter_datetime,meter_datetime_at_error");
- addOptionalLibraryFields("total_m3,flow_temperature_c,return_temperature_c,flow_return_temperature_difference_c,volume_flow_m3h");
-
- addStringFieldWithExtractorAndLookup(
- "status",
- "Meter status. Includes both meter error field and tpl status field.",
- DEFAULT_PRINT_PROPERTIES |
- PrintProperty::STATUS | PrintProperty::INCLUDE_TPL_STATUS,
- FieldMatcher::build()
- .set(VIFRange::ErrorFlags),
- Translate::Lookup(
- {
- {
- {
- "ERROR_FLAGS",
- Translate::MapType::BitToString,
- AlwaysTrigger, MaskBits(0xffffffff),
- "OK",
- {
- /* What do these bits mean? There are a lot of them...
- */
- }
- },
- },
- }));
-
- addNumericFieldWithExtractor(
- "total_heat_energy",
- "The total heating energy consumption recorded by this meter.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Energy,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyEnergyVIF)
- .add(VIFCombinable::ForwardFlow)
- );
-
- addNumericFieldWithExtractor(
- "total_cooling_energy",
- "The total cooling energy consumption recorded by this meter.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Energy,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyEnergyVIF)
- .add(VIFCombinable::BackwardFlow)
- );
-
- addNumericFieldWithExtractor(
- "power",
- "The current power consumption.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Power,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyPowerVIF)
- );
-
- addStringFieldWithExtractor(
- "target_datetime",
- "Date and time when the previous billing period ended.",
- DEFAULT_PRINT_PROPERTIES,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::DateTime)
- .set(StorageNr(16))
- );
-
- addNumericFieldWithExtractor(
- "target_heat_energy",
- "The heating energy consumption recorded at the end of the previous billing period.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Energy,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyEnergyVIF)
- .add(VIFCombinable::ForwardFlow)
- .set(StorageNr(16))
- );
-
- addNumericFieldWithExtractor(
- "target_cooling_energy",
- "The cooling energy consumption recorded at the end of the previous billing period.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Energy,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyEnergyVIF)
- .add(VIFCombinable::BackwardFlow)
- .set(StorageNr(16))
- );
- }
-}
-
-// Test: qualco qualcosonic 03016408 NOKEY
-// telegram=|76440907086401030B0d7a78000000046d030fB726346d0000010134fd170000000004200f13cf0104240f13cf0104863B0000000004863cdc0000000413B5150600042B86f1ffff043B030200000259c002025d2c05026194fd0c780864010384086d3B17Bf258408863B000000008408863c0B000000|
-// {"_":"telegram","media":"heat/cooling load","meter":"qualcosonic","name":"qualco","id":"03016408","fabrication_no":"03016408","operating_time_h":8430.013056,"on_time_h":8430.013056,"meter_datetime":"2021-06-23 15:03","meter_datetime_at_error":"2000-01-01 00:00","total_m3":398.773,"flow_temperature_c":7.04,"return_temperature_c":13.24,"flow_return_temperature_difference_c":-6.2,"volume_flow_m3h":0.515,"status":"OK","total_heat_energy_kwh":0,"total_cooling_energy_kwh":220,"power_kw":-3.706,"target_datetime":"2021-05-31 23:59","target_heat_energy_kwh":0,"target_cooling_energy_kwh":11,"timestamp":"1111-11-11T11:11:11Z"}
-// |qualco;03016408;OK;0;220;-3.706;2021-05-31 23:59;0;11;1111-11-11 11:11.11
-
-// Test: qualcoe4 qualcosonic 29481002 NOKEY
-// telegram=|76440907021048290C0D7A89000000_046D2301E122346D0000010134FD17000000000420097468020424DE736802048E3BF1190000048E3C00000000041323860100042B6E050000043B2500000002598016025D000A0261800C0C780210482984086D3B17FF2184088E3BED19000084088E3C00000000|
-// {"_":"telegram","media":"heat/cooling load","meter":"qualcosonic","name":"qualcoe4","id":"29481002","fabrication_no":"29481002","operating_time_h":11222.177222,"on_time_h":11222.189167,"meter_datetime":"2023-02-01 01:35","meter_datetime_at_error":"2000-01-01 00:00","total_m3":99.875,"flow_temperature_c":57.6,"return_temperature_c":25.6,"flow_return_temperature_difference_c":32,"volume_flow_m3h":0.037,"status":"OK","total_heat_energy_kwh":1844.722222,"total_cooling_energy_kwh":0,"power_kw":1.39,"target_datetime":"2023-01-31 23:59","target_heat_energy_kwh":1843.611111,"target_cooling_energy_kwh":0,"timestamp":"1111-11-11T11:11:11Z"}
-// |qualcoe4;29481002;OK;1844.722222;0;1.39;2023-01-31 23:59;1843.611111;0;1111-11-11 11:11.11
-
-// Test: qualcoe4long qualcosonic 98499485 NOKEY
-// telegram=|9A440907859449980C0D7A8C000000_046D3201E122346D0000010134FD17000000000420BB776802042490776802048E3BF2190000048E3C00000000041329860100042B6A010000043B0B0000000259F615025D1E0B0261D80A0C788594499884086D3B17FF21820859BC1382085D720B840824915D680284088E3BED19000084088E3C00000000840813028601008408BE5800000000027F00AD|
-// {"_":"telegram","media":"heat/cooling load","meter":"qualcosonic","name":"qualcoe4long","id":"98499485","fabrication_no":"98499485","operating_time_h":11222.44,"on_time_h":11222.451944,"meter_datetime":"2023-02-01 01:50","meter_datetime_at_error":"2000-01-01 00:00","total_m3":99.881,"flow_temperature_c":56.22,"return_temperature_c":28.46,"flow_return_temperature_difference_c":27.76,"volume_flow_m3h":0.011,"status":"OK","total_heat_energy_kwh":1845,"total_cooling_energy_kwh":0,"power_kw":0.362,"target_datetime":"2023-01-31 23:59","target_heat_energy_kwh":1843.611111,"target_cooling_energy_kwh":0,"timestamp":"1111-11-11T11:11:11Z"}
-// |qualcoe4long;98499485;OK;1845;0;0.362;2023-01-31 23:59;1843.611111;0;1111-11-11 11:11.11
diff --git a/components/wmbus_common/driver_qwater.cpp b/components/wmbus_common/driver_qwater.cpp
deleted file mode 100644
index 794afb11..00000000
--- a/components/wmbus_common/driver_qwater.cpp
+++ /dev/null
@@ -1,245 +0,0 @@
-/*
- Copyright (C) 2022-2025 Fredrik Öhrström (gpl-3.0-or-later)
-
- 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 .
-*/
-
-#include"meters_common_implementation.h"
-#include"manufacturer_specificities.h"
-
-namespace
-{
- struct Driver : public virtual MeterCommonImplementation
- {
- Driver(MeterInfo &mi, DriverInfo &di);
- protected:
- void processContent(Telegram *t) override;
- };
-
- static bool ok = staticRegisterDriver([](DriverInfo&di)
- {
- di.setName("qwater");
- di.setDefaultFields("name,id,total_m3,"
- "due_date_m3,"
- "due_date,"
- "status,"
- "timestamp");
- di.setMeterType(MeterType::WaterMeter);
- di.addLinkMode(LinkMode::S1);
- di.addMVT(MANUFACTURER_QDS, 0x37, 0x33);
- di.addMVT(MANUFACTURER_QDS, 0x37, 0x35);
- di.addMVT(MANUFACTURER_QDS, 0x06, 0x16);
- di.addMVT(MANUFACTURER_QDS, 0x07, 0x16);
- di.addMVT(MANUFACTURER_QDS, 0x06, 0x17);
- di.addMVT(MANUFACTURER_QDS, 0x07, 0x17);
- di.addMVT(MANUFACTURER_QDS, 0x06, 0x18);
- di.addMVT(MANUFACTURER_QDS, 0x07, 0x18);
- di.addMVT(MANUFACTURER_QDS, 0x07, 0x19);
- di.addMVT(MANUFACTURER_QDS, 0x06, 0x1a);
- di.addMVT(MANUFACTURER_QDS, 0x07, 0x1a);
- di.addMVT(MANUFACTURER_QDS, 0x06, 0x35);
- di.addMVT(MANUFACTURER_QDS, 0x07, 0x35);
- di.usesProcessContent();
-
- di.setConstructor([](MeterInfo& mi, DriverInfo& di){ return shared_ptr(new Driver(mi, di)); });
- });
-
- Driver::Driver(MeterInfo &mi, DriverInfo &di) :
- MeterCommonImplementation(mi, di)
- {
- addOptionalLibraryFields("meter_datetime");
- addOptionalLibraryFields("total_m3");
-
- addStringField(
- "status",
- "Meter status tpl status field.",
- DEFAULT_PRINT_PROPERTIES | PrintProperty::STATUS | PrintProperty::INCLUDE_TPL_STATUS);
-
- addNumericFieldWithExtractor(
- "due_date",
- "The water consumption at the due date.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Volume,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::Volume)
- .set(StorageNr(1))
- );
-
- addNumericFieldWithExtractor(
- "due",
- "The due date for billing date.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::PointInTime,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::Date)
- .set(StorageNr(1)),
- Unit::DateLT
- );
-
- addNumericFieldWithExtractor(
- "due_17_date",
- "The water consumption at the 17 due date.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Volume,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::Volume)
- .set(StorageNr(17))
- );
-
- addNumericFieldWithExtractor(
- "due_17",
- "The due date for billing date.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::PointInTime,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::Date)
- .set(StorageNr(17)),
- Unit::DateLT
- );
-
- addNumericFieldWithExtractor(
- "volume_flow",
- "Media volume flow when duration exceeds lower last.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Flow,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::VolumeFlow)
- .add(VIFCombinable::DurationExceedsLowerLast)
- );
-
- addNumericFieldWithExtractor(
- "error",
- "The date the error occurred at. If no error, reads 2127-15-31 (FFFF).",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::PointInTime,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::AtError)
- .set(VIFRange::Date),
- Unit::DateLT
- );
- }
-}
-
-void Driver::processContent(Telegram *t) {
- auto it = t->dv_entries.find("0DFF5F");
- if (it == t->dv_entries.end()) {
- return;
- }
- DVEntry entry = it->second.second;
- if (entry.value.length() != 53 * 2) {
- return;
- }
- qdsExtractWalkByField(t, this, entry, 24, 8, "0C13", "total", Quantity::Volume);
- qdsExtractWalkByField(t, this, entry, 32, 4, "426C", "due", Quantity::PointInTime);
- qdsExtractWalkByField(t, this, entry, 36, 8, "4C13", "due_date", Quantity::Volume);
- qdsExtractWalkByField(t, this, entry, 44, 4, "C2086C", "due_17", Quantity::PointInTime);
- qdsExtractWalkByField(t, this, entry, 48, 8, "CC0813", "due_17_date", Quantity::Volume);
-}
-
-// Test: MyQWater qwater 12353648 NOKEY
-// telegram=|374493444836351218067ac70000200c13911900004c1391170000426cbf2ccc081391170000c2086cbf2c02bb560000326cffff046d1e02de21fed0|
-// {"_":"telegram","media":"warm water","meter":"qwater","name":"MyQWater","id":"12353648","status":"OK","total_m3":1.991,"due_date_m3":1.791,"due_date":"2021-12-31","due_17_date_m3":1.791,"due_17_date":"2021-12-31","error_date":"2128-03-31","volume_flow_m3h":0,"meter_datetime":"2022-01-30 02:30","timestamp":"1111-11-11T11:11:11Z"}
-// |MyQWater;12353648;1.991;1.791;2021-12-31;OK;1111-11-11 11:11.11
-
-// And a second telegram that only updates the device date time.
-// telegram=|47449344483635121806780dff5f350082da0000600107c113ffff48200000bf2c91170000df2120200000008001000000060019001000160018000d001300350017002f046d370cc422c759|
-// {"_":"telegram","media":"warm water","meter":"qwater","name":"MyQWater","id":"12353648","status":"OK","total_m3":2.048,"due_date_m3":1.791,"due_date":"2021-12-31","due_17_date_m3":2.02,"due_17_date":"2022-01-31","error_date":"2128-03-31","volume_flow_m3h":0,"meter_datetime":"2022-02-04 12:55","timestamp":"1111-11-11T11:11:11Z"}
-// |MyQWater;12353648;2.048;1.791;2021-12-31;OK;1111-11-11 11:11.11
-
-// Test: AnotherQWater qwater 66666666 NOKEY
-// telegram=|3C449344682268363537726666666693443507720000200C13670512004C1361100300426CBF2CCC081344501100C2086CDF28326CFFFF046D0813CF29|
-// {"_":"telegram","media":"water","meter":"qwater","name":"AnotherQWater","id":"66666666","status":"OK","total_m3":120.567,"due_date_m3":31.061,"due_date":"2021-12-31","due_17_date_m3":115.044,"due_17_date":"2022-08-31","error_date":"2128-03-31","meter_datetime":"2022-09-15 19:08","timestamp":"1111-11-11T11:11:11Z"}
-// |AnotherQWater;66666666;120.567;31.061;2021-12-31;OK;1111-11-11 11:11.11
-
-// Test: YetAnoter qwater 33333333 NOKEY
-// telegram=|3C449344333333333537723333333393443506B8000020_0C13350000004C1300000000426CBF2CCC081300000000C2086CDF25326CFFFF046D0516CE26|
-// {"_":"telegram","media":"warm water","meter":"qwater","name":"YetAnoter","id":"33333333","status":"OK","total_m3":0.035,"due_date_m3":0,"due_date":"2021-12-31","due_17_date_m3":0,"due_17_date":"2022-05-31","error_date":"2128-03-31","meter_datetime":"2022-06-14 22:05","timestamp":"1111-11-11T11:11:11Z"}
-// |YetAnoter;33333333;0.035;0;2021-12-31;OK;1111-11-11 11:11.11
-
-// Test: QWater-7-18 qwater 12230094 NOKEY
-// telegram=|394493449400231218077ad30000200c13536712004c1307920500426cBf2ccc081373621200c2086cde2B02BB560000326cffff046d3714c32c|
-// {"_":"telegram","media":"water","meter":"qwater","name":"QWater-7-18","id":"12230094","status":"OK","total_m3":126.753,"due_date_m3":59.207,"due_date":"2021-12-31","due_17_date_m3":126.273,"due_17_date":"2022-11-30","error_date":"2128-03-31","volume_flow_m3h":0,"meter_datetime":"2022-12-03 20:55","timestamp":"1111-11-11T11:11:11Z"}
-// |QWater-7-18;12230094;126.753;59.207;2021-12-31;OK;1111-11-11 11:11.11
-
-
-// Test: QWoo qwater 13144514 NOKEY
-// Comment: Warm water
-// telegram=|394493441445141316067A31000020_0C13671605004C1348160500426CDF2CCC081348160500C2086CDF2C02BB560000326CFFFF046D3414E121|
-// {"_":"telegram","due_17_date": "2022-12-31","due_17_date_m3": 51.648,"due_date": "2022-12-31","due_date_m3": 51.648,"error_date": "2128-03-31","id": "13144514","media": "warm water","meter": "qwater","meter_datetime": "2023-01-01 20:52","name": "QWoo","status": "OK","timestamp": "1111-11-11T11:11:11Z","total_m3": 51.667,"volume_flow_m3h": 0}
-// |QWoo;13144514;51.667;51.648;2022-12-31;OK;1111-11-11 11:11.11
-
-// Test: QWooo qwater 13176890 NOKEY
-// Comment: Cold water
-// telegram=|394493449068171316077A0B000020_0C13358612004C1307851200426CDF2CCC081307851200C2086CDF2C02BB560000326CFFFF046D3014E121|
-// {"_":"telegram","due_17_date": "2022-12-31","due_17_date_m3": 128.507,"due_date": "2022-12-31","due_date_m3": 128.507,"error_date": "2128-03-31","id": "13176890","media": "water","meter": "qwater","meter_datetime": "2023-01-01 20:48","name": "QWooo","status": "OK","timestamp": "1111-11-11T11:11:11Z","total_m3": 128.635,"volume_flow_m3h": 0}
-// |QWooo;13176890;128.635;128.507;2022-12-31;OK;1111-11-11 11:11.11
-
-// Test: QWooo qwater 78563412 NOKEY
-// Comment: Proprietary Q walk-by message
-// telegram=|49449344123456781606780DFF5F3500824E00007F0007C113FFFF63961300DF2C82731200FE2463811300A400F200D100A900DD00E000E90006011601EA0027010F012F046D0211F225|
-// {"_":"telegram","due_17_date": "2023-04-30","due_17_date_m3": 138.163,"due_date": "2022-12-31","due_date_m3": 127.382,"id": "78563412","media": "warm water","meter": "qwater","meter_datetime": "2023-05-18 17:02","name": "QWooo","status": "OK","timestamp": "1111-11-11T11:11:11Z","total_m3": 139.663}
-// |QWooo;78563412;139.663;127.382;2022-12-31;OK;1111-11-11 11:11.11
-
-// Test: QWaaa qwater 51220588 NOKEY
-// Comment:
-// telegram=|4944934488052251190778_0DFF5F350082930000810007C113FFFF91670400FF2C265402001E34332204000000EE00F201A501DB01C1015401B70178019701B901C9012F046D06091D35|
-// {"_":"telegram","due_17_date": "2024-04-30","due_17_date_m3": 42.233,"due_date": "2023-12-31","due_date_m3": 25.426,"id": "51220588","media": "water","meter": "qwater","meter_datetime": "2024-05-29 09:06","name": "QWaaa","status": "OK","timestamp": "1111-11-11T11:11:11Z","total_m3": 46.791}
-// |QWaaa;51220588;46.791;25.426;2023-12-31;OK;1111-11-11 11:11.11
-
-// Test: QWccc qwater 13492674 NOKEY
-// Comment:
-// telegram=|394493447426491317077ADD0000200C13975710004C1330970700426CFF2CCC081387471000C2086C1E3B02BB560000326CFFFF046D1E0F113C|
-// {"_": "telegram","due_17_date": "2024-11-30","due_17_date_m3": 104.787,"due_date": "2023-12-31","due_date_m3": 79.73,"error_date": "2128-03-31","id": "13492674","media": "water","meter": "qwater","meter_datetime": "2024-12-17 15:30","name": "QWccc","status": "OK","timestamp": "1111-11-11T11:11:11Z","total_m3": 105.797,"volume_flow_m3h": 0}
-// |QWccc;13492674;105.797;79.73;2023-12-31;OK;1111-11-11 11:11.11
-
-// Test: QWddd qwater 13334995 NOKEY
-// Comment:
-// telegram=|49449344954933131706780DFF5F3500823A0000600107C113FFFF29970300FF2C580303001E3B269703000A006500750073005B0070007D0061005B004200160000002F046D1E0F113C|
-// {"_": "telegram","due_17_date": "2024-11-30","due_17_date_m3": 39.726,"due_date": "2023-12-31","due_date_m3": 30.358,"id": "13334995","media": "warm water","meter": "qwater","meter_datetime": "2024-12-17 15:30","name": "QWddd","status": "OK","timestamp": "1111-11-11T11:11:11Z","total_m3": 39.729}
-// |QWddd;13334995;39.729;30.358;2023-12-31;OK;1111-11-11 11:11.11
-
-// Test: QQ1 qwater 37439212 NOKEY
-// Comment:
-// telegram=|_5344934412924337353778077912924337934435070DFF5F3500828A0000100007C113FFFF966600001F3C000000003E3419580000008000800080008000800080008000005A0094009C00BB002F046D010F3235|
-// {"_": "telegram","due_17_date": "2025-04-30","due_17_date_m3": 5.819,"due_date": "2024-12-31","due_date_m3": 0,"id": "37439212","media": "radio converter (meter side)","meter": "qwater","meter_datetime": "2025-05-18 15:01","name": "QQ1","status": "OK","timestamp": "1111-11-11T11:11:11Z","total_m3": 6.696}
-// |QQ1;37439212;6.696;0;2024-12-31;OK;1111-11-11 11:11.11
-
-// Test: QQ2 qwater 37432649 NOKEY
-// Comment:
-// telegram=|_5344934449264337353778077949264337934435070DFF5F350082560000110007C113FFFF245300001F3C210400003E348946000000800080008000800080008000002A0066005F00730072002F046D000F3235|
-// {"_": "telegram","due_17_date": "2025-04-30","due_17_date_m3": 4.689,"due_date": "2024-12-31","due_date_m3": 0.421,"id": "37432649","media": "radio converter (meter side)","meter": "qwater","meter_datetime": "2025-05-18 15:00","name": "QQ2","status": "OK","timestamp": "1111-11-11T11:11:11Z","total_m3": 5.324}
-// |QQ2;37432649;5.324;0.421;2024-12-31;OK;1111-11-11 11:11.11
-
-// Test: QQ3 qwater 60101441 NOKEY
-// Comment:
-// telegram=|39449344411410601A067ADB000020_0C13780000004C1300000000426CFFFFCC081335000000C2086C3F3502BB560000326CFFFF046D14173136|
-// {"_": "telegram","due_17_date": "2025-05-31","due_17_date_m3": 0.035,"due_date": "2128-03-31","due_date_m3": 0,"error_date": "2128-03-31","id": "60101441","media": "warm water","meter": "qwater","meter_datetime": "2025-06-17 23:20","name": "QQ3","status": "OK","timestamp": "1111-11-11T11:11:11Z","total_m3": 0.078,"volume_flow_m3h": 0}
-// |QQ3;60101441;0.078;0;2128-03-31;OK;1111-11-11 11:11.11
-
-// Test: QQ4 qwater 60113189 NOKEY
-// Comment:
-// telegram=|39449344893111601a077a580000200c13200300004c1300000000426cffffcc081334000000c2086c3f3502BB560000326cffff046d13173136|
-// {"_": "telegram","due_17_date": "2025-05-31","due_17_date_m3": 0.034,"due_date": "2128-03-31","due_date_m3": 0,"error_date": "2128-03-31","id": "60113189","media": "water","meter": "qwater","meter_datetime": "2025-06-17 23:19","name": "QQ4","status": "OK","timestamp": "1111-11-11T11:11:11Z","total_m3": 0.32,"volume_flow_m3h": 0}
-// |QQ4;60113189;0.32;0;2128-03-31;OK;1111-11-11 11:11.11
diff --git a/components/wmbus_common/driver_rfmamb.cpp b/components/wmbus_common/driver_rfmamb.cpp
deleted file mode 100644
index d7ddd756..00000000
--- a/components/wmbus_common/driver_rfmamb.cpp
+++ /dev/null
@@ -1,223 +0,0 @@
-/*
- Copyright (C) 2019-2022 Fredrik Öhrström (gpl-3.0-or-later)
-
- 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 .
-*/
-
-#include"meters_common_implementation.h"
-
-namespace
-{
- struct Driver : public virtual MeterCommonImplementation
- {
- Driver(MeterInfo &mi, DriverInfo &di);
- };
-
- static bool ok = staticRegisterDriver([](DriverInfo&di)
- {
- di.setName("rfmamb");
- di.setDefaultFields("name,id,current_temperature_c,current_relative_humidity_rh,timestamp");
- di.setMeterType(MeterType::TempHygroMeter);
- di.addLinkMode(LinkMode::T1);
- di.setConstructor([](MeterInfo& mi, DriverInfo& di){ return shared_ptr(new Driver(mi, di)); });
- di.addMVT(MANUFACTURER_BMT, 0x1b, 0x10);
- });
-
- Driver::Driver(MeterInfo &mi, DriverInfo &di) : MeterCommonImplementation(mi, di)
- {
- addStringField(
- "status",
- "Meter status from tpl status field.",
- DEFAULT_PRINT_PROPERTIES |
- PrintProperty::STATUS | PrintProperty::INCLUDE_TPL_STATUS);
-
- addNumericFieldWithExtractor(
- "current_temperature",
- "The current temperature.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Temperature,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::ExternalTemperature)
- );
-
- addNumericFieldWithExtractor(
- "average_temperature_1h",
- "The average temperature over the last hour.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Temperature,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::ExternalTemperature)
- .set(StorageNr(1))
- );
-
- addNumericFieldWithExtractor(
- "average_temperature_24h",
- "The average temperature over the last 24 hours.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Temperature,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::ExternalTemperature)
- .set(StorageNr(2))
- );
-
- addNumericFieldWithExtractor(
- "maximum_temperature_1h",
- "The maximum temperature over the last hour.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Temperature,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Maximum)
- .set(VIFRange::ExternalTemperature)
- );
-
- addNumericFieldWithExtractor(
- "maximum_temperature_24h",
- "The maximum temperature over the last 24 hours.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Temperature,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Maximum)
- .set(VIFRange::ExternalTemperature)
- .set(StorageNr(1))
- );
-
- addNumericFieldWithExtractor(
- "minimum_temperature_1h",
- "The minimum temperature over the last hour.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Temperature,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Minimum)
- .set(VIFRange::ExternalTemperature)
- );
-
- addNumericFieldWithExtractor(
- "minimum_temperature_24h",
- "The minimum temperature over the last 24 hours.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Temperature,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Minimum)
- .set(VIFRange::ExternalTemperature)
- .set(StorageNr(1))
- );
-
- addNumericFieldWithExtractor(
- "current_relative_humidity",
- "The current relative humidity.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::RelativeHumidity,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::RelativeHumidity)
- );
-
- addNumericFieldWithExtractor(
- "average_relative_humidity_1h",
- "The average relative humidity over the last hour.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::RelativeHumidity,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::RelativeHumidity)
- .set(StorageNr(1))
- );
-
- addNumericFieldWithExtractor(
- "average_relative_humidity_24h",
- "The average relative humidity over the last 24 hours.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::RelativeHumidity,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::RelativeHumidity)
- .set(StorageNr(2))
- );
-
- addNumericFieldWithExtractor(
- "maximum_relative_humidity_1h",
- "The maximum relative humidity over the last hour.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::RelativeHumidity,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Maximum)
- .set(VIFRange::RelativeHumidity)
- );
-
- addNumericFieldWithExtractor(
- "maximum_relative_humidity_24h",
- "The maximum relative humidity over the last 24 hours.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::RelativeHumidity,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Maximum)
- .set(VIFRange::RelativeHumidity)
- .set(StorageNr(1))
- );
-
- addNumericFieldWithExtractor(
- "minimum_relative_humidity_1h",
- "The minimum relative humidity over the last hour.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::RelativeHumidity,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Minimum)
- .set(VIFRange::RelativeHumidity)
- );
-
- addNumericFieldWithExtractor(
- "minimum_relative_humidity_24h",
- "The minimum relative humidity over the last 24 hours.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::RelativeHumidity,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Minimum)
- .set(VIFRange::RelativeHumidity)
- .set(StorageNr(1))
- );
-
- addNumericFieldWithExtractor(
- "device",
- "The meters date time.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::PointInTime,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::DateTime)
- );
- }
-}
-
-// Test: Rummet rfmamb 11772288 NOKEY
-// telegram=|5744b40988227711101b7ab20800000265a00842658f088201659f08226589081265a0086265510852652b0902fb1aba0142fb1ab0018201fb1abd0122fb1aa90112fb1aba0162fb1aa60152fb1af501066d3b3bb36b2a00|
-// {"_":"telegram","media":"room sensor","meter":"rfmamb","name":"Rummet","id":"11772288","status":"PERMANENT_ERROR","current_temperature_c":22.08,"average_temperature_1h_c":21.91,"average_temperature_24h_c":22.07,"maximum_temperature_1h_c":22.08,"minimum_temperature_1h_c":21.85,"maximum_temperature_24h_c":23.47,"minimum_temperature_24h_c":21.29,"current_relative_humidity_rh":44.2,"average_relative_humidity_1h_rh":43.2,"average_relative_humidity_24h_rh":44.5,"minimum_relative_humidity_1h_rh":42.5,"maximum_relative_humidity_1h_rh":44.2,"maximum_relative_humidity_24h_rh":50.1,"minimum_relative_humidity_24h_rh":42.2,"device_datetime":"2019-10-11 19:59","timestamp":"1111-11-11T11:11:11Z"}
-// |Rummet;11772288;22.08;44.2;1111-11-11 11:11.11
diff --git a/components/wmbus_common/driver_rfmtx1.cpp b/components/wmbus_common/driver_rfmtx1.cpp
deleted file mode 100644
index 61a6bab4..00000000
--- a/components/wmbus_common/driver_rfmtx1.cpp
+++ /dev/null
@@ -1,114 +0,0 @@
-/*
- Copyright (C) 2020 Fredrik Öhrström (gpl-3.0-or-later)
-
- 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 .
-*/
-
-#include"meters_common_implementation.h"
-
-namespace
-{
- struct Driver : public virtual MeterCommonImplementation
- {
- Driver(MeterInfo &mi, DriverInfo &di);
- void processContent(Telegram *t);
- };
-
- static bool ok = staticRegisterDriver([](DriverInfo&di)
- {
- di.setName("rfmtx1");
- di.setDefaultFields("name,id,total_m3,meter_datetime,timestamp");
- di.setMeterType(MeterType::WaterMeter);
- di.addLinkMode(LinkMode::T1);
- di.addMVT(MANUFACTURER_BMT, 0x07, 0x05);
- di.usesProcessContent();
- di.setConstructor([](MeterInfo& mi, DriverInfo& di){ return shared_ptr(new Driver(mi, di)); });
- });
-
- Driver::Driver(MeterInfo &mi, DriverInfo &di) : MeterCommonImplementation(mi, di)
- {
- addNumericFieldWithExtractor(
- "total",
- "The total water consumption recorded by this meter.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Volume,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::Volume)
- );
-
- addStringFieldWithExtractor(
- "meter_datetime",
- "Date time when meter sent this telegram.",
- DEFAULT_PRINT_PROPERTIES,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::DateTime));
- }
-
- uchar decode_vectors_[16][6] = { { 117, 150, 122, 16, 26, 10 }, { 91, 127, 112, 19, 34, 19 }, { 179, 24, 185, 11, 142, 153 },
- { 142, 125, 121, 7, 74, 22 }, { 181, 145, 7, 154, 203, 105 }, { 184, 163, 50, 161, 57, 14 },
- { 189, 128, 156, 126, 96, 153 }, { 39, 92, 180, 196, 128, 163 }, { 48, 208, 10, 206, 25, 3 },
- { 194, 76, 240, 5, 165, 134 }, { 84, 75, 22, 152, 17, 94 }, { 75, 238, 12, 201, 125, 162 },
- { 135, 202, 74, 72, 228, 31 }, { 196, 135, 119, 46, 138, 232 }, { 227, 48, 189, 120, 87, 140 },
- { 164, 154, 57, 111, 40, 5 } };
-
- void Driver::processContent(Telegram *t)
- {
- if (t->tpl_cfg == 0x1006)
- {
- // This is the old type of meter and some values needs to be de-obfuscated.
- std::vector frame;
- t->extractFrame(&frame);
-
- debugPayload("(rftx1) decoding raw frame", frame);
-
- uchar decoded_total[6];
-
- for (int i = 0; i < 6; ++i)
- {
- decoded_total[i] = (uchar)(frame[0xf + i] ^ frame[0xb] ^ decode_vectors_[frame[0xb] & 0x0f][i]);
- }
-
- double total = 0;
- double mul = 1;
- for (int i = 2; i < 6; ++i)
- {
- total += mul*bcd2bin(decoded_total[i]);
- mul *= 100;
- }
- setNumericValue("total", Unit::M3, total/1000.0);
-
- int o = 28; // Offset to datetime.
- int y = 2000+bcd2bin(frame[o+5]);
- int M = bcd2bin(frame[o+4]);
- int d = bcd2bin(frame[o+3]);
- int H = bcd2bin(frame[o+2]);
- int m = bcd2bin(frame[o+1]);
- int s = bcd2bin(frame[o+0]);
-
- std::string tmp;
- strprintf(&tmp, "%d-%02d-%02d %02d:%02d:%02d",y, M%99, d%99, H%99, m%99, s%99);
- setStringValue("meter_datetime", tmp);
-
- return;
- }
- }
-}
-
-// Test: Wasser rfmtx1 74737271 NOKEY
-// telegram=|4644B4097172737405077AA5000610_1115F78184AB0F1D1E200000005904103103208047004A4800E73C00193E00453F003E4000E64000E74100F442000144001545005B460000|
-// {"_":"telegram","media":"water","meter":"rfmtx1","name":"Wasser","id":"74737271","total_m3":188.56,"meter_datetime":"2020-03-31 10:04:59","timestamp":"1111-11-11T11:11:11Z"}
-// |Wasser;74737271;188.56;2020-03-31 10:04:59;1111-11-11 11:11.11
diff --git a/components/wmbus_common/driver_sharky.cpp b/components/wmbus_common/driver_sharky.cpp
deleted file mode 100644
index 373ad9b0..00000000
--- a/components/wmbus_common/driver_sharky.cpp
+++ /dev/null
@@ -1,211 +0,0 @@
-/*
- Copyright (C) 2021 Vincent Privat (gpl-3.0-or-later)
- Copyright (C) 2022 Fredrik Öhrström (gpl-3.0-or-later)
-
- 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 .
-*/
-
-#include"meters_common_implementation.h"
-
-namespace
-{
- struct Driver : public virtual MeterCommonImplementation
- {
- Driver(MeterInfo &mi, DriverInfo &di);
- };
-
- static bool ok = staticRegisterDriver([](DriverInfo&di)
- {
- // This is the sharky 775 heat meter driver, should this merge with the sharky 774 driver?
- di.setName("sharky");
- di.setDefaultFields("name,id,total_energy_consumption_kwh,total_energy_consumption_tariff1_kwh,total_volume_m3,"
- "total_volume_tariff2_m3,volume_flow_m3h,power_kw,flow_temperature_c,"
- "return_temperature_c,temperature_difference_c,timestamp");
- di.setMeterType(MeterType::HeatMeter);
- di.addLinkMode(LinkMode::T1);
- di.addMVT(MANUFACTURER_HYD, 0x04, 0x20);
- di.addMVT(MANUFACTURER_DME, 0x04, 0x40);
- di.setConstructor([](MeterInfo& mi, DriverInfo& di){ return shared_ptr(new Driver(mi, di)); });
- });
-
- Driver::Driver(MeterInfo &mi, DriverInfo &di) : MeterCommonImplementation(mi, di)
- {
- addOptionalLibraryFields("operating_time_h");
-
- addStringFieldWithExtractorAndLookup(
- "status",
- "Status of meter.",
- DEFAULT_PRINT_PROPERTIES | PrintProperty::STATUS,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::ErrorFlags),
- Translate::Lookup()
- .add(Translate::Rule("ERROR_FLAGS", Translate::MapType::BitToString)
- .set(MaskBits(0x0000))
- .set(DefaultMessage("OK"))
- ));
-
- addNumericFieldWithExtractor(
- "total_energy_consumption",
- "The total heat energy consumption recorded by this meter.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Energy,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyEnergyVIF)
- );
-
- addNumericFieldWithExtractor(
- "total_energy_consumption_tariff1",
- "The total heat energy consumption recorded by this meter on tariff 1.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Energy,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyEnergyVIF)
- .set(TariffNr(1))
- );
-
- addNumericFieldWithExtractor(
- "total_volume",
- "The total heating media volume recorded by this meter.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Volume,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::Volume)
- );
-
- addNumericFieldWithExtractor(
- "total_volume_tariff2",
- "The total heating media volume recorded by this meter on tariff 2.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Volume,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::Volume)
- .set(TariffNr(2))
- );
-
- addNumericFieldWithExtractor(
- "volume_flow",
- "The current heat media volume flow.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Flow,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::VolumeFlow)
- );
-
- addNumericFieldWithExtractor(
- "power",
- "The current power consumption.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Power,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::PowerW)
- );
-
- addNumericFieldWithExtractor(
- "flow_temperature",
- "The current forward heat media temperature.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Temperature,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::FlowTemperature)
- );
-
- addNumericFieldWithExtractor(
- "return_temperature",
- "The current return heat media temperature.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Temperature,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::ReturnTemperature)
- );
-
- addNumericFieldWithExtractor(
- "temperature_difference",
- "The current return heat media temperature.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Temperature,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::TemperatureDifference)
- );
-
- addNumericFieldWithExtractor(
- "target_energy_consumption",
- "The total heat energy consumption recorded by this meter at the end of the previous billing period.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Energy,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyEnergyVIF)
- .set(StorageNr(5))
- );
-
- addNumericFieldWithExtractor(
- "target_volume",
- "The total heating media volume recorded by this meter at the end of the previous billing period.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Volume,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::Volume)
- .set(StorageNr(5))
- );
-
- addNumericFieldWithExtractor(
- "target",
- "The last billing period end date.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::PointInTime,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::Date)
- .set(StorageNr(5)),
- Unit::DateLT);
- }
-}
-
-// Test: Heat sharky 68926025 NOKEY
-// telegram=|534424232004256092687A370045752235854DEEEA5939FAD81C25FEEF5A23C38FB9168493C563F08DB10BAF87F660FBA91296BA2397E8F4220B86D3A192FB51E0BFCF24DCE72118E0C75A9E89F43BDFE370824B|
-// {"_":"telegram","media":"heat","meter":"sharky","name":"Heat","id":"68926025","total_energy_consumption_kwh":2651,"total_energy_consumption_tariff1_kwh":0,"total_volume_m3":150.347,"total_volume_tariff2_m3":0.018,"volume_flow_m3h":0,"power_kw":0,"flow_temperature_c":42.3,"return_temperature_c":28.1,"status":"OK","temperature_difference_c":14.1,"timestamp":"1111-11-11T11:11:11Z"}
-// |Heat;68926025;2651;0;150.347;0.018;0;0;42.3;28.1;14.1;1111-11-11 11:11.11
-
-// Test: Heat sharky 68926025 NOKEY
-// telegram=|5e44a5115376916140047a0B0050052f2f0c0e829311008c100e000000000c14014938000c2B751400000B3B2902000a5a52070a5e95060a6256000a279015cc020e92831100cc021478113800c2026cdf2c2f2f2f2f2f2f2f2f2f2f2f2f2f|
-// {"_":"telegram","flow_temperature_c": 42.3,"id": "68926025","media": "heat","meter": "sharky","name": "Heat","power_kw": 0,"return_temperature_c": 28.1,"status": "OK","temperature_difference_c": 14.1,"timestamp": "1111-11-11T11:11:11Z","total_energy_consumption_kwh": 2651,"total_energy_consumption_tariff1_kwh": 0,"total_volume_m3": 150.347,"total_volume_tariff2_m3": 0.018,"volume_flow_m3h": 0}
-// |Heat;68926025;2651;0;150.347;0.018;0;0;42.3;28.1;14.1;1111-11-11 11:11.11
-
-// Test: Heato sharky 69696969 NOKEY
-// telegram=|5e44a5116969696940047aBe0050052f2f0c06975100008c1006000000000c13849345000c2B000000000B3B0000000a5a06020a5e08020a6202f00B26110201cc020623500000cc021329554400c2026cdf2c2f2f2f2f2f2f2f2f2f2f2f2f|
-// {"_":"telegram","flow_temperature_c": 20.6,"id": "69696969","media": "heat","meter": "sharky","name": "Heato","operating_time_h": 10211,"power_kw": 0,"return_temperature_c": 20.8,"target_date": "2022-12-31","target_energy_consumption_kwh": 5023,"target_volume_m3": 445.529,"temperature_difference_c": -0.2,"timestamp": "1111-11-11T11:11:11Z","total_energy_consumption_kwh": 5197,"total_energy_consumption_tariff1_kwh": 0,"total_volume_m3": 459.384, "volume_flow_m3h": 0}
-// |Heato;69696969;5197;0;459.384;null;0;0;20.6;20.8;-0.2;1111-11-11 11:11.11
diff --git a/components/wmbus_common/driver_sharky774.cpp b/components/wmbus_common/driver_sharky774.cpp
deleted file mode 100644
index 6f11f640..00000000
--- a/components/wmbus_common/driver_sharky774.cpp
+++ /dev/null
@@ -1,225 +0,0 @@
-/*
- Copyright (C) 2021 Vincent Privat (gpl-3.0-or-later)
- Copyright (C) 2022 Fredrik Öhrström (gpl-3.0-or-later)
-
- 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 .
-*/
-
-#include"meters_common_implementation.h"
-
-namespace
-{
- struct Driver : public virtual MeterCommonImplementation
- {
- Driver(MeterInfo &mi, DriverInfo &di);
- };
-
- static bool ok = staticRegisterDriver([](DriverInfo&di)
- {
- di.setName("sharky774");
- di.setDefaultFields("name,id,"
- "total_energy_consumption_kwh,"
- "energy_at_set_date_kwh,"
- "set_date,"
- "timestamp");
- di.setMeterType(MeterType::HeatMeter);
- di.addLinkMode(LinkMode::T1);
- di.addMVT(MANUFACTURER_DME, 0x04, 0x41);
- di.addMVT(MANUFACTURER_DME, 0x0d, 0x41);
- di.addMVT(MANUFACTURER_DME, 0x0c, 0x41);
- di.setConstructor([](MeterInfo& mi, DriverInfo& di){ return shared_ptr(new Driver(mi, di)); });
- });
-
- Driver::Driver(MeterInfo &mi, DriverInfo &di) : MeterCommonImplementation(mi, di)
- {
- addNumericFieldWithExtractor(
- "total_energy_consumption",
- "The total energy consumption recorded by this meter.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Energy,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyEnergyVIF)
- );
-
- addNumericFieldWithExtractor(
- "total_cooling_consumption",
- "The total cooling energy consumption recorded by this meter.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Energy,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyEnergyVIF)
- .set(TariffNr(1))
- );
-
- addNumericFieldWithExtractor(
- "total_volume",
- "The total volume recorded by this meter.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Volume,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyVolumeVIF)
- );
-
- addNumericFieldWithExtractor(
- "total_cooling_volume",
- "The total cooling volume recorded by this meter.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Volume,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyVolumeVIF)
- .set(TariffNr(2))
- );
-
- addNumericFieldWithExtractor(
- "volume_flow",
- "The current flow.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Flow,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::VolumeFlow)
- );
-
- addNumericFieldWithExtractor(
- "power",
- "The power.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Power,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyPowerVIF)
- );
-
- addNumericFieldWithExtractor(
- "flow_temperature",
- "The flow temperature.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Temperature,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::FlowTemperature)
- );
-
- addNumericFieldWithExtractor(
- "return_temperature",
- "The return temperature.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Temperature,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::ReturnTemperature)
- );
-
- addNumericFieldWithExtractor(
- "operating_time",
- "How long the meter has been collecting data.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Time,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::OperatingTime)
- );
-
- addNumericFieldWithExtractor(
- "operating_time_in_error",
- "How long the meter has been in an error state and not collected data.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Time,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::OperatingTime)
- .add(VIFCombinable::RecordErrorCodeMeterToController)
- );
-
- addNumericFieldWithExtractor(
- "energy_at_set_date",
- "The total energy consumption recorded by this meter at the set date.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Energy,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyEnergyVIF)
- .set(StorageNr(1))
- );
-
- addNumericFieldWithExtractor(
- "cooling_at_set_date",
- "The total cooling energy consumption recorded by this meter at the set date.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Energy,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyEnergyVIF)
- .set(StorageNr(1))
- .set(TariffNr(1))
- );
-
- addNumericFieldWithExtractor(
- "set",
- "The last billing set date.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::PointInTime,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::Date)
- .set(StorageNr(1)),
- Unit::DateLT
- );
- }
-}
-
-// Test: Heato sharky774 58496405 NOKEY
-// telegram=3E44A5110564495841047A700030052F2F_0C06846800000C13195364000B3B0400000C2B110100000A5A17050A5E76020AA61800004C0647630000426CBF25
-// {"_":"telegram","media":"heat","meter":"sharky774","name":"Heato","id":"58496405","total_energy_consumption_kwh":6884,"total_volume_m3":645.319,"volume_flow_m3h":0.004,"power_kw":0.111,"flow_temperature_c":51.7,"return_temperature_c":27.6,"operating_time_in_error_h":0,"energy_at_set_date_kwh":6347,"set_date":"2021-05-31","timestamp":"1111-11-11T11:11:11Z"}
-// |Heato;58496405;6884;6347;2021-05-31;1111-11-11 11:11.11
-
-// Test: diehl_meter sharky774 52173898 NOKEY
-// telegram=|3E44A51198381752410C7AA80030052F2F_0C06105104000C13093835020B3B9401000C2B342600000A5A12060A5E91040AA61800004C0641460400426CFF21|
-// {"_":"telegram","media":"heat volume at inlet","meter":"sharky774","name":"diehl_meter","id":"52173898","total_energy_consumption_kwh":45110,"total_volume_m3":2353.809,"volume_flow_m3h":0.194,"power_kw":2.634,"flow_temperature_c":61.2,"return_temperature_c":49.1,"operating_time_in_error_h":0,"energy_at_set_date_kwh":44641,"set_date":"2023-01-31","timestamp":"1111-11-11T11:11:11Z"}
-// |diehl_meter;52173898;45110;44641;2023-01-31;1111-11-11 11:11.11
-
-// This test telegram has more historical data!
-// Test: Heatoo sharky774 72615127 NOKEY
-// telegram=|5E44A5112751617241047A8B0050052F2F0C0E000000000C13010000000B3B0000000C2B000000000A5A26020A5E18020B260321000AA6180000C2026CBE2BCC020E00000000CC021301000000DB023B000000DC022B000000002F2F2F2F2F|
-// {"_":"telegram","media":"heat","meter":"sharky774","name":"Heatoo","id":"72615127","total_energy_consumption_kwh":0,"total_volume_m3":0.001,"volume_flow_m3h":0,"power_kw":0,"flow_temperature_c":22.6,"return_temperature_c":21.8,"operating_time_h":2103,"operating_time_in_error_h":0,"timestamp":"1111-11-11T11:11:11Z"}
-// |Heatoo;72615127;0;null;null;1111-11-11 11:11.11
-
-// This telegram contains a negative power value encoded in bcd.
-// Test: Heatooo sharky774 61243590 NOKEY
-// telegram=3E44A5119035246141047A1A0030052F2F_0C06026301000C13688609040B3B0802000C2B220000F00A5A71020A5E72020AA61800004C0636370100426CBF25
-// {"_":"telegram","media":"heat","meter":"sharky774","name":"Heatooo","id":"61243590","total_energy_consumption_kwh":16302,"total_volume_m3":4098.668,"volume_flow_m3h":0.208,"power_kw":-0.022,"flow_temperature_c":27.1,"return_temperature_c":27.2,"operating_time_in_error_h":0,"energy_at_set_date_kwh":13736,"set_date":"2021-05-31","timestamp":"1111-11-11T11:11:11Z"}
-// |Heatooo;61243590;16302;13736;2021-05-31;1111-11-11 11:11.11
-
-// This telegram contains cooling data as well.
-// Test: Coolo sharky774 71942539 NOKEY
-// telegram=5E44A51139259471410D7A720050052F2F_0C06742400008C1006000000000C13823522008C2013494400000B3B0000000C2B000000000A5A22030A5E91020AA61800004C0619130000CC100600000000426CDF252F2F2F2F2F2F2F2F2F2F2F
-// {"_":"telegram","cooling_at_set_date_kwh": 0,"energy_at_set_date_kwh": 1319,"flow_temperature_c": 32.2,"id": "71942539","media": "heat/cooling load","meter": "sharky774","name": "Coolo","operating_time_in_error_h": 0,"power_kw": 0,"return_temperature_c": 29.1,"set_date": "2022-05-31","timestamp": "1111-11-11T11:11:11Z","total_cooling_consumption_kwh": 0,"total_cooling_volume_m3": 4.449,"total_energy_consumption_kwh": 2474,"total_volume_m3": 223.582,"volume_flow_m3h": 0}
-// |Coolo;71942539;2474;1319;2022-05-31;1111-11-11 11:11.11
diff --git a/components/wmbus_common/driver_sontex868.cpp b/components/wmbus_common/driver_sontex868.cpp
deleted file mode 100644
index 506ef5f2..00000000
--- a/components/wmbus_common/driver_sontex868.cpp
+++ /dev/null
@@ -1,134 +0,0 @@
-/*
- Copyright (C) 2020-2022 Fredrik Öhrström (gpl-3.0-or-later)
-
- 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 .
-*/
-
-#include"meters_common_implementation.h"
-
-namespace
-{
- struct Driver : public virtual MeterCommonImplementation
- {
- Driver(MeterInfo &mi, DriverInfo &di);
- };
-
- static bool ok = staticRegisterDriver([](DriverInfo&di)
- {
- di.setName("sontex868");
- di.setDefaultFields("name,id,current_consumption_hca,set_date,consumption_at_set_date_hca,timestamp");
-
- di.setMeterType(MeterType::HeatCostAllocationMeter);
- di.addLinkMode(LinkMode::T1);
- di.addMVT(MANUFACTURER_SON, 0x08, 0x16);
-
- di.setConstructor([](MeterInfo& mi, DriverInfo& di){ return shared_ptr(new Driver(mi, di)); });
- });
-
- Driver::Driver(MeterInfo &mi, DriverInfo &di) : MeterCommonImplementation(mi, di)
- {
- addNumericFieldWithExtractor(
- "current_consumption",
- "The current heat cost allocation for this meter.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::HCA,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::HeatCostAllocation)
- );
-
- addStringFieldWithExtractor(
- "set_date",
- "The most recent billing period date.",
- DEFAULT_PRINT_PROPERTIES,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::Date)
- .set(StorageNr(1))
- );
-
- addNumericFieldWithExtractor(
- "consumption_at_set_date",
- "Heat cost allocation at the most recent billing period date.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::HCA,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::HeatCostAllocation)
- .set(StorageNr(1))
- );
-
- addNumericFieldWithExtractor(
- "current_temp",
- "The current temperature of the heating element.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Temperature,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::FlowTemperature)
- );
-
- addNumericFieldWithExtractor(
- "current_room_temp",
- "The current room temperature.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Temperature,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::ExternalTemperature)
- );
-
- addNumericFieldWithExtractor(
- "max_temp",
- "The maximum temperature so far during this billing period.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Temperature,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Maximum)
- .set(VIFRange::FlowTemperature)
- );
-
-
- addNumericFieldWithExtractor(
- "max_temp_previous_period",
- "The maximum temperature during the previous billing period.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Temperature,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Maximum)
- .set(VIFRange::FlowTemperature)
- .set(StorageNr(1))
- );
-
- addStringFieldWithExtractor(
- "device_date_time",
- "Date and time when the meter sent the telegram.",
- DEFAULT_PRINT_PROPERTIES,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::DateTime)
- );
- }
-}
-
-// Test: MyHeatCoster sontex868 27282728 NOKEY
-// telegram=|AF46EE4D2827282716087A80000000_046D040A9F2A036E770000426CE1F7436E660000525900008288016C61258388016E0000008D8801EE1E3533FE00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005FF2D0000803F8520FF2D0000803F0259AD0A0265D8041259AD0A8310FD3100000082106C01018110FD610082206C9F2A0BFD0F01030102FF2C000002FD66AC08|
-// {"_":"telegram","media":"heat cost allocation","meter":"sontex868","name":"MyHeatCoster","id":"27282728","current_consumption_hca":119,"set_date":"2127-07-01","consumption_at_set_date_hca":102,"current_temp_c":27.33,"current_room_temp_c":12.4,"max_temp_c":27.33,"max_temp_previous_period_c":0,"device_date_time":"2020-10-31 10:04","timestamp":"1111-11-11T11:11:11Z"}
-// |MyHeatCoster;27282728;119;2127-07-01;102;1111-11-11 11:11.11
diff --git a/components/wmbus_common/driver_supercom587.cpp b/components/wmbus_common/driver_supercom587.cpp
deleted file mode 100644
index 31ea8771..00000000
--- a/components/wmbus_common/driver_supercom587.cpp
+++ /dev/null
@@ -1,66 +0,0 @@
-/*
- Copyright (C) 2017-2022 Fredrik Öhrström (gpl-3.0-or-later)
-
- 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 .
-*/
-
-#include"meters_common_implementation.h"
-
-namespace
-{
- struct Driver : public virtual MeterCommonImplementation
- {
- Driver(MeterInfo &mi, DriverInfo &di);
- };
-
- static bool ok = staticRegisterDriver([](DriverInfo&di)
- {
- di.setName("supercom587");
- di.setDefaultFields("name,id,total_m3,timestamp");
- di.setMeterType(MeterType::WaterMeter);
- di.addLinkMode(LinkMode::T1);
- di.addMVT(MANUFACTURER_SON, 0x06, 0x3c);
- di.addMVT(MANUFACTURER_SON, 0x07, 0x3c);
- di.setConstructor([](MeterInfo& mi, DriverInfo& di){ return shared_ptr(new Driver(mi, di)); });
- });
-
- Driver::Driver(MeterInfo &mi, DriverInfo &di) : MeterCommonImplementation(mi, di)
- {
- addOptionalLibraryFields("software_version");
- addOptionalLibraryFields("total_m3");
-
- addStringFieldWithExtractorAndLookup(
- "status",
- "Status of meter.",
- DEFAULT_PRINT_PROPERTIES | PrintProperty::STATUS | PrintProperty::INCLUDE_TPL_STATUS,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::ErrorFlags),
- Translate::Lookup()
- .add(Translate::Rule("ERROR_FLAGS", Translate::MapType::BitToString)
- .set(MaskBits(0x000f))
- .set(DefaultMessage("OK"))
- ));
- }
-}
-
-// Test: MyWarmWater supercom587 12345678 NOKEY
-// telegram=|A244EE4D785634123C067A8F000000_0C1348550000426CE1F14C130000000082046C21298C0413330000008D04931E3A3CFE3300000033000000330000003300000033000000330000003300000033000000330000003300000033000000330000004300000034180000046D0D0B5C2B03FD6C5E150082206C5C290BFD0F0200018C4079678885238310FD3100000082106C01018110FD610002FD66020002FD170000|
-// {"_":"telegram","media":"warm water","meter":"supercom587","name":"MyWarmWater","id":"12345678","total_m3":5.548,"software_version":"010002","status":"OK","timestamp":"1111-11-11T11:11:11Z"}
-// |MyWarmWater;12345678;5.548;1111-11-11 11:11.11
-
-// Test: MyColdWater supercom587 11111111 NOKEY
-// telegram=|A244EE4D111111113C077AAC000000_0C1389490000426CE1F14C130000000082046C21298C0413010000008D04931E3A3CFE0100000001000000010000000100000001000000010000000100000001000000010000000100000001000000010000001600000031130000046D0A0C5C2B03FD6C60150082206C5C290BFD0F0200018C4079629885238310FD3100000082106C01018110FD610002FD66020002FD170000|
-// {"_":"telegram","media":"water","meter":"supercom587","name":"MyColdWater","id":"11111111","total_m3":4.989,"software_version":"010002","status":"OK","timestamp":"1111-11-11T11:11:11Z"}
-// |MyColdWater;11111111;4.989;1111-11-11 11:11.11
diff --git a/components/wmbus_common/driver_topaseskr.cpp b/components/wmbus_common/driver_topaseskr.cpp
deleted file mode 100644
index 7650bad2..00000000
--- a/components/wmbus_common/driver_topaseskr.cpp
+++ /dev/null
@@ -1,164 +0,0 @@
-/*
- Copyright (C) 2017-2022 Fredrik Öhrström (gpl-3.0-or-later)
- Copyright (C) 2020 Avandorp (gpl-3.0-or-later)
-
- 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 .
-*/
-
-/*
- AquaMetro / Integra water meter "TOPAS ES KR"
- Models TOPAS ES KR 95077 95056 95345 95490 95373 95059 95065 95068 95071 95074 should be compatible. Only 95059 in one configuration tested.
- Identifies itself with Manufacturer "AMT" and Version "f1"
- Product leaflet and observation says the following values are sent:
- Current total volume
- Total volume at end of year-period day (that means: current total volume - total volume at end of year-period day = current year-periods volume up until now)
- Total backward volume on end of year-period day or total backward volume in current year-period. Backward volume remains untested (luckily only 0 values encountered).
- Date of end of last year-period day
- Total volume at end of last month-period dateTime
- DateTime of end of last month-period
- Current flow rate
- Battery life (days left)
- Water temperature
-
- Example telegram:
- telegram=|4E44B40512345678F1077A310040052F2F|01FD08040C13991848004C1359423500CC101300000000CC201359423500426C7F2C0B3B00000002FD74DA10025AD300C4016D3B179F27CC011387124600|+2
-*/
-
-#include"meters_common_implementation.h"
-
-namespace
-{
- struct Driver : public virtual MeterCommonImplementation
- {
- Driver(MeterInfo &mi, DriverInfo &di);
- };
-
- static bool ok = staticRegisterDriver([](DriverInfo&di)
- {
- di.setName("topaseskr");
- di.setDefaultFields(
- "name,id,total_m3,temperature_c,current_flow_m3h,volume_year_period_m3,"
- "reverse_volume_year_period_m3,meter_year_period_end_date,volume_month_period_m3,"
- "meter_month_period_end_datetime,timestamp");
- di.setMeterType(MeterType::WaterMeter);
- di.addLinkMode(LinkMode::T1);
- di.addMVT(MANUFACTURER_AMT, 0x06, 0xf1);
- di.addMVT(MANUFACTURER_AMT, 0x07, 0xf1);
- di.setConstructor([](MeterInfo& mi, DriverInfo& di){ return shared_ptr(new Driver(mi, di)); });
- });
-
- Driver::Driver(MeterInfo &mi, DriverInfo &di) : MeterCommonImplementation(mi, di)
- {
- addOptionalLibraryFields("total_m3,access_counter");
-
- addNumericFieldWithExtractor(
- "temperature",
- "Current water temperature recorded by this meter.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Temperature,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::FlowTemperature));
-
- addNumericFieldWithExtractor(
- "current_flow",
- "The current water flow.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Flow,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::VolumeFlow));
-
- addNumericFieldWithExtractor(
- "volume_year_period",
- "Volume up to end of last year-period.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Volume,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::Volume)
- .set(StorageNr(1))
- );
-
- addNumericFieldWithExtractor(
- "reverse_volume_year_period",
- "Reverse volume in this year-period (?)",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Volume,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::Volume)
- .set(StorageNr(1))
- .set(TariffNr(1))
- );
-
-
- addStringFieldWithExtractor(
- "meter_year_period_end_date",
- "Meter date for year-period end.",
- DEFAULT_PRINT_PROPERTIES,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::Date)
- .set(StorageNr(1))
- );
-
- addNumericFieldWithExtractor(
- "volume_month_period",
- "Volume up to end of last month-period.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Volume,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::Volume)
- .set(StorageNr(3))
- );
-
- addStringFieldWithExtractor(
- "meter_month_period_end_datetime",
- "Meter timestamp for month-period end.",
- DEFAULT_PRINT_PROPERTIES,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::DateTime)
- .set(StorageNr(3))
- );
-
- addNumericFieldWithExtractor(
- "battery",
- "Remaining battery life in years.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Time,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::RemainingBattery),
- Unit::Year);
- }
-}
-
-// Test: Witer topaseskr 78563412 NOKEY
-// telegram=|4E44B40512345678F1077A310040052F2F_01FD08040C13991848004C1359423500CC101300000000CC201359423500426C7F2C0B3B00000002FD74DA10025AD300C4016D3B179F27CC011387124600|
-// {"_":"telegram","media":"water","meter":"topaseskr","name":"Witer","id":"78563412","total_m3":481.899,"access_counter":4,"temperature_c":21.1,"current_flow_m3h":0,"volume_year_period_m3":354.259,"reverse_volume_year_period_m3":0,"meter_year_period_end_date":"2019-12-31","volume_month_period_m3":461.287,"meter_month_period_end_datetime":"2020-07-31 23:59","battery_y":11.811331,"timestamp":"1111-11-11T11:11:11Z"}
-// |Witer;78563412;481.899;21.1;0;354.259;0;2019-12-31;461.287;2020-07-31 23:59;1111-11-11 11:11.11
-
-// Test: Woter topaseskr 69190253 NOKEY
-// telegram=|4E44B40553021969F1077A0C0040052F2F_01FD08800C13914544004C1393673500CC101300000000CC201393673500426CDF2C0B3B0100F002FD747912025AAE00C4016D3B17FF25CC011325584100|
-// {"_":"telegram","access_counter": 128,"battery_y": 12.947562,"current_flow_m3h": -0.001,"id": "69190253","media": "water","meter": "topaseskr","meter_month_period_end_datetime": "2023-05-31 23:59","meter_year_period_end_date": "2022-12-31","name": "Woter","reverse_volume_year_period_m3": 0,"temperature_c": 17.4,"timestamp": "1111-11-11T11:11:11Z","total_m3": 444.591,"volume_month_period_m3": 415.825,"volume_year_period_m3": 356.793}
-// |Woter;69190253;444.591;17.4;-0.001;356.793;0;2022-12-31;415.825;2023-05-31 23:59;1111-11-11 11:11.11
diff --git a/components/wmbus_common/driver_tsd2.cpp b/components/wmbus_common/driver_tsd2.cpp
deleted file mode 100644
index d935d5b9..00000000
--- a/components/wmbus_common/driver_tsd2.cpp
+++ /dev/null
@@ -1,96 +0,0 @@
-/*
- Copyright (C) 2020-2023 Fredrik Öhrström (gpl-3.0-or-later)
-
- 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 .
-*/
-
-#include"meters_common_implementation.h"
-
-namespace
-{
- struct Driver : public virtual MeterCommonImplementation
- {
- Driver(MeterInfo &mi, DriverInfo &di);
-
- void processContent(Telegram *t);
- };
-
- static bool ok = staticRegisterDriver([](DriverInfo&di)
- {
- di.setName("tsd2");
- di.setDefaultFields("name,id,status,prev_date,timestamp");
- di.setMeterType(MeterType::SmokeDetector);
- di.addLinkMode(LinkMode::T1);
- di.addMVT(MANUFACTURER_TCH, 0xf0, 0x76);
- di.usesProcessContent();
- di.setConstructor([](MeterInfo& mi, DriverInfo& di){ return shared_ptr(new Driver(mi, di)); });
- });
-
- Driver::Driver(MeterInfo &mi, DriverInfo &di) : MeterCommonImplementation(mi, di)
- {
- addStringField("status",
- "The current status: OK, SMOKE or ERROR.",
- DEFAULT_PRINT_PROPERTIES);
-
- addStringField("prev_date",
- "Date of previous billing period.",
- DEFAULT_PRINT_PROPERTIES);
- }
-
-#define INFO_CODE_SMOKE 0x01
-
- void Driver::processContent(Telegram *t)
- {
- std::vector data;
- t->extractPayload(&data);
-
- if(data.size() < 3)
- {
- setStringValue("status", "ERROR");
- return;
- }
-
- uint8_t status = data[0];
- if (status & INFO_CODE_SMOKE)
- {
- setStringValue("status", "SMOKE");
- }
- else
- {
- setStringValue("status", "OK");
- }
-
- uint16_t prev_date = (data[2] << 8) | data[1];
- uint prev_date_day = (prev_date >> 0) & 0x1F;
- uint prev_date_month = (prev_date >> 5) & 0x0F;
- uint prev_date_year = (prev_date >> 9) & 0x3F;
-
- setStringValue("prev_date",
- tostrprintf("%d-%02d-%02dT02:00:00Z", prev_date_year+2000, prev_date_month, prev_date_day));
- }
-}
-
-// Test: Smokey tsd2 91633569 NOKEY
-
-// telegram=|294468506935639176F0A0_019F|
-// {"_":"telegram","media":"smoke detector","meter":"tsd2","name":"Smokey","id":"91633569","status":"ERROR","timestamp":"1111-11-11T11:11:11Z"}
-// |Smokey;91633569;ERROR;null;1111-11-11 11:11.11
-
-// telegram=|294468506935639176F0A0_009F2782290060822900000401D6311AF93E1BF93E008DC3009ED4000FE500|
-// {"_":"telegram","media":"smoke detector","meter":"tsd2","name":"Smokey","id":"91633569","status":"OK","prev_date":"2019-12-31T02:00:00Z","timestamp":"1111-11-11T11:11:11Z"}
-// |Smokey;91633569;OK;2019-12-31T02:00:00Z;1111-11-11 11:11.11
-
-// telegram=|294468506935639176F0A0_019F2782290060822900000401D6311AF93E1BF93E008DC3009ED4000FE500|
-// {"_":"telegram","media":"smoke detector","meter":"tsd2","name":"Smokey","id":"91633569","status":"SMOKE","prev_date":"2019-12-31T02:00:00Z","timestamp":"1111-11-11T11:11:11Z"}
-// |Smokey;91633569;SMOKE;2019-12-31T02:00:00Z;1111-11-11 11:11.11
diff --git a/components/wmbus_common/driver_ultraheat.cpp b/components/wmbus_common/driver_ultraheat.cpp
deleted file mode 100644
index 7bdcb079..00000000
--- a/components/wmbus_common/driver_ultraheat.cpp
+++ /dev/null
@@ -1,138 +0,0 @@
-/*
- Copyright (C) 2022-2023 Fredrik Öhrström (gpl-3.0-or-later)
-
- 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 .
-*/
-
-#include"meters_common_implementation.h"
-
-namespace
-{
- struct Driver : public virtual MeterCommonImplementation
- {
- Driver(MeterInfo &mi, DriverInfo &di);
- };
-
- static bool ok = staticRegisterDriver([](DriverInfo&di)
- {
- di.setName("ultraheat");
- di.setDefaultFields("name,id,heat_kwh,timestamp");
- di.setMeterType(MeterType::HeatMeter);
- di.addMVT(MANUFACTURER_LUG, 0x04, 0x04);
- di.setConstructor([](MeterInfo& mi, DriverInfo& di){ return shared_ptr(new Driver(mi, di)); });
- });
-
- Driver::Driver(MeterInfo &mi, DriverInfo &di) : MeterCommonImplementation(mi, di)
- {
- addOptionalLibraryFields("meter_datetime,fabrication_no");
-
- addNumericFieldWithExtractor(
- "heat",
- "The total heat energy consumption recorded by this meter.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Energy,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyEnergyVIF)
- );
-
- addNumericFieldWithExtractor(
- "volume",
- "The total heating media volume recorded by this meter.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Volume,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::Volume)
- );
-
- addNumericFieldWithExtractor(
- "power",
- "The current power consumption.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Power,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::PowerW)
- );
-
- addNumericFieldWithExtractor(
- "flow",
- "The current heat media volume flow.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Flow,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::VolumeFlow)
- );
-
- addNumericFieldWithExtractor(
- "flow",
- "The current forward heat media temperature.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Temperature,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::FlowTemperature)
- );
-
- addNumericFieldWithExtractor(
- "return",
- "The current return heat media temperature.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Temperature,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::ReturnTemperature)
- );
-
- addStringFieldWithExtractorAndLookup(
- "status",
- "Meter status.",
- PrintProperty::STATUS | PrintProperty::INCLUDE_TPL_STATUS,
- FieldMatcher::build()
- .set(VIFRange::ErrorFlags),
- Translate::Lookup(
- {
- {
- {
- "ERROR_FLAGS",
- Translate::MapType::BitToString,
- AlwaysTrigger, MaskBits(0xffff),
- "OK",
- {
- /* TODO */
- }
- },
- },
- }));
-
- }
-}
-
-// Test: MyUltra ultraheat 70444600 NOKEY
-// telegram=|68F8F86808007200464470A7320404270000000974040970040C0E082303000C14079519000B2D0500000B3B0808000A5B52000A5F51000A6206004C14061818004C0E490603000C7800464470891071609B102D020100DB102D0201009B103B6009009A105B78009A105F74000C22726701003C22000000007C2200000000426C01018C2006000000008C3006000000008C80100600000000CC200600000000CC300600000000CC801006000000009A115B64009A115F63009B113B5208009B112D020100BC0122000000008C010E490603008C2106000000008C3106000000008C811006000000008C011406181800046D310ACA210F21040010A0C116|
-// {"_":"telegram","media":"heat","status":"OK","meter":"ultraheat","meter_datetime": "2022-01-10 10:49", "name":"MyUltra","id":"70444600","heat_kwh":8974.444444,"volume_m3":1995.07,"power_kw":0.5,"flow_m3h":0.808,"flow_c":52,"return_c":51,"fabrication_no": "70444600","timestamp":"1111-11-11T11:11:11Z"}
-// |MyUltra;70444600;8974.444444;1111-11-11 11:11.11
-
-// Test: MyUltra2 ultraheat 71635605 NOKEY
-// telegram=|3b44a7320556637104047afa2000202f2f0c06774202000c14399956000b2d0200f00b3b3018000a5a51030a5e520302fd170000066d0c080af42500|
-// {"_":"telegram","flow_c": 35.1,"flow_m3h": 1.83,"heat_kwh": 24277,"id": "71635605","media": "heat","meter": "ultraheat","meter_datetime": "2023-05-20 10:08:12","name": "MyUltra2","power_kw": -0.2,"return_c": 35.2,"status": "UNKNOWN_20","timestamp": "1111-11-11T11:11:11Z","volume_m3": 5699.39}
-// |MyUltra2;71635605;24277;1111-11-11 11:11.11
diff --git a/components/wmbus_common/driver_ultrimis.cpp b/components/wmbus_common/driver_ultrimis.cpp
deleted file mode 100644
index 6fa458ea..00000000
--- a/components/wmbus_common/driver_ultrimis.cpp
+++ /dev/null
@@ -1,106 +0,0 @@
-/*
- Copyright (C) 2017-2022 Fredrik Öhrström (gpl-3.0-or-later)
-
- 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 .
-*/
-
-#include"meters_common_implementation.h"
-
-namespace
-{
- struct Driver : public virtual MeterCommonImplementation {
- Driver(MeterInfo &mi, DriverInfo &di);
- };
-
- static bool ok = staticRegisterDriver([](DriverInfo&di)
- {
- di.setName("ultrimis");
- di.setDefaultFields("name,id,total_m3,target_m3,current_status,total_backward_flow_m3,timestamp");
- di.setMeterType(MeterType::WaterMeter);
- di.addLinkMode(LinkMode::T1);
- di.addMVT(MANUFACTURER_APA, 0x16, 0x01);
- di.setConstructor([](MeterInfo& mi, DriverInfo& di){ return shared_ptr(new Driver(mi, di)); });
- });
-
- Driver::Driver(MeterInfo &mi, DriverInfo &di) : MeterCommonImplementation(mi, di)
- {
- addNumericFieldWithExtractor(
- "total",
- "The total water consumption recorded by this meter.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Volume,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::Volume)
- );
-
- addNumericFieldWithExtractor(
- "target",
- "The total water consumption recorded at the beginning of this month.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Volume,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::Volume)
- .set(StorageNr(1))
- );
-
- addStringFieldWithExtractorAndLookup(
- "current_status",
- "Status and error flags.",
- DEFAULT_PRINT_PROPERTIES,
- FieldMatcher::build()
- .set(DifVifKey("03FD17")),
- {
- {
- {
- "ERROR_FLAGS",
- Translate::MapType::BitToString,
- AlwaysTrigger, MaskBits(0xffffff),
- "OK",
- {
- /* What are the bits?
- According to the manual this meter offers these alarms:
- Back flow
- Meter leak
- Water main leak
- Zero flow
- Tampering detected
- No water
- Low battery
- */
- }
- },
- },
- });
-
- addNumericFieldWithExtractor(
- "total_backward_flow",
- "The total backward water volume recorded by this meter.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Volume,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(DifVifKey("04933C"))
- );
- }
-}
-
-// Test: Water ultrimis 95969798 NOKEY
-// Comment:
-// telegram=|2E4401069897969501167A4B0320052F2F_0413320C000003FD1700000044132109000004933C000000002F2F2F2F2F|
-// {"_":"telegram","media":"cold water","meter":"ultrimis","name":"Water","id":"95969798","total_m3":3.122,"target_m3":2.337,"current_status":"OK","total_backward_flow_m3":0,"timestamp":"1111-11-11T11:11:11Z"}
-// |Water;95969798;3.122;2.337;OK;0;1111-11-11 11:11.11
diff --git a/components/wmbus_common/driver_unismart.cpp b/components/wmbus_common/driver_unismart.cpp
deleted file mode 100644
index 8ed95b3b..00000000
--- a/components/wmbus_common/driver_unismart.cpp
+++ /dev/null
@@ -1,168 +0,0 @@
-/*
- Copyright (C) 2021-2022 Fredrik Öhrström (gpl-3.0-or-later)
-
- 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 .
-*/
-
-#include"meters_common_implementation.h"
-
-namespace
-{
- struct Driver : public virtual MeterCommonImplementation
- {
- Driver(MeterInfo &mi, DriverInfo &di);
- };
-
- static bool ok = staticRegisterDriver([](DriverInfo&di)
- {
- di.setName("unismart");
- di.setDefaultFields("name,id,total_m3,target_m3,timestamp");
- di.setMeterType(MeterType::GasMeter);
- di.addLinkMode(LinkMode::T1);
- di.addMVT(MANUFACTURER_AMX, 0x03, 0x01);
-
- di.setConstructor([](MeterInfo& mi, DriverInfo& di){ return shared_ptr(new Driver(mi, di)); });
- });
-
- Driver::Driver(MeterInfo &mi, DriverInfo &di) : MeterCommonImplementation(mi, di)
- {
- addOptionalLibraryFields("fabrication_no");
-
- addStringFieldWithExtractorAndLookup(
- "status",
- "Status of meter?",
- DEFAULT_PRINT_PROPERTIES | PrintProperty::STATUS,
- FieldMatcher::build()
- .set(DifVifKey("02FD74")),
- {
- {
- {
- "STATUS_FLAGS",
- Translate::MapType::BitToString,
- AlwaysTrigger, MaskBits(0xffff),
- "OK",
- {
- }
- },
- },
- });
-
- addStringFieldWithExtractorAndLookup(
- "other",
- "Other status of meter?",
- DEFAULT_PRINT_PROPERTIES | PrintProperty::STATUS,
- FieldMatcher::build()
- .set(DifVifKey("017F")),
- {
- {
- {
- "OTHER_FLAGS",
- Translate::MapType::BitToString,
- AlwaysTrigger, MaskBits(0xff),
- "",
- {
- }
- },
- },
- });
-
- addStringFieldWithExtractor(
- "total_date_time",
- "Timestamp for this total measurement.",
- DEFAULT_PRINT_PROPERTIES,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::DateTime)
- .set(IndexNr(1))
- );
-
- addNumericFieldWithExtractor(
- "total",
- "The total gas consumption recorded by this meter.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Volume,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::Volume)
- .add(VIFCombinable::UncorrectedMeterUnit)
- );
-
- addStringFieldWithExtractor(
- "target_date_time",
- "Timestamp for gas consumption recorded at the beginning of this month.",
- DEFAULT_PRINT_PROPERTIES,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::DateTime)
- .set(StorageNr(1))
- );
-
- addNumericFieldWithExtractor(
- "target",
- "The total gas consumption recorded by this meter at the beginning of this month.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Volume,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::Volume)
- .set(StorageNr(1))
- .add(VIFCombinable::UncorrectedMeterUnit)
- );
-
- addStringFieldWithExtractor(
- "version",
- "Model version.",
- DEFAULT_PRINT_PROPERTIES,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::ModelVersion)
- );
-
- addStringFieldWithExtractor(
- "supplier_info",
- "Supplier info?",
- DEFAULT_PRINT_PROPERTIES,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::SpecialSupplierInformation)
- );
-
- addStringFieldWithExtractor(
- "parameter_set",
- "Meter configued with this parameter set?",
- DEFAULT_PRINT_PROPERTIES,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::ParameterSet)
- );
-
- addStringFieldWithExtractor(
- "meter_timestamp",
- "Timestamp when this measurement was sent.",
- DEFAULT_PRINT_PROPERTIES,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::DateTime)
- .set(IndexNr(2))
- );
-
- }
-}
-
-// Test: GasMeter unismart 00043094 00000000000000000000000000000000
-// telegram=|6044B8059430040001037A1D005085E2B670BCF1A5C87E0C1A51DA18924EF984613DA2A9CD39D8F4C7208326C76D42DBEADF80D574192B71BD7C4F56A7F1513151768A9DB804883B28CB085CA2D0F7438C361CB9E2734712ED9BFBB2A14EF55208|
-// {"_":"telegram","media":"gas","meter":"unismart","name":"GasMeter","id":"00043094","fabrication_no":"03162296","status":"STATUS_FLAGS_CF0","other":"OTHER_FLAGS_14","total_date_time":"2021-09-15 13:18","total_m3":917,"target_date_time":"2021-09-01 06:00","target_m3":911.32,"version":" 4GGU","supplier_info":"00","parameter_set":"02","meter_timestamp":"2021-09-15 13:18:30","timestamp":"1111-11-11T11:11:11Z"}
-// |GasMeter;00043094;917;911.32;1111-11-11 11:11.11
diff --git a/components/wmbus_common/driver_unknown.cpp b/components/wmbus_common/driver_unknown.cpp
deleted file mode 100644
index febe2c52..00000000
--- a/components/wmbus_common/driver_unknown.cpp
+++ /dev/null
@@ -1,42 +0,0 @@
-/*
- Copyright (C) 2021 Fredrik Öhrström (gpl-3.0-or-later)
-
- 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 .
-*/
-
-#include"meters_common_implementation.h"
-
-struct MeterUnknown : public virtual MeterCommonImplementation
-{
- MeterUnknown(MeterInfo &mi, DriverInfo &di);
-};
-
-static bool ok = staticRegisterDriver([](DriverInfo&di)
-{
- di.setName("unknown");
- di.setDefaultFields("name,id,timestamp");
- di.setMeterType(MeterType::UnknownMeter);
- di.setConstructor([](MeterInfo& mi, DriverInfo& di){ return shared_ptr(new MeterUnknown(mi, di)); });
-});
-
-MeterUnknown::MeterUnknown(MeterInfo &mi, DriverInfo &di) : MeterCommonImplementation(mi, di)
-{
-}
-
-shared_ptr createUnknown(MeterInfo &mi)
-{
- DriverInfo di;
- di.setName("unknown");
- return shared_ptr(new MeterUnknown(mi, di));
-}
diff --git a/components/wmbus_common/driver_vario411.cpp b/components/wmbus_common/driver_vario411.cpp
deleted file mode 100644
index a7ebe337..00000000
--- a/components/wmbus_common/driver_vario411.cpp
+++ /dev/null
@@ -1,72 +0,0 @@
-/*
- Copyright (C) 2022 Fredrik Öhrström (gpl-3.0-or-later)
-
- 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 .
-*/
-
-#include"meters_common_implementation.h"
-
-namespace
-{
- struct Driver : public virtual MeterCommonImplementation
- {
- Driver(MeterInfo &mi, DriverInfo &di);
- };
-
- static bool ok = staticRegisterDriver([](DriverInfo&di)
- {
- di.setName("vario411");
- di.setDefaultFields("name,id,target_kwh,target_date,timestamp");
- di.setMeterType(MeterType::HeatMeter);
- di.addLinkMode(LinkMode::C1);
- di.addLinkMode(LinkMode::T1);
- di.addMVT(MANUFACTURER_TCH, 0x04, 0x28);
- di.setConstructor([](MeterInfo& mi, DriverInfo& di){ return shared_ptr(new Driver(mi, di)); });
- });
-
- Driver::Driver(MeterInfo &mi, DriverInfo &di) : MeterCommonImplementation(mi, di)
- {
- setExpectedTPLSecurityMode(TPLSecurityMode::AES_CBC_NO_IV);
-
- addNumericFieldWithExtractor(
- "target",
- "Total energy consumption at the end of the year",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Energy,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyEnergyVIF)
- .set(StorageNr(1))
- );
-
- addNumericFieldWithExtractor(
- "target",
- "Date when previous year ended.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::PointInTime,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::Date)
- .set(StorageNr(1)),
- Unit::DateLT
- );
- }
-}
-
-// Test: Howdy vario411 67627875 NOKEY
-// telegram=|624468507578626728048C00F3900F002C25FEEB0600BA84134D9202A1327AFF003007102F2F_4406E1190000426CBF2C0F206730E2E7516874F5DB46B5A97816F575A29A1EA2717D6ADE5C2FE64517ED2B0497EE0FF64C2674CD0832572C484DDFED30|
-// {"_":"telegram","id": "67627875","media": "heat","meter": "vario411","name": "Howdy","target_date": "2021-12-31","target_kwh": 6625,"timestamp":"1111-11-11T11:11:11Z"}
-// |Howdy;67627875;6625;2021-12-31;1111-11-11 11:11.11
diff --git a/components/wmbus_common/driver_vario451.cpp b/components/wmbus_common/driver_vario451.cpp
deleted file mode 100644
index 34222b17..00000000
--- a/components/wmbus_common/driver_vario451.cpp
+++ /dev/null
@@ -1,104 +0,0 @@
-/*
- Copyright (C) 2019-2022 Fredrik Öhrström (gpl-3.0-or-later)
-
- 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 .
-*/
-
-#include"meters_common_implementation.h"
-
-namespace
-{
- struct Driver : public virtual MeterCommonImplementation
- {
- Driver(MeterInfo &mi, DriverInfo &di);
-
- void processContent(Telegram *t);
- };
-
- static bool ok = staticRegisterDriver([](DriverInfo&di)
- {
- di.setName("vario451");
- di.setDefaultFields("name,id,total_kwh,current_kwh,previous_kwh,timestamp");
- di.setMeterType(MeterType::WaterMeter);
- di.addLinkMode(LinkMode::C1);
- di.addLinkMode(LinkMode::T1);
- di.addMVT(MANUFACTURER_TCH, 0x04, 0x27);
- di.addMVT(MANUFACTURER_TCH, 0xc3, 0x27);
- di.usesProcessContent();
- di.setConstructor([](MeterInfo& mi, DriverInfo& di){ return shared_ptr(new Driver(mi, di)); });
- });
-
- Driver::Driver(MeterInfo &mi, DriverInfo &di) : MeterCommonImplementation(mi, di)
- {
- addNumericField("total",
- Quantity::Energy,
- DEFAULT_PRINT_PROPERTIES,
- "The total energy consumption recorded by this meter.");
-
- addNumericField("current",
- Quantity::Energy,
- DEFAULT_PRINT_PROPERTIES,
- "Energy consumption so far in this billing period.");
-
- addNumericField("previous",
- Quantity::Energy,
- DEFAULT_PRINT_PROPERTIES,
- "Energy consumption in previous billing period.");
- }
-
- void Driver::processContent(Telegram *t)
- {
- // Unfortunately, the Techem Vario 4 Typ 4.5.1 is mostly a proprieatary protocol
- // simple wrapped inside a wmbus telegram since the ci-field is 0xa2.
- // Which means that the entire payload is manufacturer specific.
-
- std::map> vendor_values;
- std::vector content;
-
- t->extractPayload(&content);
- if (content.size() < 9) return;
-
- uchar prev_lo = content[3];
- uchar prev_hi = content[4];
- double prev_gj = (256.0*prev_hi+prev_lo)/1000;
-
- std::string prevs;
- strprintf(&prevs, "%02x%02x", prev_lo, prev_hi);
- int offset = t->parsed.size()+3;
- vendor_values["0215"] = { offset, DVEntry(offset, DifVifKey("0215"), MeasurementType::Instantaneous, 0x15, {}, {}, 0, 0, 0, prevs) };
- t->explanations.push_back(Explanation(offset, 2, prevs, KindOfData::CONTENT, Understanding::FULL));
- t->addMoreExplanation(offset, " energy used in previous billing period (%f GJ)", prev_gj);
-
- uchar curr_lo = content[7];
- uchar curr_hi = content[8];
- double curr_gj = (256.0*curr_hi+curr_lo)/1000;
-
- std::string currs;
- strprintf(&currs, "%02x%02x", curr_lo, curr_hi);
- offset = t->parsed.size()+7;
- vendor_values["0215"] = { offset, DVEntry(offset, DifVifKey("0215"), MeasurementType::Instantaneous, 0x15, {}, {}, 0, 0, 0, currs) };
- t->explanations.push_back(Explanation(offset, 2, currs, KindOfData::CONTENT, Understanding::FULL));
- t->addMoreExplanation(offset, " energy used in current billing period (%f GJ)", curr_gj);
-
- setNumericValue("total", Unit::GJ, curr_gj+prev_gj);
- setNumericValue("current", Unit::GJ, curr_gj);
- setNumericValue("previous", Unit::GJ, prev_gj);
- }
-
-}
-
-// Test: HeatMeter vario451 58234965 NOKEY
-// telegram=|374468506549235827C3A2_129F25383300A8622600008200800A2AF862115175552877A36F26C9AB1CB24400000004000000000004908002|
-// {"_":"telegram","media":"heat","meter":"vario451","name":"HeatMeter","id":"58234965","total_kwh":6371.666667,"current_kwh":2729.444444,"previous_kwh":3642.222222,"timestamp":"1111-11-11T11:11:11Z"}
-// |HeatMeter;58234965;6371.666667;2729.444444;3642.222222;1111-11-11 11:11.11
diff --git a/components/wmbus_common/driver_vario451mid.cpp b/components/wmbus_common/driver_vario451mid.cpp
deleted file mode 100644
index 112ebcfc..00000000
--- a/components/wmbus_common/driver_vario451mid.cpp
+++ /dev/null
@@ -1,104 +0,0 @@
-/*
- Copyright (C) 2022 Fredrik Öhrström (gpl-3.0-or-later)
-
- 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 .
-*/
-
-#include"meters_common_implementation.h"
-
-namespace
-{
- struct Driver : public virtual MeterCommonImplementation
- {
- Driver(MeterInfo &mi, DriverInfo &di);
- };
-
- static bool ok = staticRegisterDriver([](DriverInfo&di)
- {
- di.setName("vario451mid");
- di.setDefaultFields("name,id,total_energy_consumption_kwh,energy_at_old_date_kwh,energy_at_set_date_kwh,timestamp");
- di.setMeterType(MeterType::HeatMeter);
- // Mode 7
- di.addLinkMode(LinkMode::T1);
- di.addMVT(MANUFACTURER_TCH, 0x04, 0x17);
- di.setConstructor([](MeterInfo& mi, DriverInfo& di){ return shared_ptr(new Driver(mi, di)); });
- });
-
- Driver::Driver(MeterInfo &mi, DriverInfo &di) : MeterCommonImplementation(mi, di)
- {
- addNumericFieldWithExtractor(
- "total_energy_consumption",
- "The total energy consumption recorded by this meter.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Energy,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyEnergyVIF)
- );
-
- addNumericFieldWithExtractor(
- "energy_at_old_date",
- "The total energy consumption recorded when?",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Energy,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyEnergyVIF)
- .set(StorageNr(1))
- );
-
- addNumericFieldWithExtractor(
- "old",
- "The last billing old date?",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::PointInTime,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::Date)
- .set(StorageNr(1)),
- Unit::DateLT);
-
- addNumericFieldWithExtractor(
- "energy_at_set_date",
- "The total energy consumption recorded by this meter at the due date.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Energy,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyEnergyVIF)
- .set(StorageNr(8))
- );
-
- addNumericFieldWithExtractor(
- "set",
- "The last billing set date.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::PointInTime,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::Date)
- .set(StorageNr(8)),
- Unit::DateLT);
- }
-}
-
-// Test: Heato vario451mid 94430412 NOKEY
-// telegram=734468501204439417048c0084900f002c2536700000B767B64527c50ac67a33005007102f2f8404062846000082046c9f2c8d04861f1e72fe00000000000000000000000000000000000000000000000000000000440600000000426cffff0406c94700002f2f2f2f2f2f2f2f2f2f2f2f2f2f2f
-// {"_":"telegram","media":"heat","meter":"vario451mid","name":"Heato","id":"94430412","total_energy_consumption_kwh":18377,"energy_at_old_date_kwh":0,"old_date":"2128-03-31","energy_at_set_date_kwh":17960,"set_date":"2020-12-31","timestamp":"1111-11-11T11:11:11Z"}
-// |Heato;94430412;18377;0;17960;1111-11-11 11:11.11
diff --git a/components/wmbus_common/driver_waterstarm.cpp b/components/wmbus_common/driver_waterstarm.cpp
deleted file mode 100644
index 295d1455..00000000
--- a/components/wmbus_common/driver_waterstarm.cpp
+++ /dev/null
@@ -1,268 +0,0 @@
-/*
- Copyright (C) 2020-2022 Fredrik Öhrström (gpl-3.0-or-later)
-
- 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 .
-*/
-
-#include"meters_common_implementation.h"
-
-namespace
-{
- struct Driver : public virtual MeterCommonImplementation
- {
- Driver(MeterInfo &mi, DriverInfo &di);
- };
-
- static bool ok = staticRegisterDriver([](DriverInfo&di)
- {
- di.setName("waterstarm");
- di.setDefaultFields("name,id,total_m3,total_backwards_m3,status,timestamp");
- di.setMeterType(MeterType::WaterMeter);
- di.addLinkMode(LinkMode::T1);
- di.addLinkMode(LinkMode::C1);
- di.addMVT(MANUFACTURER_DWZ, 0x06, 0x00); // warm water
- di.addMVT(MANUFACTURER_DWZ, 0x06, 0x02); // warm water
- di.addMVT(MANUFACTURER_DWZ, 0x07, 0x02);
- di.addMVT(MANUFACTURER_EFE, 0x07, 0x03);
- di.addMVT(MANUFACTURER_EFE, 0x07, 0x70);
- di.addMVT(MANUFACTURER_DWZ, 0x07, 0x00); // water meter
-
- di.setConstructor([](MeterInfo& mi, DriverInfo& di){ return shared_ptr(new Driver(mi, di)); });
- });
-
- Driver::Driver(MeterInfo &mi, DriverInfo &di) : MeterCommonImplementation(mi, di)
- {
- addStringFieldWithExtractorAndLookup(
- "status",
- "Status and error flags.",
- DEFAULT_PRINT_PROPERTIES | PrintProperty::INCLUDE_TPL_STATUS | PrintProperty::STATUS,
- FieldMatcher::build()
- .set(VIFRange::ErrorFlags),
- {
- {
- {
- "ERROR_FLAGS",
- Translate::MapType::BitToString,
- AlwaysTrigger, MaskBits(0xffff),
- "OK",
- {
- { 0x01, "SW_ERROR" },
- { 0x02, "CRC_ERROR" },
- { 0x04, "SENSOR_ERROR" },
- { 0x08, "MEASUREMENT_ERROR" },
- { 0x10, "BATTERY_VOLTAGE_ERROR" },
- { 0x20, "MANIPULATION" },
- { 0x40, "LEAKAGE_OR_NO_USAGE" },
- { 0x80, "REVERSE_FLOW" },
- { 0x100, "OVERLOAD" },
- }
- },
- },
- });
-
- addNumericFieldWithExtractor(
- "meter",
- "Device date time.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::PointInTime,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::DateTime)
- );
-
- addNumericFieldWithExtractor(
- "total",
- "The total water consumption recorded by this meter.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Volume,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::Volume)
- );
-
- addNumericFieldWithExtractor(
- "total_backwards",
- "The total backward water volume recorded by this meter.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Volume,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::AnyVolumeVIF)
- .add(VIFCombinable::BackwardFlow)
- );
-
- addStringFieldWithExtractorAndLookup(
- "current_status",
- "Status and error flags. (Deprecated use status instead.)",
- DEFAULT_PRINT_PROPERTIES | PrintProperty::INCLUDE_TPL_STATUS | PrintProperty::STATUS | PrintProperty::DEPRECATED,
- FieldMatcher::build()
- .set(VIFRange::ErrorFlags),
- {
- {
- {
- "ERROR_FLAGS",
- Translate::MapType::BitToString,
- AlwaysTrigger, MaskBits(0xffff),
- "OK",
- {
- { 0x01, "SW_ERROR" },
- { 0x02, "CRC_ERROR" },
- { 0x04, "SENSOR_ERROR" },
- { 0x08, "MEASUREMENT_ERROR" },
- { 0x10, "BATTERY_VOLTAGE_ERROR" },
- { 0x20, "MANIPULATION" },
- { 0x40, "LEAKAGE_OR_NO_USAGE" },
- { 0x80, "REVERSE_FLOW" },
- { 0x100, "OVERLOAD" },
- }
- },
- },
- });
-
- addStringFieldWithExtractor(
- "meter_version",
- "Meter model/version.",
- DEFAULT_PRINT_PROPERTIES,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::ModelVersion)
- );
-
- addStringFieldWithExtractor(
- "parameter_set",
- "Parameter set.",
- DEFAULT_PRINT_PROPERTIES,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::ParameterSet)
- );
-
- addNumericFieldWithExtractor(
- "battery",
- "The battery voltage.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Voltage,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::Voltage)
- );
-
- addNumericFieldWithExtractor(
- "set",
- "The most recent billing period date.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::PointInTime,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::Date)
- .set(StorageNr(1)),
- Unit::DateLT
- );
-
- addNumericFieldWithExtractor(
- "consumption_at_set_date",
- "The total water consumption at the most recent billing period date.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Volume,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::Volume)
- .set(StorageNr(1))
- );
-
- addNumericFieldWithExtractor(
- "total_backwards_at_set_date",
- "The total media volume that has flowed backwards at the end of the previous billing period.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Volume,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::Volume)
- .set(StorageNr(1))
- .add(VIFCombinable::BackwardFlow)
- );
-
- addNumericFieldWithExtractor(
- "consumption_at_history_{storage_counter - 1 counter}",
- "The total water consumption at the historic date.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Volume,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::Volume)
- .set(StorageNr(2),StorageNr(16))
- );
-
- addNumericFieldWithCalculatorAndMatcher(
- "history_{storage_counter - 1 counter}",
- "The historic date.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::PointInTime,
- "meter_datetime - ((storage_counter-1counter) * 1 month)",
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::Volume)
- .set(StorageNr(2),StorageNr(16)),
- Unit::DateLT
- );
- }
-}
-
-// Test: Woter waterstarm 20096221 BEDB81B52C29B5C143388CBB0D15A051
-// telegram=|3944FA122162092002067A3600202567C94D48D00DC47B11213E23383DB51968A705AAFA60C60E263D50CD259D7C9A03FD0C08000002FD0B0011|
-// {"_":"telegram","media":"warm water","meter":"waterstarm","name":"Woter","id":"20096221","meter_datetime":"2020-07-30 10:40","total_m3":0.106,"total_backwards_m3":0,"current_status":"OK","status":"OK","meter_version":"000008","parameter_set":"1100","timestamp":"1111-11-11T11:11:11Z"}
-// |Woter;20096221;0.106;0;OK;1111-11-11 11:11.11
-
-// telegram=|3944FA122162092002067A3604202567C94D48D00DC47B11213E23383DB51968A705AAFA60C60E263D50CD259D7C9A03FD0C08000002FD0B0011|
-// {"_":"telegram","media":"warm water","meter":"waterstarm","name":"Woter","id":"20096221","meter_datetime":"2020-07-30 10:40","total_m3":0.106,"total_backwards_m3":0,"current_status":"POWER_LOW","status":"POWER_LOW","meter_version":"000008","parameter_set":"1100","timestamp":"1111-11-11T11:11:11Z"}
-// |Woter;20096221;0.106;0;POWER_LOW;1111-11-11 11:11.11
-
-// Test: Water waterstarm 22996221 NOKEY
-// telegram=|3944FA122162992202067A360420252F2F_046D282A9E2704136A00000002FD17400004933C000000002F2F2F2F2F2F03FD0C08000002FD0B0011|
-// {"_":"telegram","media":"warm water","meter":"waterstarm","name":"Water","id":"22996221","meter_datetime":"2020-07-30 10:40","total_m3":0.106,"total_backwards_m3":0,"current_status":"LEAKAGE_OR_NO_USAGE POWER_LOW","status":"LEAKAGE_OR_NO_USAGE POWER_LOW","meter_version":"000008","parameter_set":"1100","timestamp":"1111-11-11T11:11:11Z"}
-// |Water;22996221;0.106;0;LEAKAGE_OR_NO_USAGE POWER_LOW;1111-11-11 11:11.11
-
-
-// Test: Water waterstarm 11559999 NOKEY
-// telegram=|2E44FA129999551100077A070020252F2F_046D0F28C22404139540000002FD17000001FD481D2F2F2F2F2F2F2F2F2F|
-// {"_":"telegram","media":"water","meter":"waterstarm","name":"Water","id":"11559999","meter_datetime":"2022-04-02 08:15","total_m3":16.533,"current_status":"OK","status":"OK","battery_v":2.9,"timestamp":"1111-11-11T11:11:11Z"}
-// |Water;11559999;16.533;null;OK;1111-11-11 11:11.11
-
-// Test: WarmLorenz waterstarm 20050666 NOKEY
-// telegram=|9644FA126606052000067A1E000020_046D3B2ED729041340D8000002FD17000001FD481D426CBF2C4413026C000084011348D20000C40113F3CB0000840213DCC40000C40213B8B60000840313849B0000C403138B8C0000840413E3800000C4041337770000840513026C0000C40513D65F00008406134F560000C40613604700008407139D370000C407137F3300008408135B2C0000|
-// {"_":"telegram","battery_v":2.9,"consumption_at_history_10_m3":24.534,"consumption_at_history_11_m3":22.095,"consumption_at_history_12_m3":18.272,"consumption_at_history_13_m3":14.237,"consumption_at_history_14_m3":13.183,"consumption_at_history_15_m3":11.355,"consumption_at_history_1_m3":53.832,"consumption_at_history_2_m3":52.211,"consumption_at_history_3_m3":50.396,"consumption_at_history_4_m3":46.776,"consumption_at_history_5_m3":39.812,"consumption_at_history_6_m3":35.979,"consumption_at_history_7_m3":32.995,"consumption_at_history_8_m3":30.519,"consumption_at_history_9_m3":27.65,"consumption_at_set_date_m3":27.65,"current_status":"OK","status":"OK","history_10_date":"2021-11-23","history_11_date":"2021-10-23","history_12_date":"2021-09-23","history_13_date":"2021-08-23","history_14_date":"2021-07-23","history_15_date":"2021-06-23","history_1_date":"2022-08-23","history_2_date":"2022-07-23","history_3_date":"2022-06-23","history_4_date":"2022-05-23","history_5_date":"2022-04-23","history_6_date":"2022-03-23","history_7_date":"2022-02-23","history_8_date":"2022-01-23","history_9_date":"2021-12-23","id":"20050666","media":"warm water","meter":"waterstarm","meter_datetime":"2022-09-23 14:59","name":"WarmLorenz","set_date":"2021-12-31","timestamp":"1111-11-11T11:11:11Z","total_m3":55.36}
-// |WarmLorenz;20050666;55.36;null;OK;1111-11-11 11:11.11
-
-// Test: ColdLorenz waterstarm 20065160 NOKEY
-// telegram=|9644FA126051062000077A78000020_046D392DD7290413901A000002FD17000001FD481D426CBF2C4413D312000084011399190000C40113841800008402130C180000C40213EC16000084031395150000C40313E3140000840413BD130000C404134C130000840513D3120000C4051322120000840613AF110000C4061397100000840713D00F0000C40713890E0000840813980C0000|
-// {"_":"telegram","battery_v":2.9,"consumption_at_history_10_m3":4.642,"consumption_at_history_11_m3":4.527,"consumption_at_history_12_m3":4.247,"consumption_at_history_13_m3":4.048,"consumption_at_history_14_m3":3.721,"consumption_at_history_15_m3":3.224,"consumption_at_history_1_m3":6.553,"consumption_at_history_2_m3":6.276,"consumption_at_history_3_m3":6.156,"consumption_at_history_4_m3":5.868,"consumption_at_history_5_m3":5.525,"consumption_at_history_6_m3":5.347,"consumption_at_history_7_m3":5.053,"consumption_at_history_8_m3":4.94,"consumption_at_history_9_m3":4.819,"consumption_at_set_date_m3":4.819,"current_status":"OK","status":"OK","history_10_date":"2021-11-23","history_11_date":"2021-10-23","history_12_date":"2021-09-23","history_13_date":"2021-08-23","history_14_date":"2021-07-23","history_15_date":"2021-06-23","history_1_date":"2022-08-23","history_2_date":"2022-07-23","history_3_date":"2022-06-23","history_4_date":"2022-05-23","history_5_date":"2022-04-23","history_6_date":"2022-03-23","history_7_date":"2022-02-23","history_8_date":"2022-01-23","history_9_date":"2021-12-23","id":"20065160","media":"water","meter":"waterstarm","meter_datetime":"2022-09-23 13:57","name":"ColdLorenz","set_date":"2021-12-31","timestamp":"1111-11-11T11:11:11Z","total_m3":6.8}
-// |ColdLorenz;20065160;6.8;null;OK;1111-11-11 11:11.11
-
-// Test: water waterstarm 50496629 FFEEDDCCBBAA00998877665544332211
-// telegram=|a144c5142966495070078c20d07a0d0090257db8bb9938a1ff7c4b9a4492f4ea5f278b725057eee6837c17397e9605f3a448a338bd8407a2a5846632c334dff577f427f054f55e68a00fa5c85ccbc808ccdd2bb537a83234a50392968f00c1d7473455f9fc4c88fb195ca1712325da8c6aa7fdb7c4b77b9b84a4a0ccac4f586775fa66007dbdc2c615b69247401e9b9a863ac5a5873484b1b7d0178198e88f701e53|
-// {"_": "telegram","consumption_at_history_10_m3": -0.001,"consumption_at_history_11_m3": -0.001,"consumption_at_history_12_m3": -0.001,"consumption_at_history_13_m3": -0.001,"consumption_at_history_14_m3": -0.001,"consumption_at_history_15_m3": -0.001,"consumption_at_history_1_m3": 0.003,"consumption_at_history_2_m3": 0.003,"consumption_at_history_3_m3": 0.003,"consumption_at_history_4_m3": 0.002,"consumption_at_history_5_m3": 0.002,"consumption_at_history_6_m3": 0,"consumption_at_history_7_m3": 0,"consumption_at_history_8_m3": -0.001,"consumption_at_history_9_m3": -0.001,"consumption_at_set_date_m3": 0,"current_status": "OK","history_10_date": "2024-10-20","history_11_date": "2024-09-20","history_12_date": "2024-08-20","history_13_date": "2024-07-20","history_14_date": "2024-06-20","history_15_date": "2024-05-20","history_1_date": "2025-07-20","history_2_date": "2025-06-20","history_3_date": "2025-05-20","history_4_date": "2025-04-20","history_5_date": "2025-03-20","history_6_date": "2025-02-20","history_7_date": "2025-01-20","history_8_date": "2024-12-20","history_9_date": "2024-11-20","id": "50496629","media": "water","meter": "waterstarm","meter_datetime": "2025-08-20 14:51","name": "water","set_date": "2128-03-31","status": "OK","timestamp": "1111-11-11T11:11:11Z","total_backwards_at_set_date_m3": 0,"total_m3": 0.003}
-// |water;50496629;0.003;null;OK;1111-11-11 11:11.11
-
-// Test: water2 waterstarm 51398765 NOKEY
-// telegram=|A144C5146587395170078C20947AD10090252F2F_046D862D4A3104131300000001FD1700426C3F3C44130000000044933C0000000084011300000000C401130100000084021300000000C402130000000084031300000000C4031300000000840413FFFFFFFFC40413FFFFFFFF840513FFFFFFFFC40513FFFFFFFF840613FFFFFFFFC40613FFFFFFFF840713FFFFFFFFC40713FFFFFFFF840813FFFFFFFF2F2F2F2F|
-// {"_": "telegram","consumption_at_history_10_m3": -0.001,"consumption_at_history_11_m3": -0.001,"consumption_at_history_12_m3": -0.001,"consumption_at_history_13_m3": -0.001,"consumption_at_history_14_m3": -0.001,"consumption_at_history_15_m3": -0.001,"consumption_at_history_1_m3": 0,"consumption_at_history_2_m3": 0.001,"consumption_at_history_3_m3": 0,"consumption_at_history_4_m3": 0,"consumption_at_history_5_m3": 0,"consumption_at_history_6_m3": 0,"consumption_at_history_7_m3": -0.001,"consumption_at_history_8_m3": -0.001,"consumption_at_history_9_m3": -0.001,"consumption_at_set_date_m3": 0,"current_status": "OK","history_10_date": "2025-03-10","history_11_date": "2025-02-10","history_12_date": "2025-01-10","history_13_date": "2024-12-10","history_14_date": "2024-11-10","history_15_date": "2024-10-10","history_1_date": "2025-12-10","history_2_date": "2025-11-10","history_3_date": "2025-10-10","history_4_date": "2025-09-10","history_5_date": "2025-08-10","history_6_date": "2025-07-10","history_7_date": "2025-06-10","history_8_date": "2025-05-10","history_9_date": "2025-04-10","id": "51398765","media": "water","meter": "waterstarm","meter_datetime": "2026-01-10 13:06","name": "water2","set_date": "2025-12-31","status": "OK","timestamp": "1111-11-11T11:11:11Z","total_backwards_at_set_date_m3": 0,"total_m3": 0.019}
-// |water2;51398765;0.019;null;OK;1111-11-11 11:11.11
diff --git a/components/wmbus_common/driver_watertech.cpp b/components/wmbus_common/driver_watertech.cpp
deleted file mode 100644
index e5e8fae7..00000000
--- a/components/wmbus_common/driver_watertech.cpp
+++ /dev/null
@@ -1,76 +0,0 @@
-/*
- Copyright (C) 2020-2023 Fredrik Öhrström (gpl-3.0-or-later)
-
- 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 .
-*/
-
-#include"meters_common_implementation.h"
-
-namespace
-{
- struct Driver : public virtual MeterCommonImplementation
- {
- Driver(MeterInfo &mi, DriverInfo &di);
- };
-
- static bool ok = staticRegisterDriver([](DriverInfo&di)
- {
- di.setName("watertech");
- di.setDefaultFields("name,id,status,total_m3,timestamp");
- di.setMeterType(MeterType::WaterMeter);
- di.addLinkMode(LinkMode::T1);
- di.addMVT(MANUFACTURER_WTT, 0x07, 0x59);
- di.setConstructor([](MeterInfo& mi, DriverInfo& di){ return shared_ptr(new Driver(mi, di)); });
- });
-
- Driver::Driver(MeterInfo &mi, DriverInfo &di) : MeterCommonImplementation(mi, di)
- {
- addOptionalLibraryFields("software_version,meter_datetime");
-
- addStringFieldWithExtractorAndLookup(
- "status",
- "Status and error flags.",
- DEFAULT_PRINT_PROPERTIES | PrintProperty::INCLUDE_TPL_STATUS | PrintProperty::STATUS,
- FieldMatcher::build()
- .set(VIFRange::ErrorFlags),
- {
- {
- {
- "ERROR_FLAGS",
- Translate::MapType::BitToString,
- AlwaysTrigger, MaskBits(0xffff),
- "OK",
- {
- }
- },
- },
- });
-
- addNumericFieldWithExtractor(
- "total",
- "The total water consumption recorded by this meter.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Volume,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::Volume)
- );
- }
-}
-
-// Test: wtt watertech 38383838 NOKEY
-// telegram=|3144945E3838383859078C20007A010020252F2F_066D1C1F0EF021000413B91E000002FD17000002FD0F4C2B2F2F2F2F2F2F|
-// {"_":"telegram","media": "water","meter": "watertech","name": "wtt","id": "38383838","software_version" :"+L","meter_datetime": "2023-01-16 14:31:28","status": "OK","total_m3": 7.865, "timestamp":"1111-11-11T11:11:11Z"}
-// |wtt;38383838;OK;7.865;1111-11-11 11:11.11
diff --git a/components/wmbus_common/driver_weh_07.cpp b/components/wmbus_common/driver_weh_07.cpp
deleted file mode 100644
index 155b8d92..00000000
--- a/components/wmbus_common/driver_weh_07.cpp
+++ /dev/null
@@ -1,89 +0,0 @@
-/*
- Copyright (C) 2021-2022 Fredrik Öhrström (gpl-3.0-or-later)
-
- 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 .
-*/
-
-#include"meters_common_implementation.h"
-
-namespace
-{
- struct Driver : public virtual MeterCommonImplementation
- {
- Driver(MeterInfo &mi, DriverInfo &di);
- };
-
- static bool ok = staticRegisterDriver([](DriverInfo&di)
- {
- di.setName("weh_07");
- di.setDefaultFields("name,id,total_m3,timestamp");
- di.setMeterType(MeterType::WaterMeter);
- di.addLinkMode(LinkMode::C1);
- di.addMVT(MANUFACTURER_WEH, 0x07, 0xfe);
- di.addMVT(MANUFACTURER_WEH, 0x07, 0x03);
-
- di.setConstructor([](MeterInfo& mi, DriverInfo& di){ return shared_ptr(new Driver(mi, di)); });
- });
-
- Driver::Driver(MeterInfo &mi, DriverInfo &di) : MeterCommonImplementation(mi, di)
- {
- addNumericFieldWithExtractor(
- "total",
- "The total water consumption recorded by this meter.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Volume,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::Volume)
- );
-
- addNumericFieldWithExtractor(
- "target",
- "The most recent billing period date.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::PointInTime,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::Date)
- .set(StorageNr(1)),
- Unit::DateLT
- );
-
- addNumericFieldWithExtractor(
- "target",
- "The total water consumption at the most recent billing period date.",
- DEFAULT_PRINT_PROPERTIES,
- Quantity::Volume,
- VifScaling::Auto, DifSignedness::Signed,
- FieldMatcher::build()
- .set(MeasurementType::Instantaneous)
- .set(VIFRange::Volume)
- .set(StorageNr(1))
- );
- }
-}
-
-// Test: Vatten weh_07 86868686 NOKEY
-// Comment: Techem radio convert + Wehrle water meter combo.
-// telegram=|494468509494949495377286868686A85CFE07A90030052F2F_0413100000000F52FCF6A52A90A8D83CA8F7FEAE86990502323D0C70EFF49833C7C1696F75BCABC1E52E6305308D0F31FB|
-// {"_":"telegram","media":"water","meter":"weh_07","name":"Vatten","id":"86868686","total_m3":0.016,"timestamp":"1111-11-11T11:11:11Z"}
-// |Vatten;86868686;0.016;1111-11-11 11:11.11
-
-// Test: Vattenn weh_07 27604781 NOKEY
-// Comment: A normal water meter.
-// telegram=|5244A85C8147602703077A5B0840252F2F_0413B39100004413000000004D931E2C73FE0000000000000000000000000000000000000000000000000000000000000000000000009885001A0C002F2F426CBE29|
-// {"_":"telegram","id": "27604781","media": "water","meter": "weh_07","name": "Vattenn","target_date": "2021-09-30","target_m3": 0,"timestamp": "1111-11-11T11:11:11Z","total_m3": 37.299 }
-// |Vattenn;27604781;37.299;1111-11-11 11:11.11
diff --git a/components/wmbus_common/driver_wme5.cpp b/components/wmbus_common/driver_wme5.cpp
deleted file mode 100644
index 16f8fa97..00000000
--- a/components/wmbus_common/driver_wme5.cpp
+++ /dev/null
@@ -1,84 +0,0 @@
-/*
- Copyright (C) 2023 Fredrik Öhrström (gpl-3.0-or-later)
-
- 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 .
-*/
-
-#include"meters_common_implementation.h"
-
-namespace
-{
- struct Driver : public virtual MeterCommonImplementation
- {
- Driver(MeterInfo &mi, DriverInfo &di);
- void processContent(Telegram *t);
- };
-
- static bool ok = staticRegisterDriver([](DriverInfo&di)
- {
- di.setName("wme5");
- di.setDefaultFields("name,id,total_m3,total_hex,timestamp");
-
- di.setMeterType(MeterType::HeatMeter);
- di.addLinkMode(LinkMode::T1);
- // di.addMVT(MANUFACTURER_QDS, 0x07, 0x1a); // Hmmm, this mvt is better handled by QWater
- // too bad we have to example telegrams for wme5....
- di.usesProcessContent();
- di.setConstructor([](MeterInfo& mi, DriverInfo& di){ return shared_ptr(new Driver(mi, di)); });
- });
-
- Driver::Driver(MeterInfo &mi, DriverInfo &di) : MeterCommonImplementation(mi, di)
- {
- addOptionalLibraryFields("meter_datetime,model_version");
-
- addNumericField(
- "total",
- Quantity::Volume,
- DEFAULT_PRINT_PROPERTIES,
- "Perhaps the total water consumption recorded by this meter.");
-
- addStringField(
- "total_hex",
- "Perhaps the total but in hex?",
- DEFAULT_PRINT_PROPERTIES);
- }
-
- void Driver::processContent(Telegram *t)
- {
- std::string content;
- int offset;
- bool ok = extractDVHexString(&t->dv_entries,
- "0DFF5F",
- &offset,
- &content);
-
- if (!ok) return;
-
- // 00826100 _ 35AE6A130B8A8CF07C0C6F9EA35C8C5274671347D73DA9810CD664F2F9616388CE7B4835BD06D7E2253741F2667DC5D8C
- if (content.length() >= 8)
- {
- std::vector bytes;
- content = content.substr(0, 8);
- ok = hex2bin(content, &bytes);
- if (!ok) return;
- if (bytes.size() != 4) return;
-
- uint64_t total = bytes[0]<<24 | bytes[1] << 16 | bytes [2] << 8 | bytes[3];
- double totald = total;
- setNumericValue("total", Unit::M3, totald);
- setStringValue("total_hex", content);
- }
- }
-
-}
diff --git a/components/wmbus_common/driver_zenner0b.cpp b/components/wmbus_common/driver_zenner0b.cpp
deleted file mode 100644
index 3b92a94b..00000000
--- a/components/wmbus_common/driver_zenner0b.cpp
+++ /dev/null
@@ -1,127 +0,0 @@
-/*
- Copyright (C) 2025 Fredrik Öhrström (gpl-3.0-or-later)
-
- 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 .
-*/
-
-#include"meters_common_implementation.h"
-
-namespace
-{
- struct Driver : public virtual MeterCommonImplementation
- {
- Driver(MeterInfo &mi, DriverInfo &di);
-
- void processContent(Telegram *t);
- };
-
- static bool ok = staticRegisterDriver([](DriverInfo&di)
- {
- di.setName("zenner0b");
- di.setDefaultFields("name,id,status,total_m3,target_m3,timestamp");
- di.setMeterType(MeterType::WaterMeter);
- di.addLinkMode(LinkMode::C1);
- di.addMVT(MANUFACTURER_ZRI, 0x16, 0x0b);
- di.usesProcessContent();
- di.setConstructor([](MeterInfo& mi, DriverInfo& di){ return shared_ptr(new Driver(mi, di)); });
- });
-
- Driver::Driver(MeterInfo &mi, DriverInfo &di) : MeterCommonImplementation(mi, di)
- {
- addNumericField(
- "total",
- Quantity::Volume,
- DEFAULT_PRINT_PROPERTIES,
- "The total water consumption recorded by this meter.",
- Unit::M3);
-
- addNumericField(
- "target",
- Quantity::Volume,
- DEFAULT_PRINT_PROPERTIES,
- "The current month water consumption.",
- Unit::M3);
-
- addStringField(
- "status",
- "Meter status flags (bytes 0-3 from manufacturer data).",
- DEFAULT_PRINT_PROPERTIES | PrintProperty::STATUS);
- }
-
- void Driver::processContent(Telegram *t)
- {
- // ZENNER EDC B.One wireless M-Bus module
- // Telegram scenario 322: Manufacturer specific data
- // The payload contains only a 0x0F marker followed by raw bytes:
- // bytes 0-3: status (currently always 0x00000000)
- // bytes 4-7: target consumption counter (little-endian, units of 1/256000 m³)
- // bytes 8-11: total consumption counter (little-endian, units of 1/256000 m³)
- // bytes 12: padding (0x00)
-
- vector bytes;
- t->extractMfctData(&bytes); // Extract raw frame data after the DIF 0x0F.
-
- if (bytes.size() < 12) return;
-
- int offset = t->header_size+t->mfct_0f_index; // This is where the mfct data starts in the telegram.
-
- // Extract total consumption (bytes 8-11, little-endian 32-bit)
- string total;
- strprintf(&total, "%02x%02x%02x%02x", bytes[8], bytes[9], bytes[10], bytes[11]);
-
- uint32_t total_raw = bytes[8] | (bytes[9] << 8) | (bytes[10] << 16) | (bytes[11] << 24);
- double total_m3 = total_raw / 256000.0;
-
- total = "*** "+total+" total consumption (%f m3)";
- t->addSpecialExplanation(offset+8, 4, KindOfData::CONTENT, Understanding::FULL, total.c_str(), total_m3);
-
- // Extract target consumption (bytes 4-7, little-endian 32-bit)
- string target;
- strprintf(&target, "%02x%02x%02x%02x", bytes[4], bytes[5], bytes[6], bytes[7]);
-
- uint32_t target_raw = bytes[4] | (bytes[5] << 8) | (bytes[6] << 16) | (bytes[7] << 24);
- double target_m3 = target_raw / 256000.0;
-
- target = "*** "+target+" target consumption (%f m3)";
- t->addSpecialExplanation(offset+4, 4, KindOfData::CONTENT, Understanding::FULL, target.c_str(), target_m3);
-
- // Extract status (bytes 0-3, little-endian 32-bit)
- string status;
- strprintf(&status, "%02x%02x%02x%02x", bytes[0], bytes[1], bytes[2], bytes[3]);
-
- uint32_t status_raw = bytes[0] | (bytes[1] << 8) | (bytes[2] << 16) | (bytes[3] << 24);
- string status_str;
- if (status_raw == 0)
- {
- status_str = "OK";
- }
- else
- {
- // Format as hex for now until we decode the bit meanings
- status_str = tostrprintf("0x%08X", status_raw);
- }
-
- status = "*** "+status+" status (%s)";
- t->addSpecialExplanation(offset+0, 4, KindOfData::CONTENT, Understanding::FULL, status.c_str(), status_str.c_str());
-
- setNumericValue("total", Unit::M3, total_m3);
- setNumericValue("target", Unit::M3, target_m3);
- setStringValue("status", status_str, NULL);
- }
-}
-
-// Test: TestWater zenner0b 50087367 NOKEY
-// telegram=|1E44496A677308500B167AD80010252F2F_0F_00000000_80BF1B00_00A64200_00|
-// {"_": "telegram","id": "50087367","media": "cold water","meter": "zenner0b","name": "TestWater","status": "OK","target_m3": 7.1035,"timestamp": "1111-11-11T11:11:11Z","total_m3": 17.062}
-// |TestWater;50087367;OK;17.062;7.1035;1111-11-11 11:11.11
diff --git a/components/wmbus_common/drivers/library.xmq b/components/wmbus_common/drivers/library.xmq
new file mode 100644
index 00000000..14f3dc53
--- /dev/null
+++ b/components/wmbus_common/drivers/library.xmq
@@ -0,0 +1,441 @@
+/* This is the library of fields that can be referenced from
+ a driver with library { use = total_m3 }
+
+ The library speeds up creating drivers for meters with mostly
+ standardized values. It also helps normalize the names of the fields.
+
+ A field name is short. For a single direction meter with only consumption
+ values. Use for example:
+ total_m3 (for water/gas consumption volume)
+ total_kwh (for energy consumption)
+
+ The total accumulated at the end of last month is usually called:
+ target_m3
+ target_kwh
+ These are useful for billing since the readout can happen at
+ any time during the next month.
+
+ For more complex meters with both consumption and production:
+ total_consumption_kwh (Electricity consumed.)
+ total_production_kwh (Electricity produced.)
+ target_consumption_kwh
+ target_production_kwh
+
+ There should almost always be a status field (with attribute=STATUS) that accumulates all error flags.
+ If the telegram includes a TPL status byte, then those flags (as seen below) are automatically
+ included the status field.
+
+ All flags that a driver uses must be declared either in the flags here in the library,
+ or in the driver flags entry. */
+library {
+ flags {
+ // The following flags are defined in the standard [EN 13757-7:2018 §7.5.6] for the TPL status byte.
+ // The error flag is defined in [EN 13757-7:2018 §7.5.6; EN 13757-3:2018 §10; OMS Vol.2 §8.8].
+ flag {
+ name = BUSY
+ info = 'Meter busy. (tpl sts)'
+ }
+ flag {
+ name = ERROR
+ info = 'Meter error; details via error reporting. (tpl sts)'
+ }
+ flag {
+ name = ALARM
+ info = 'Abnormal condition/alarm. (tpl sts)'
+ }
+ flag {
+ name = POWER_LOW
+ info = 'Power low. (tpl sts)'
+ }
+ flag {
+ name = PERMANENT_ERROR
+ info = 'Permanent meter error. (tpl sts)'
+ }
+ flag {
+ name = TEMPORARY_ERROR
+ info = 'Temporary meter error. (tpl sts)'
+ }
+ // The following flag is wmbusmeters specific.
+ flag {
+ name = FAILED_DECODE
+ info = 'The telegram failed a manufacturer specific decode or decryption.'
+ }
+ }
+ template(id = total_m3)
+ {
+ name = total
+ quantity = Volume
+ info = 'The total {media} volume recorded.'
+ change = Increasing
+ match {
+ measurement_type = Instantaneous
+ vif_range = Volume
+ }
+ }
+ template(id = total_kwh)
+ {
+ name = total
+ quantity = Energy
+ info = 'The total {media} energy recorded.'
+ change = Increasing
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyEnergyVIF
+ }
+ }
+ template(id = target_m3)
+ {
+ name = target
+ quantity = Volume
+ info = 'The {water} volume recorded at the last billing date.'
+ change = Increasing
+ match {
+ measurement_type = Instantaneous
+ vif_range = Volume
+ storage_nr = 1
+ }
+ }
+ template(id = target_kwh)
+ {
+ name = target
+ quantity = Energy
+ info = 'The {water} energy recorded at the last billing date.'
+ change = Increasing
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyEnergyVIF
+ storage_nr = 1
+ }
+ }
+ template(id = status)
+ {
+ name = status
+ // Kept working for old C++ drivers that use alias status-tpl-only
+ aliases = status-tpl-only
+ info = 'Status and error flags.'
+ attributes = STATUS,INCLUDE_TPL_STATUS
+ }
+ template(id = actuality_duration_s)
+ {
+ name = actuality_duration
+ quantity = Time
+ info = 'Lapsed time between measurement and transmission.'
+ display_unit = s
+ match {
+ measurement_type = Instantaneous
+ vif_range = ActualityDuration
+ }
+ }
+ template(id = actuality_duration_h)
+ {
+ name = actuality_duration
+ quantity = Time
+ info = 'Lapsed time between measurement and transmission.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = ActualityDuration
+ }
+ }
+ template(id = battery_v)
+ {
+ name = battery
+ quantity = Voltage
+ info = 'Battery voltage.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = Voltage
+ }
+ }
+ template(id = battery_y)
+ {
+ name = battery
+ quantity = Time
+ info = 'Remaining battery life in years.'
+ display_unit = y
+ match {
+ measurement_type = Instantaneous
+ vif_range = RemainingBattery
+ }
+ }
+ template(id = fabrication_no)
+ {
+ name = fabrication_no
+ quantity = Text
+ info = 'Fabrication number.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = FabricationNo
+ }
+ }
+ template(id = enhanced_id)
+ {
+ name = enhanced_id
+ quantity = Text
+ info = 'Enhanced identification number.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = EnhancedIdentification
+ }
+ }
+ template(id = software_version)
+ {
+ name = software_version
+ quantity = Text
+ info = 'Software version.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = SoftwareVersion
+ }
+ }
+ template(id = manufacturer)
+ {
+ name = manufacturer
+ quantity = Text
+ info = 'Meter manufacturer.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = Manufacturer
+ }
+ }
+ template(id = model_version)
+ {
+ name = model_version
+ quantity = Text
+ info = 'Meter model version.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = ModelVersion
+ }
+ }
+ template(id = firmware_version)
+ {
+ name = firmware_version
+ quantity = Text
+ info = 'Meter firmware version.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = FirmwareVersion
+ }
+ }
+ template(id = parameter_set)
+ {
+ name = parameter_set
+ quantity = Text
+ info = 'Parameter set for this meter.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = ParameterSet
+ }
+ }
+ template(id = customer)
+ {
+ name = customer
+ quantity = Text
+ info = 'Customer name.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = Customer
+ }
+ }
+ template(id = location)
+ {
+ name = location
+ quantity = Text
+ info = 'Meter installed at this customer location.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = Location
+ }
+ }
+ template(id = operating_time_h)
+ {
+ name = operating_time
+ quantity = Time
+ info = 'How long the meter has been collecting data.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = OperatingTime
+ }
+ }
+ template(id = on_time_h)
+ {
+ name = on_time
+ quantity = Time
+ info = 'How long the meter has been powered up.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = OnTime
+ }
+ }
+ template(id = on_time_at_error_h)
+ {
+ name = on_time_at_error
+ quantity = Time
+ info = 'How long the meter has been in an error state while powered up.'
+ match {
+ measurement_type = AtError
+ vif_range = OnTime
+ }
+ }
+ template(id = meter_date)
+ {
+ name = meter_date
+ quantity = Text
+ info = 'Date when the meter sent the telegram.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = Date
+ }
+ }
+ template(id = meter_date_at_error)
+ {
+ name = meter_date_at_error
+ quantity = Text
+ info = 'Date when the meter was in error.'
+ match {
+ measurement_type = AtError
+ vif_range = Date
+ }
+ }
+ template(id = meter_datetime)
+ {
+ name = meter_datetime
+ quantity = Text
+ info = 'Date and time when the meter sent the telegram.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = DateTime
+ }
+ }
+ template(id = meter_datetime_at_error)
+ {
+ name = meter_datetime_at_error
+ quantity = Text
+ info = 'Date and time when the meter was in error.'
+ match {
+ measurement_type = AtError
+ vif_range = DateTime
+ }
+ }
+ template(id = target_date)
+ {
+ name = target
+ quantity = PointInTime
+ info = 'The target date. Usually the end of the previous billing period.'
+ display_unit = date
+ match {
+ measurement_type = Instantaneous
+ vif_range = Date
+ storage_nr = 1
+ }
+ }
+ template(id = total_forward_m3)
+ {
+ name = total_forward
+ quantity = Volume
+ info = 'The total {media} volume flowing forward.'
+ change = Increasing
+ match {
+ measurement_type = Instantaneous
+ vif_range = Volume
+ add_combinable = ForwardFlow
+ }
+ }
+ template(id = total_backward_m3)
+ {
+ name = total_backward
+ quantity = Volume
+ info = 'The total {media} volume flowing backward.'
+ change = Increasing
+ match {
+ measurement_type = Instantaneous
+ vif_range = Volume
+ add_combinable = BackwardFlow
+ }
+ }
+ template(id = flow_temperature_c)
+ {
+ name = flow_temperature
+ quantity = Temperature
+ info = 'Forward media temperature.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = FlowTemperature
+ }
+ }
+ template(id = external_temperature_c)
+ {
+ name = external_temperature
+ quantity = Temperature
+ info = 'Temperature outside of meter.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = ExternalTemperature
+ }
+ }
+ template(id = return_temperature_c)
+ {
+ name = return_temperature
+ quantity = Temperature
+ info = 'Return media temperature.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = ReturnTemperature
+ }
+ }
+ template(id = flow_return_temperature_difference_c)
+ {
+ name = flow_return_temperature_difference
+ quantity = Temperature
+ info = 'The difference between flow and return media temperatures.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = TemperatureDifference
+ }
+ }
+ template(id = volume_flow_m3h)
+ {
+ name = volume_flow
+ quantity = Flow
+ info = 'Media volume flow.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = VolumeFlow
+ }
+ }
+ template(id = access_counter)
+ {
+ name = access
+ quantity = Dimensionless
+ info = 'Meter access counter.'
+ vif_scaling = None
+ dif_signedness = Unsigned
+ match {
+ measurement_type = Instantaneous
+ vif_range = AccessNumber
+ }
+ }
+ template(id = consumption_hca)
+ {
+ name = consumption
+ quantity = HCA
+ info = 'The current heat cost allocation for this meter.'
+ change = Increasing
+ match {
+ measurement_type = Instantaneous
+ vif_range = HeatCostAllocation
+ }
+ }
+ template(id = target_hca)
+ {
+ name = target
+ quantity = HCA
+ info = 'The heat cost allocation recorded by this meter at the target date.'
+ change = Increasing
+ match {
+ measurement_type = Instantaneous
+ vif_range = HeatCostAllocation
+ storage_nr = 1
+ }
+ }
+}
diff --git a/components/wmbus_common/drivers/loader/__init__.py b/components/wmbus_common/drivers/loader/__init__.py
new file mode 100644
index 00000000..381fcd46
--- /dev/null
+++ b/components/wmbus_common/drivers/loader/__init__.py
@@ -0,0 +1,36 @@
+import esphome.config_validation as cv
+
+from .driver import AutoDriver, Driver
+from .driver_manager import GENERATED_DIR
+from .driver_manager import DriverManager as _DriverManagerClass
+from .units import UNITS, Unit, split_unit
+from .xmq_loader import FieldKind
+
+DriverManager = _DriverManagerClass()
+DriverManager.load_drivers()
+
+AUTO = "auto"
+
+validate_driver_name = cv.All(cv.one_of(*DriverManager.available_drivers), DriverManager.request_driver)
+
+
+def validate_meter_type(value):
+ if value == AUTO:
+ return DriverManager.request_auto_driver()
+
+ return validate_driver_name(value)
+
+
+__all__ = [
+ "AUTO",
+ "AutoDriver",
+ "DriverManager",
+ "GENERATED_DIR",
+ "UNITS",
+ "Unit",
+ "validate_driver_name",
+ "validate_meter_type",
+ "Driver",
+ "FieldKind",
+ "split_unit",
+]
diff --git a/components/wmbus_common/drivers/loader/cpp.py b/components/wmbus_common/drivers/loader/cpp.py
new file mode 100644
index 00000000..daa97876
--- /dev/null
+++ b/components/wmbus_common/drivers/loader/cpp.py
@@ -0,0 +1,90 @@
+"""C++ emitted for the compiler only; not formatted."""
+
+from __future__ import annotations
+
+import math
+from dataclasses import dataclass
+from functools import partial
+from pathlib import Path
+from typing import Literal
+
+import jinja2
+from esphome.cpp_generator import (
+ ArrayInitializer,
+ BinOpExpression,
+ Expression,
+ FloatLiteral,
+ MockObj,
+ RawExpression,
+ StructInitializer,
+ UnaryOpExpression,
+ literal,
+)
+from esphome.cpp_types import global_ns
+
+wmbus_ns = global_ns.namespace("wmbus")
+
+# Designated members keep a row honest when a header reorders them.
+Aggregate = partial(StructInitializer, RawExpression(""))
+
+TEMPLATES = jinja2.Environment(
+ loader=jinja2.FileSystemLoader(Path(__file__).parent / "templates"),
+ undefined=jinja2.StrictUndefined,
+ trim_blocks=True,
+ lstrip_blocks=True,
+ keep_trailing_newline=True,
+)
+
+
+@dataclass
+class Declaration:
+ kind: Literal["array", "value", "formula"]
+ type: str
+ symbol: str
+ value: Expression
+
+
+class Tables:
+ """A driver's static declarations, in the order they refer to each other."""
+
+ def __init__(self):
+ self.declarations: list[Declaration] = []
+
+ def span(self, type_: str, symbol: str, rows: list) -> Expression:
+ if not rows:
+ return ArrayInitializer()
+ self.declarations.append(Declaration("array", type_, symbol, ArrayInitializer(*rows, multiline=True)))
+ return ArrayInitializer(literal(symbol), len(rows))
+
+ def define(self, type_: str, symbol: str, value: Expression) -> Expression:
+ self.declarations.append(Declaration("value", type_, symbol, value))
+ return MockObj(UnaryOpExpression("&", literal(symbol)))
+
+ def formula(self, symbol: str, value: Expression) -> Expression:
+ self.declarations.append(Declaration("formula", "double", symbol, value))
+ return literal(symbol)
+
+
+class _DoubleLiteral(FloatLiteral):
+ def __str__(self) -> str:
+ if math.isinf(self.f):
+ return "INFINITY" if self.f > 0 else "(-INFINITY)"
+ text = super().__str__().removesuffix("f")
+ return f"({text})" if text.startswith("-") else text
+
+
+def double(value: float) -> MockObj:
+ return MockObj(_DoubleLiteral(value))
+
+
+def cast(type_: str, expr: Expression) -> MockObj:
+ return literal("static_cast").template(literal(type_))(expr)
+
+
+def ternary(cond: Expression, if_true: Expression, if_false: Expression) -> MockObj:
+ return MockObj(RawExpression(f"({cond} ? {if_true} : {if_false})"))
+
+
+def bin_op(left: Expression, op: str, right: Expression) -> MockObj:
+ """For && and ||, which Python cannot overload."""
+ return MockObj(BinOpExpression(left, op, right))
diff --git a/components/wmbus_common/drivers/loader/driver.py b/components/wmbus_common/drivers/loader/driver.py
new file mode 100644
index 00000000..659027b0
--- /dev/null
+++ b/components/wmbus_common/drivers/loader/driver.py
@@ -0,0 +1,103 @@
+from dataclasses import dataclass, field
+from functools import cached_property
+from pathlib import Path
+
+import esphome.config_validation as cv
+
+from .xmq import parse_xmq
+from .xmq_loader import SCHEMA, FieldKind, concrete_fields, driver_symbol, generate_from_definition
+
+# Answered by wmbus_meter from the telegram, not by the driver.
+WELL_KNOWN_FIELDS = {
+ "media": {FieldKind.Text},
+ "rssi_dbm": {FieldKind.Numeric},
+ "timestamp": {FieldKind.Numeric, FieldKind.Text},
+}
+
+
+@dataclass(frozen=True)
+class FieldDefinition:
+ kind: FieldKind
+ name: str
+
+
+class FieldSelection:
+ fields: set[FieldDefinition]
+
+ def available_fields(self, kind: FieldKind | None = None) -> list[str]:
+ return sorted(
+ {f.name for f in self.fields if kind is None or f.kind is kind}
+ | {n for n, kinds in WELL_KNOWN_FIELDS.items() if kind is None or kind in kinds}
+ )
+
+ def kinds_of(self, field_name: str) -> set[FieldKind]:
+ return WELL_KNOWN_FIELDS.get(field_name, set()) | {
+ f.kind for f in self.fields if f.name == field_name
+ }
+
+ def request_field(self, field_name: str, kind: FieldKind | None = None) -> str:
+ kinds = self.kinds_of(field_name)
+ if kinds and kind is not None and kind not in kinds:
+ raise cv.Invalid(
+ f"field '{field_name}' of driver '{self.name}' holds "
+ f"{' and '.join(sorted(kinds))} values, not {kind}"
+ )
+ cv.one_of(*self.available_fields(kind))(field_name)
+ self.select(field_name)
+ return field_name
+
+ def select(self, field_name: str) -> None:
+ pass
+
+
+@dataclass(frozen=True, order=True)
+class Driver(FieldSelection):
+ name: str
+ source_path: Path = field(compare=False, repr=False)
+ # Empty means every field.
+ requested: set[str] = field(default_factory=set, compare=False, repr=False)
+
+ @classmethod
+ def from_source(cls, path: Path) -> "Driver":
+ return cls(name=path.stem, source_path=path)
+
+ @cached_property
+ def _definition(self) -> dict:
+ return SCHEMA(parse_xmq(self.source_path.read_text()))["driver"]
+
+ @cached_property
+ def fields(self) -> set[FieldDefinition]:
+ return {
+ FieldDefinition(f.definition.kind, f.json_name)
+ for f in concrete_fields(self._definition)
+ }
+
+ def select(self, field_name: str) -> None:
+ self.requested.add(field_name)
+
+ @property
+ def aliases(self) -> list[str]:
+ return self._definition["aliases"]
+
+ @property
+ def symbol(self) -> str:
+ return driver_symbol(self.name)
+
+ def serialize(self) -> str:
+ return generate_from_definition(
+ self._definition,
+ self.source_path.name,
+ include_fields=self.requested or None,
+ )
+
+
+@dataclass(frozen=True)
+class AutoDriver(FieldSelection):
+ """`type: auto`: fields are those of every compiled driver; generates no source."""
+
+ manager: object = field(repr=False, compare=False)
+ name: str = "auto"
+
+ @property
+ def fields(self) -> set[FieldDefinition]:
+ return {f for d in self.manager.registered_drivers for f in d.fields}
diff --git a/components/wmbus_common/drivers/loader/driver_manager.py b/components/wmbus_common/drivers/loader/driver_manager.py
new file mode 100644
index 00000000..ec883ded
--- /dev/null
+++ b/components/wmbus_common/drivers/loader/driver_manager.py
@@ -0,0 +1,75 @@
+from pathlib import Path
+
+from .cpp import TEMPLATES
+from .driver import AutoDriver, Driver
+
+XMQ_DRIVERS_PATH = Path(__file__).parents[1] / "src"
+
+GENERATED_DIR = "wmbusmeters_generated"
+
+
+class DriverManager:
+ def __init__(self):
+ self._all_drivers: dict[str, Driver] = {}
+ self._aliases: dict[str, str] = {}
+ self._requested_drivers: set[Driver] = set()
+ self._auto_driver = AutoDriver(manager=self)
+ self._auto_requested = False
+
+ def load_drivers(self) -> None:
+ for p in sorted(XMQ_DRIVERS_PATH.glob("*.xmq")):
+ driver = Driver.from_source(p)
+ self._all_drivers[driver.name] = driver
+ for alias in driver.aliases:
+ self._aliases.setdefault(alias, driver.name)
+
+ @property
+ def available_drivers(self) -> list[str]:
+ return sorted(set(self._all_drivers) | set(self._aliases))
+
+ def request_driver(self, driver_name: str) -> Driver:
+ driver = self._all_drivers[self._aliases.get(driver_name, driver_name)]
+ self._requested_drivers.add(driver)
+ return driver
+
+ @property
+ def auto_requested(self) -> bool:
+ return self._auto_requested
+
+ def request_auto_driver(self) -> AutoDriver:
+ self._auto_requested = True
+ return self._auto_driver
+
+ @property
+ def registered_drivers(self) -> list[Driver]:
+ return sorted(self._requested_drivers)
+
+ def registry_index(self, driver: Driver) -> int:
+ return self.registered_drivers.index(driver)
+
+ def sync_to_directory(self, target_dir: str | Path) -> None:
+ """Outside src/esphome/, because writer.copy_src_tree() deletes anything there it did not put."""
+ if self._auto_requested:
+ # An auto meter may bind to any compiled driver, so none can be trimmed.
+ for driver in self._requested_drivers:
+ driver.requested.clear()
+
+ files = {
+ "registry.h": TEMPLATES.get_template("registry.h.j2").render(
+ symbols=[d.symbol for d in self.registered_drivers]
+ ),
+ **{f"{d.name}.cpp": d.serialize() for d in self._requested_drivers},
+ }
+
+ target_dir = Path(target_dir)
+ target_dir.mkdir(exist_ok=True, parents=True)
+
+ for name, content in files.items():
+ path = target_dir / name
+ if not path.exists() or path.read_text() != content:
+ path.write_text(content)
+
+ # esp-idf globs sources with CONFIGURE_DEPENDS, so stale files would compile.
+ for path in target_dir.iterdir():
+ if path.name not in files:
+ path.unlink()
diff --git a/components/wmbus_common/drivers/loader/formula.py b/components/wmbus_common/drivers/loader/formula.py
new file mode 100644
index 00000000..222d8ce7
--- /dev/null
+++ b/components/wmbus_common/drivers/loader/formula.py
@@ -0,0 +1,542 @@
+"""Compiles XMQ `calculate` formulas to C++ at codegen, keeping upstream's quirks.
+
+ * no precedence: left to right, a binary operator takes one operand on its right,
+ * + and - convert the left side to the right side's unit,
+ * counter operators (%, >>, ==, &, ...) and round/floor/ceil ignore the target unit,
+ * a date literal (mktime in the device's zone) and adding months stay run-time calls.
+
+Unsupported because no driver uses them: **, time literals, temperature scale
+conversion, adding years to a date. Upstream has parsed every formula already, so
+only these raise; the syntax and the field references are taken as valid.
+"""
+
+from __future__ import annotations
+
+import math
+import operator
+import re
+from dataclasses import dataclass
+from enum import Enum, StrEnum
+from typing import Callable
+
+from esphome.cpp_generator import Expression, MockObj, literal
+from esphome.cpp_types import std_ns
+
+from .cpp import bin_op, cast, double, ternary, wmbus_ns
+from .units import BY_SYMBOL, UNITS, Quantity, SIExp, Unit, split_unit
+
+
+@dataclass(frozen=True)
+class SIUnit:
+ quantity: Quantity
+ scale: float
+ exp: SIExp
+
+ # A product or a quotient never lands on a named unit, so its quantity is Unknown.
+ def mul(self, other: "SIUnit") -> "SIUnit":
+ return SIUnit(Quantity.Unknown, self.scale * other.scale, self.exp.mul(other.exp))
+
+ def div(self, other: "SIUnit") -> "SIUnit":
+ return SIUnit(Quantity.Unknown, self.scale / other.scale, self.exp.div(other.exp))
+
+ def sqrt(self) -> "SIUnit":
+ return SIUnit(Quantity.Unknown, math.sqrt(self.scale), self.exp.sqrt())
+
+
+def si_unit(unit: Unit) -> SIUnit:
+ """A unit without SI terms (TXT, DateTimeLT) converts to nothing."""
+ info = UNITS[unit]
+ if info.si is None:
+ return SIUnit(Quantity.Unknown, 0.0, SIExp())
+ return SIUnit(info.quantity, *info.si)
+
+
+ZERO = double(0.0)
+ONE = double(1.0)
+
+Emit = Callable[..., Expression]
+
+
+def _truth(spelling: str) -> tuple[str, int, Emit]:
+ return (
+ spelling,
+ 2,
+ lambda left, right: ternary(bin_op(left != ZERO, spelling, right != ZERO), ONE, ZERO),
+ )
+
+
+def _comparison(spelling: str, compare: Callable) -> tuple[str, int, Emit]:
+ return spelling, 2, lambda left, right: ternary(compare(left, right), ONE, ZERO)
+
+
+class Op(Enum):
+ ADD = ("+", 2, operator.add)
+ SUB = ("-", 2, operator.sub)
+ MUL = ("*", 2, operator.mul)
+ DIV = ("/", 2, operator.truediv)
+ MOD = ("%", 2, wmbus_ns.modulo)
+ SHL = ("<<", 2, wmbus_ns.shift_left)
+ SHR = (">>", 2, wmbus_ns.shift_right)
+ BAND = ("&", 2, wmbus_ns.bit_and)
+ BOR = ("|", 2, wmbus_ns.bit_or)
+ BXOR = ("^", 2, wmbus_ns.bit_xor)
+ LAND = _truth("&&")
+ LOR = _truth("||")
+ EQ = _comparison("==", operator.eq)
+ NEQ = _comparison("!=", operator.ne)
+ LT = _comparison("<", operator.lt)
+ GT = _comparison(">", operator.gt)
+ LTE = _comparison("<=", operator.le)
+ GTE = _comparison(">=", operator.ge)
+ SQRT = ("sqrt", 1, std_ns.sqrt)
+ ROUND = ("round", 1, std_ns.round)
+ FLOOR = ("floor", 1, std_ns.floor)
+ CEIL = ("ceil", 1, std_ns.ceil)
+
+ def __init__(self, spelling: str, arity: int, emit: Emit):
+ self.spelling = spelling
+ self.arity = arity
+ self.emit = emit
+
+
+class Tok(Enum):
+ NUMBER = "number"
+ FIELD = "field"
+ UNIT = "unit"
+ DATETIME = "datetime"
+ LPAR = "("
+ RPAR = ")"
+ COMMA = ","
+ MKDATE = "mkdate"
+
+
+def conversion(src: SIUnit, dst: SIUnit) -> tuple[float, float] | None:
+ """(from scale, to scale): a value converts as v * from / to."""
+ if src.exp.exps != dst.exp.exps:
+ return None
+ return src.scale, dst.scale
+
+
+def _unix_timestamp_exp() -> SIExp:
+ return UNITS[Unit.UnixTimestamp].si[1]
+
+
+def _is_second(exp: SIExp) -> bool:
+ return exp == UNITS[Unit.Second].si[1]
+
+
+def math_op_unit(op: Op, left: SIUnit, right: SIUnit) -> SIUnit | None:
+ ut = _unix_timestamp_exp()
+ if left.exp == right.exp:
+ if op is Op.ADD and left.exp == ut:
+ return None
+ return right
+ if left.exp == ut or right.exp == ut:
+ if right.exp == ut:
+ return math_op_unit(op, right, left)
+ if _is_second(right.exp) or right.exp == UNITS[Unit.Month].si[1]:
+ return si_unit(Unit.UnixTimestamp)
+ return None
+
+
+_DATETIME_FORMATS = (
+ (21, r"'\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}'"),
+ (12, r"'\d{4}-\d{2}-\d{2}'"),
+)
+
+# Longest spelling first, so that >> wins over >.
+_FIXED_TOKENS = tuple(
+ sorted(
+ [(op.spelling, op) for op in Op]
+ + [(tok.value, tok) for tok in (Tok.LPAR, Tok.RPAR, Tok.COMMA, Tok.MKDATE)],
+ key=lambda pair: len(pair[0]),
+ reverse=True,
+ )
+)
+
+
+@dataclass(frozen=True)
+class Token:
+ kind: Tok | Op
+ text: str
+
+
+def _is_letter(c: str) -> bool:
+ return "a" <= c <= "z"
+
+
+def _is_letter_digit_or_underscore(c: str) -> bool:
+ return c == "_" or "a" <= c <= "z" or "0" <= c <= "9"
+
+
+def tokenize(f: str) -> list[Token]:
+ tokens = []
+ i = 0
+ n = len(f)
+ lcnames = sorted(BY_SYMBOL, key=len, reverse=True)
+
+ while i < n:
+ c = f[i]
+ if c.isspace():
+ i += 1
+ continue
+
+ matched = False
+ for length, pattern in _DATETIME_FORMATS:
+ if i + length - 1 < n and re.match(pattern, f[i : i + length]):
+ tokens.append(Token(Tok.DATETIME, f[i : i + length]))
+ i += length
+ matched = True
+ break
+ if matched:
+ continue
+
+ m = re.match(r"[0-9][0-9.]*", f[i:])
+ if m and m.group(0).count(".") <= 1:
+ tokens.append(Token(Tok.NUMBER, m.group(0)))
+ i += len(m.group(0))
+ continue
+
+ for text, kind in _FIXED_TOKENS:
+ if not f.startswith(text, i):
+ continue
+ # Upstream wants a character after a spelled-out operator, so one cannot end a formula.
+ if text.isalpha() and (i + len(text) >= n or _is_letter(f[i + len(text)])):
+ continue
+ tokens.append(Token(kind, text))
+ i += len(text)
+ matched = True
+ break
+ if matched:
+ continue
+
+ if _is_letter(c):
+ # Upstream tries units before fields.
+ for lc in lcnames:
+ end = i + len(lc)
+ if f.startswith(lc, i) and (end >= n or not _is_letter_digit_or_underscore(f[end])):
+ tokens.append(Token(Tok.UNIT, lc))
+ i = end
+ matched = True
+ break
+ if matched:
+ continue
+
+ m = re.match(r"[a-z][a-z0-9_]*", f[i:])
+ tokens.append(Token(Tok.FIELD, m.group(0)))
+ i += len(m.group(0))
+
+ return tokens
+
+
+class Counter(StrEnum):
+ """Values are DVEntryCounterType enumerators; formulas name them in lower case."""
+
+ STORAGE_COUNTER = "STORAGE_COUNTER"
+ TARIFF_COUNTER = "TARIFF_COUNTER"
+ SUBUNIT_COUNTER = "SUBUNIT_COUNTER"
+
+ @staticmethod
+ def named(field_name: str) -> "Counter | None":
+ return next((c for c in Counter if c.lower() == field_name), None)
+
+ @property
+ def prefix(self) -> str:
+ """As in storage_from, match_tariff_nr."""
+ return self.lower().removesuffix("_counter")
+
+
+@dataclass
+class Node:
+ si: SIUnit
+
+
+@dataclass
+class Constant(Node):
+ value: float
+
+
+@dataclass
+class DateTimeLiteral(Node):
+ parts: tuple[int, int, int, int, int, int]
+
+
+@dataclass
+class MeterField(Node):
+ vname: str
+ slot: int
+ display_unit: Unit
+
+
+@dataclass
+class CounterField(Node):
+ counter: Counter
+
+
+@dataclass
+class Binary(Node):
+ op: Op
+ left: Node
+ right: Node
+
+
+@dataclass
+class Unary(Node):
+ op: Op
+ inner: Node
+
+
+@dataclass
+class MkDate(Node):
+ year: Node
+ month: Node
+ day: Node
+
+
+@dataclass(frozen=True)
+class FieldRef:
+ slot: int
+ display_unit: Unit
+
+
+# (vname, the unit the formula names it in) -> the field it refers to.
+ResolveField = Callable[[str, Unit], FieldRef]
+
+
+class Parser:
+ def __init__(self, formula: str, resolve_field: ResolveField):
+ self.tokens = tokenize(formula)
+ self.resolve_field = resolve_field
+ self.stack: list[Node] = []
+
+ def la(self, i: int) -> Token | None:
+ return self.tokens[i] if i < len(self.tokens) else None
+
+ def parse(self) -> Node:
+ i = 0
+ while (nxt := self.parse_ops(i)) != i:
+ i = nxt
+ return self.stack.pop()
+
+ def parse_ops(self, i: int) -> int:
+ tok = self.la(i)
+ if tok is None:
+ return i
+
+ match tok.kind:
+ case Tok.FIELD:
+ self.handle_field(tok)
+ return i + 1
+
+ case Tok.DATETIME:
+ self.stack.append(DateTimeLiteral(si_unit(Unit.UnixTimestamp), self.datetime_parts(tok.text)))
+ return i + 1
+
+ case Op(arity=2):
+ after = self.parse_ops(i + 1)
+ self.handle_binary(tok)
+ return after
+
+ case Op(arity=1):
+ after = self.parse_ops(i + 1)
+ self.handle_unary(tok)
+ return after
+
+ case Tok.MKDATE:
+ return self.parse_mkdate(i)
+
+ case Tok.LPAR:
+ return self.parse_par(i)
+
+ case Tok.NUMBER:
+ unit = BY_SYMBOL[self.tokens[i + 1].text]
+ self.stack.append(Constant(si_unit(unit), float(tok.text)))
+ return i + 2
+
+ # A comma, a closing parenthesis or a bare unit is for the caller to read.
+ case _:
+ return i
+
+ def parse_par(self, i: int) -> int:
+ i += 1
+ while self.tokens[i].kind is not Tok.RPAR:
+ i = self.parse_ops(i)
+ return i + 1
+
+ def parse_mkdate(self, i: int) -> int:
+ i += 2 # mkdate (
+ for _arg in range(3):
+ while self.tokens[i].kind not in (Tok.COMMA, Tok.RPAR):
+ i = self.parse_ops(i)
+ i += 1 # , or )
+ day, month, year = self.stack.pop(), self.stack.pop(), self.stack.pop()
+ self.stack.append(MkDate(si_unit(Unit.UnixTimestamp), year, month, day))
+ return i
+
+ @staticmethod
+ def datetime_parts(text: str) -> tuple[int, int, int, int, int, int]:
+ digits = [int(d) for d in re.findall(r"\d+", text)]
+ return tuple(digits + [0] * (6 - len(digits))) # type: ignore[return-value]
+
+ def handle_field(self, tok: Token) -> None:
+ counter = Counter.named(tok.text)
+ if counter is not None:
+ self.stack.append(CounterField(si_unit(Unit.COUNTER), counter))
+ return
+
+ vname, named_unit = split_unit(tok.text)
+ ref = self.resolve_field(vname, named_unit)
+ self.stack.append(MeterField(si_unit(named_unit), vname, ref.slot, ref.display_unit))
+
+ def handle_binary(self, tok: Token) -> None:
+ right, left = self.stack.pop(), self.stack.pop()
+ match tok.kind:
+ case Op.ADD | Op.SUB:
+ si = math_op_unit(tok.kind, left.si, right.si)
+ case Op.MUL:
+ si = left.si.mul(right.si)
+ case Op.DIV:
+ si = left.si.div(right.si)
+ case _:
+ si = si_unit(Unit.COUNTER)
+ self.stack.append(Binary(si, tok.kind, left, right))
+
+ def handle_unary(self, tok: Token) -> None:
+ inner = self.stack.pop()
+ si = inner.si.sqrt() if tok.kind is Op.SQRT else inner.si
+ self.stack.append(Unary(si, tok.kind, inner))
+
+
+STATE = MockObj("state")
+DVE = literal("dve") # null for a field with no matcher
+COUNTER_TYPE = wmbus_ns.enum("DVEntryCounterType", is_class=True)
+
+
+def _convert_emit(expr: Expression, src: SIUnit, dst: SIUnit) -> Expression:
+ conv = conversion(src, dst)
+ if conv is None:
+ return double(math.nan)
+ from_scale, to_scale = conv
+ return expr if from_scale == to_scale else expr * double(from_scale) / double(to_scale)
+
+
+def _convert_value(value: float, src: SIUnit, dst: SIUnit) -> float:
+ conv = conversion(src, dst)
+ if conv is None:
+ return math.nan
+ from_scale, to_scale = conv
+ return value if from_scale == to_scale else (value * from_scale) / to_scale
+
+
+def _math_op_emit(
+ op: Op, l: Expression, left: SIUnit, r: Expression, right: SIUnit
+) -> tuple[Expression, SIUnit]:
+ """Only the unit combinations math_op_unit accepts get here."""
+ ut = _unix_timestamp_exp()
+ if left.exp == right.exp:
+ return op.emit(_convert_emit(l, left, right), r), right
+ if right.exp == ut:
+ # Upstream swaps the operands, for subtraction too.
+ return _math_op_emit(op, r, right, l, left)
+ if _is_second(right.exp):
+ converted = _convert_emit(r, right, si_unit(Unit.Second))
+ return op.emit(l, converted), si_unit(Unit.UnixTimestamp)
+ signed_r = -r if op is Op.SUB else r
+ return wmbus_ns.add_months(l, cast("int", signed_r)), si_unit(Unit.UnixTimestamp)
+
+
+def emit(node: Node, to: SIUnit) -> Expression:
+ counter = si_unit(Unit.COUNTER)
+
+ match node:
+ case Constant(value=value):
+ return double(_convert_value(value, node.si, to))
+
+ case DateTimeLiteral(parts=parts):
+ return _convert_emit(wmbus_ns.datetime_literal(*parts), node.si, to)
+
+ case MeterField(slot=slot, display_unit=display_unit):
+ return _convert_emit(STATE.field_value(slot), si_unit(display_unit), to)
+
+ case CounterField(counter=counter_type):
+ return _convert_emit(
+ wmbus_ns.counter_value(DVE, getattr(COUNTER_TYPE, counter_type)), node.si, to
+ )
+
+ case MkDate(year=year, month=month, day=day):
+ return wmbus_ns.make_date(
+ emit(year, counter), emit(month, counter), emit(day, counter)
+ )
+
+ case Unary(op=Op.SQRT, inner=inner):
+ return _convert_emit(Op.SQRT.emit(emit(inner, inner.si)), node.si, to)
+
+ case Unary(op=op, inner=inner):
+ # Upstream does not convert a rounded result to `to`.
+ return op.emit(emit(inner, inner.si))
+
+ case Binary(op=(Op.ADD | Op.SUB) as op, left=left, right=right):
+ expr, si = _math_op_emit(
+ op, emit(left, left.si), left.si, emit(right, right.si), right.si
+ )
+ return _convert_emit(expr, si, to)
+
+ case Binary(op=(Op.MUL | Op.DIV) as op, left=left, right=right):
+ return _convert_emit(op.emit(emit(left, left.si), emit(right, right.si)), node.si, to)
+
+ case Binary(op=op, left=left, right=right):
+ return op.emit(emit(left, counter), emit(right, counter))
+
+
+def compile_formula(formula: str, resolve_field: ResolveField, display_unit: Unit) -> Expression:
+ return emit(Parser(formula, resolve_field).parse(), si_unit(display_unit))
+
+
+def referenced_fields(formula: str) -> set[tuple[str, Quantity]]:
+ refs = set()
+ for tok in tokenize(formula):
+ if tok.kind is Tok.FIELD and Counter.named(tok.text) is None:
+ vname, unit = split_unit(tok.text)
+ if unit is not None:
+ refs.add((vname, UNITS[unit].quantity))
+ return refs
+
+
+_TEMPLATE = re.compile(r"\{([^}]*)\}")
+
+
+def _template_terms(formula: str) -> list[tuple[int, Counter | int]]:
+ """`storage_counter-31counter` as signed terms: a counter, or a constant."""
+ terms = []
+ sign = 1
+ tokens = re.findall(r"[+-]|\d+|[a-z_]+", formula)
+ while tokens:
+ token = tokens.pop(0)
+ if token in "+-":
+ sign = 1 if token == "+" else -1
+ continue
+ counter = Counter.named(token)
+ if counter is None:
+ counter = int(token)
+ tokens.pop(0)
+ terms.append((sign, counter))
+ sign = 1
+ return terms
+
+
+def template_counters(name: str) -> set[Counter]:
+ return {
+ term for formula in _TEMPLATE.findall(name)
+ for _sign, term in _template_terms(formula) if isinstance(term, Counter)
+ }
+
+
+def expand_template(name: str, counters: dict[Counter, int]) -> str:
+ def replace(match: re.Match) -> str:
+ value = sum(
+ sign * (counters[term] if isinstance(term, Counter) else term)
+ for sign, term in _template_terms(match.group(1))
+ )
+ return str(value)
+
+ return _TEMPLATE.sub(replace, name)
diff --git a/components/wmbus_common/drivers/loader/ixml.py b/components/wmbus_common/drivers/loader/ixml.py
new file mode 100644
index 00000000..d90a78de
--- /dev/null
+++ b/components/wmbus_common/drivers/loader/ixml.py
@@ -0,0 +1,491 @@
+"""Compiles driver ixml grammars to tables at codegen time.
+
+Grammars match a frame's hex transcript, the device its bytes: character runs become
+byte patterns, one pair of nibble masks per byte.
+"""
+
+import re
+from dataclasses import dataclass, field
+from enum import StrEnum
+from typing import ClassVar
+
+from esphome.core import HexInt
+from esphome.cpp_generator import Expression, literal
+
+from .cpp import Aggregate, Tables, wmbus_ns
+
+
+class Repeat(StrEnum):
+ Once = ""
+ Star = "*"
+ Plus = "+"
+ Optional = "?"
+
+
+@dataclass(kw_only=True)
+class Term:
+ deleted: bool = False
+ attribute: bool = False
+ repeat: Repeat = Repeat.Once
+
+
+@dataclass
+class Literal(Term):
+ text: str
+
+
+@dataclass
+class Insert(Term):
+ text: str
+
+
+@dataclass
+class RuleRef(Term):
+ name: str
+
+
+@dataclass
+class CharClass(Term):
+ ranges: list[tuple[str, str]]
+
+
+@dataclass
+class Group(Term):
+ alternatives: list[list[Term]]
+
+
+@dataclass
+class Rule:
+ name: str
+ alternatives: list[list[Term]] = field(default_factory=list)
+ alias: str | None = None # "DV_0413>dvk": the node is named dvk
+
+
+@dataclass(frozen=True)
+class Token:
+ text: str
+ pattern: ClassVar[re.Pattern]
+
+ @classmethod
+ def at(cls, text: str, position: int) -> "Token":
+ for kind in cls.__subclasses__():
+ if match := kind.pattern.match(text, position):
+ return kind(match.group())
+
+
+class Blank(Token):
+ pattern = re.compile(r"\s+|\{[^{}]*\}")
+
+
+class Quoted(Token):
+ pattern = re.compile(r"'[^']*'")
+
+
+class Bracketed(Token):
+ pattern = re.compile(r"\[[^\]]*\]")
+
+
+class Name(Token):
+ pattern = re.compile(r"[A-Za-z_][A-Za-z0-9_]*")
+
+
+class Punct(Token):
+ pattern = re.compile(r"[-@^+*?|,.=()>]")
+
+
+def tokenize(text: str):
+ position = 0
+ while position < len(text):
+ token = Token.at(text, position)
+ position += len(token.text)
+ if not isinstance(token, Blank):
+ yield token
+
+
+class _Parser:
+ def __init__(self, text: str):
+ self.tokens = list(tokenize(text))
+ self.pos = 0
+
+ def peek(self) -> Token | None:
+ return self.tokens[self.pos] if self.pos < len(self.tokens) else None
+
+ def take(self) -> Token:
+ token = self.peek()
+ self.pos += 1
+ return token
+
+ def parse(self) -> dict[str, Rule]:
+ rules: dict[str, Rule] = {}
+ while self.peek() is not None:
+ rule = self.parse_rule()
+ rules[rule.name] = rule
+ return rules
+
+ def parse_rule(self) -> Rule:
+ # A "-" rule hides its node, which is irrelevant: only the keys matter.
+ if self.peek() == Punct("-"):
+ self.take()
+
+ name = self.take().text
+
+ alias = None
+ if self.peek() == Punct(">"):
+ self.take()
+ alias = self.take().text
+
+ self.take() # =
+ alternatives = self.parse_alternatives()
+ self.take() # .
+
+ return Rule(name=name, alternatives=alternatives, alias=alias)
+
+ def parse_alternatives(self) -> list[list[Term]]:
+ alternatives = [self.parse_sequence()]
+ while self.peek() == Punct("|"):
+ self.take()
+ alternatives.append(self.parse_sequence())
+ return alternatives
+
+ def parse_sequence(self) -> list[Term]:
+ terms = [self.parse_term()]
+ while self.peek() == Punct(","):
+ self.take()
+ terms.append(self.parse_term())
+ return terms
+
+ def parse_term(self) -> Term:
+ deleted = attribute = inserted = False
+
+ while True:
+ match self.peek():
+ case Punct("-"):
+ deleted = True
+ case Punct("@"):
+ attribute = True
+ case Punct("+"):
+ inserted = True
+ case Punct("^"):
+ pass # show despite a hidden rule: makes no difference here
+ case _:
+ break
+ self.take()
+
+ match self.take():
+ case Quoted(text) if inserted:
+ term = Insert(text[1:-1])
+ case Quoted(text):
+ term = Literal(text[1:-1])
+ case Name(text):
+ term = RuleRef(text)
+ case Bracketed(text):
+ term = CharClass(parse_char_class(text))
+ case Punct("("):
+ term = Group(self.parse_alternatives())
+ self.take() # )
+
+ term.deleted = deleted
+ term.attribute = attribute
+
+ match self.peek():
+ case Punct(mark) if mark in Repeat:
+ self.take()
+ term.repeat = Repeat(mark)
+
+ return term
+
+
+def parse_char_class(text: str) -> list[tuple[str, str]]:
+ ranges = []
+ for part in text[1:-1].split(";"):
+ part = part.strip()
+ if not part:
+ continue
+ if match := re.fullmatch(r"'(.)'\s*-\s*'(.)'", part):
+ ranges.append((match.group(1), match.group(2)))
+ else:
+ ranges.extend((c, c) for c in part[1:-1])
+
+ return ranges
+
+
+def walk_terms(alternatives):
+ for sequence in alternatives:
+ for term in sequence:
+ yield term
+ if isinstance(term, Group):
+ yield from walk_terms(term.alternatives)
+
+
+class TermKind(StrEnum):
+ Pattern = "Pattern"
+ Rule = "Rule"
+
+
+START_RULE = "decode"
+
+ANY_NIBBLE = 0xFFFF
+
+
+def class_mask(ranges: list[tuple[str, str]]) -> int:
+ values = {value for first, last in ranges for value in range(int(first, 16), int(last, 16) + 1)}
+ return sum(1 << value for value in values)
+
+
+@dataclass(kw_only=True)
+class NormTerm:
+ repeat: Repeat
+ deleted: bool
+
+
+@dataclass
+class NormPattern(NormTerm):
+ nibbles: list[int]
+
+
+@dataclass
+class NormRule(NormTerm):
+ rule: int
+
+
+@dataclass
+class CompiledTerm:
+ kind: TermKind
+ arg: int # first pattern byte, or rule index
+ length: int
+ deleted: bool
+ repeat: Repeat
+
+
+@dataclass
+class CompiledRule:
+ name: str
+ key: bytes | None
+ alternatives: list[list[CompiledTerm]]
+
+
+class Compiler:
+ def __init__(self, rules: dict[str, Rule]):
+ self.source = dict(rules)
+ self.rules: list[CompiledRule] = []
+ self.index_of: dict[str, int] = {}
+ self.patterns: list[tuple[int, int]] = []
+ self.pattern_at: dict[tuple, int] = {}
+ self.keys = bytearray()
+
+ # @X always points at a rule of the form X>dvk = +'literal'.
+ self.attribute_keys = {
+ name: bytes.fromhex(rule.alternatives[0][0].text)
+ for name, rule in rules.items()
+ if rule.alias == "dvk"
+ }
+
+ def intern_pattern(self, nibbles: list[int]) -> tuple[int, int]:
+ pattern = tuple(zip(nibbles[0::2], nibbles[1::2]))
+ if pattern not in self.pattern_at:
+ self.pattern_at[pattern] = len(self.patterns)
+ self.patterns.extend(pattern)
+ return self.pattern_at[pattern], len(pattern)
+
+ def intern_key(self, key: bytes) -> tuple[int, int]:
+ at = self.keys.find(key)
+ if at < 0:
+ at = len(self.keys)
+ self.keys.extend(key)
+ return at, len(key)
+
+ def reserve(self, name: str) -> int:
+ if name not in self.index_of:
+ self.index_of[name] = len(self.rules)
+ self.rules.append(None)
+ return self.index_of[name]
+
+ def fixed_nibbles(self, term: Term, visiting: tuple = ()) -> list[int] | None:
+ if term.attribute:
+ return None
+
+ if isinstance(term, Literal):
+ return [1 << int(char, 16) for char in term.text]
+
+ if isinstance(term, CharClass):
+ return [class_mask(term.ranges)]
+
+ alternatives = self.body_of(term, visiting)
+ if alternatives is None or len(alternatives) != 1:
+ return None
+
+ nibbles = []
+ for inner in alternatives[0]:
+ if inner.repeat is not Repeat.Once:
+ return None
+ inner_nibbles = self.fixed_nibbles(inner, visiting + (self.name_of(term),))
+ if inner_nibbles is None:
+ return None
+ nibbles.extend(inner_nibbles)
+ return nibbles
+
+ def name_of(self, term: Term) -> str | None:
+ return term.name if isinstance(term, RuleRef) else None
+
+ def body_of(self, term: Term, visiting: tuple) -> list[list[Term]] | None:
+ if isinstance(term, Group):
+ return term.alternatives
+ if not isinstance(term, RuleRef) or term.name in visiting:
+ return None
+
+ rule = self.source[term.name]
+ if rule.alias is not None:
+ return None
+ if any(inner.attribute for inner in walk_terms(rule.alternatives)):
+ return None
+ return rule.alternatives
+
+ def expand(self, term: Term, deleted: bool, owner: str, pending: list[str],
+ visiting: tuple = ()) -> list[NormTerm]:
+ nibbles = self.fixed_nibbles(term, visiting)
+ if nibbles is not None:
+ return [NormPattern(repeat=term.repeat, deleted=deleted,
+ nibbles=self.repeatable(nibbles, term.repeat))]
+
+ # Key-less helpers ('byte', 'word', 'hdr') are inlined, taking the caller's mark.
+ body = self.body_of(term, visiting) if term.repeat is Repeat.Once else None
+ if body is not None and len(body) == 1:
+ inlined = []
+ for inner in body[0]:
+ inlined.extend(self.expand(inner, deleted, owner, pending,
+ visiting + (self.name_of(term),)))
+ return inlined
+
+ return [NormRule(repeat=term.repeat, deleted=deleted,
+ rule=self.reference(term, owner, pending))]
+
+ def reference(self, term: Term, owner: str, pending: list[str]) -> int:
+ match term:
+ case RuleRef(name=name):
+ pending.append(name)
+ return self.reserve(name)
+
+ case Group(alternatives=alternatives):
+ name = f"{owner}__group{len(self.rules)}"
+ self.source[name] = Rule(name=name, alternatives=alternatives)
+ pending.append(name)
+ return self.reserve(name)
+
+ def repeatable(self, nibbles: list[int], repeat: Repeat) -> list[int]:
+ if repeat is Repeat.Once or len(nibbles) % 2 == 0:
+ return nibbles
+ # 'hex*' in a byte-aligned grammar only matches an even count, so repeat the pair.
+ return nibbles + nibbles
+
+ def pack(self, terms: list[NormTerm]) -> list[NormTerm]:
+ packed: list[NormTerm] = []
+ for term in terms:
+ previous = packed[-1] if packed else None
+ joinable = (isinstance(previous, NormPattern) and isinstance(term, NormPattern)
+ and previous.repeat is Repeat.Once and term.repeat is Repeat.Once
+ and previous.deleted == term.deleted)
+ if joinable:
+ previous.nibbles.extend(term.nibbles)
+ continue
+ packed.append(term)
+ return packed
+
+ def compile(self) -> list[CompiledRule]:
+ self.reserve(START_RULE)
+ pending = [START_RULE]
+ seen = set()
+
+ while pending:
+ name = pending.pop(0)
+ if name in seen:
+ continue
+ seen.add(name)
+
+ key = None
+ alternatives = []
+ for sequence in self.source[name].alternatives:
+ terms = []
+ for term in sequence:
+ if term.attribute:
+ key = self.attribute_keys[term.name]
+ continue
+ terms.extend(self.expand(term, term.deleted, name, pending))
+ alternatives.append([self.settle(t) for t in self.pack(terms)])
+
+ self.rules[self.index_of[name]] = CompiledRule(name, key, alternatives)
+ if key is not None:
+ self.intern_key(key)
+
+ return self.rules
+
+ def settle(self, term: NormTerm) -> CompiledTerm:
+ match term:
+ case NormRule(rule=rule):
+ return CompiledTerm(TermKind.Rule, rule, 0, term.deleted, term.repeat)
+
+ case NormPattern(nibbles=nibbles):
+ first, length = self.intern_pattern(nibbles)
+ return CompiledTerm(TermKind.Pattern, first, length, term.deleted, term.repeat)
+
+
+def compile_grammar(text: str) -> Compiler:
+ compiler = Compiler(_Parser(text).parse())
+ compiler.compile()
+ return compiler
+
+
+TERM_KIND = wmbus_ns.enum("IxmlTermKind", is_class=True)
+REPEAT = wmbus_ns.enum("IxmlRepeat", is_class=True)
+
+
+def nibble_values(mask: int) -> Expression:
+ return wmbus_ns.IXML_ANY if mask == ANY_NIBBLE else literal(f"0x{mask:04X}")
+
+
+def emit_tables(compiler: Compiler, tables: Tables, prefix: str) -> Expression:
+ patterns = [
+ Aggregate(("high", nibble_values(high)), ("low", nibble_values(low)))
+ for high, low in compiler.patterns
+ ]
+ keys = [HexInt(byte) for byte in compiler.keys]
+
+ terms: list[Expression] = []
+ alternatives: list[Expression] = []
+ rules: list[Expression] = []
+
+ for rule in compiler.rules:
+ first_alternative = len(alternatives)
+ for sequence in rule.alternatives:
+ first_term = len(terms)
+ for term in sequence:
+ terms.append(Aggregate(
+ ("kind", getattr(TERM_KIND, term.kind)),
+ ("repeat", getattr(REPEAT, term.repeat.name)),
+ ("deleted", term.deleted),
+ ("length", term.length),
+ ("arg", term.arg),
+ ))
+ alternatives.append(
+ Aggregate(("first_term", first_term), ("term_count", len(sequence)))
+ )
+
+ first_key, key_length = compiler.intern_key(rule.key) if rule.key else (0, 0)
+ rules.append(Aggregate(
+ ("first_alternative", first_alternative),
+ ("alternative_count", len(rule.alternatives)),
+ ("first_key_byte", first_key),
+ ("key_length", key_length),
+ ))
+
+ grammar = Aggregate(
+ ("rules", tables.span("IxmlRule", f"{prefix}_rules", rules)),
+ ("alternatives", tables.span("IxmlAlternative", f"{prefix}_alternatives", alternatives)),
+ ("terms", tables.span("IxmlTerm", f"{prefix}_terms", terms)),
+ ("patterns", tables.span("IxmlBytePattern", f"{prefix}_patterns", patterns)),
+ ("keys", tables.span("uint8_t", f"{prefix}_keys", keys)),
+ )
+ return tables.define("IxmlGrammar", f"{prefix}_grammar", grammar)
+
+
+def compile_to_tables(text: str, tables: Tables, prefix: str) -> Expression:
+ return emit_tables(compile_grammar(text), tables, prefix)
diff --git a/components/wmbus_common/drivers/loader/templates/driver.cpp.j2 b/components/wmbus_common/drivers/loader/templates/driver.cpp.j2
new file mode 100644
index 00000000..fa294769
--- /dev/null
+++ b/components/wmbus_common/drivers/loader/templates/driver.cpp.j2
@@ -0,0 +1,33 @@
+// Auto-generated from {{ origin }}; do not edit.
+
+#include
+#include
+#include
+
+#include "esphome/components/wmbus_common/meter.h"
+
+namespace {
+
+using namespace wmbus;
+
+{% for d in declarations %}
+{% if d.kind == "formula" %}
+static double {{ d.symbol }}(const MeterState &state, const Record *dve) {
+ (void) state;
+ (void) dve;
+ return {{ d.value }};
+}
+{% elif d.kind == "array" %}
+static const {{ d.type }} {{ d.symbol }}[] = {{ d.value }};
+{% else %}
+static const {{ d.type }} {{ d.symbol }} = {{ d.value }};
+{% endif %}
+
+{% endfor %}
+} // namespace
+
+namespace wmbus::drivers {
+
+extern const DriverSpec {{ symbol }} = {{ spec }};
+
+} // namespace wmbus::drivers
diff --git a/components/wmbus_common/drivers/loader/templates/registry.h.j2 b/components/wmbus_common/drivers/loader/templates/registry.h.j2
new file mode 100644
index 00000000..4d0898be
--- /dev/null
+++ b/components/wmbus_common/drivers/loader/templates/registry.h.j2
@@ -0,0 +1,26 @@
+// Auto-generated from the loaded drivers list; do not edit.
+#pragma once
+
+#include
+
+namespace wmbus {
+struct DriverSpec;
+}
+
+namespace wmbus::drivers {
+
+{% for symbol in symbols %}
+extern const DriverSpec {{ symbol }};
+{% endfor %}
+
+} // namespace wmbus::drivers
+
+namespace wmbus {
+
+inline constexpr std::array registered_drivers = {
+{% for symbol in symbols %}
+ &drivers::{{ symbol }},
+{% endfor %}
+};
+
+} // namespace wmbus
diff --git a/components/wmbus_common/drivers/loader/units.py b/components/wmbus_common/drivers/loader/units.py
new file mode 100644
index 00000000..e1c85dbe
--- /dev/null
+++ b/components/wmbus_common/drivers/loader/units.py
@@ -0,0 +1,109 @@
+"""StrEnums over tables/*.yaml, so a typo fails at import rather than in C++."""
+
+import math
+from dataclasses import dataclass
+from enum import StrEnum
+from functools import cache
+from pathlib import Path
+
+import yaml
+
+TABLES_DIR = Path(__file__).parents[2] / "tables"
+
+EXPONENT_KEYS = ("s", "m", "kg", "a", "mol", "cd", "k", "c", "f", "month", "year", "ut")
+
+
+@cache
+def table(name: str):
+ return yaml.safe_load((TABLES_DIR / f"{name}.yaml").read_text(encoding="utf-8"))
+
+
+@dataclass(frozen=True)
+class SIExp:
+ exps: tuple[int, ...] = (0,) * len(EXPONENT_KEYS)
+ nonlinear: bool = False
+
+ @staticmethod
+ def from_table(si: dict) -> "SIExp":
+ return SIExp(tuple(si.get(k, 0) for k in EXPONENT_KEYS), si.get("nonlinear", False))
+
+ def mul(self, other: "SIExp") -> "SIExp":
+ return SIExp(tuple(a + b for a, b in zip(self.exps, other.exps)))
+
+ def div(self, other: "SIExp") -> "SIExp":
+ return SIExp(tuple(a - b for a, b in zip(self.exps, other.exps)))
+
+ def sqrt(self) -> "SIExp":
+ # Truncating towards zero, as the C++ division does.
+ return SIExp(tuple(int(a / 2) for a in self.exps))
+
+
+def _str_enum(name: str, names) -> type[StrEnum]:
+ # The functional StrEnum would lowercase the C++ enumerator names.
+ return StrEnum(name, [(n, n) for n in names])
+
+
+# "Any" is the matcher's neutral value, not one of upstream's names, and
+# "Unknown" is the quantity a computed unit has.
+VifRange = _str_enum("VifRange", ["Any"] + list(table("vif_ranges")))
+VifCombinable = _str_enum("VifCombinable", ["Any"] + list(table("vif_combinables")))
+MeasurementType = _str_enum("MeasurementType", ["Any", "Instantaneous", "Minimum", "Maximum", "AtError"])
+Quantity = _str_enum("Quantity", ["Unknown"] + list(table("quantities")))
+Unit = _str_enum("Unit", table("units"))
+# "None" is one of the enumerators and not a name Python can spell.
+VifScaling = _str_enum("VifScaling", ["Auto", "None"])
+DifSignedness = _str_enum("DifSignedness", ["Signed", "Unsigned"])
+
+
+@dataclass(frozen=True)
+class UnitInfo:
+ symbol: str # the field name suffix: total_m3
+ display: str
+ quantity: Quantity
+ si: tuple[float, SIExp] | None # scale and exponents; None for text and dates
+
+
+UNITS = {
+ Unit(name): UnitInfo(
+ u["symbol"],
+ u["display"],
+ Quantity(u["quantity"]),
+ (u["si"]["scale"], SIExp.from_table(u["si"])) if "si" in u else None,
+ )
+ for name, u in table("units").items()
+}
+BY_SYMBOL = {info.symbol: unit for unit, info in UNITS.items()}
+# Upstream reads a unit by its name or by its symbol: DateLT, but kwh.
+Unit._missing_ = classmethod(lambda cls, value: BY_SYMBOL.get(value))
+DEFAULT_UNIT = {Quantity(q): Unit(u) for q, u in table("quantities").items()}
+
+
+def split_unit(name: str) -> tuple[str, Unit | None]:
+ """total_m3 -> (total, M3); a name without a unit suffix stays whole."""
+ vname, _, suffix = name.rpartition("_")
+ if vname and suffix in BY_SYMBOL:
+ return vname, BY_SYMBOL[suffix]
+ return name, None
+
+
+# Upstream's unit conversions count a year as 365.2425 days; only date arithmetic keeps years apart.
+SECONDS_PER_YEAR = 3600.0 * 24.0 * 365.2425
+
+
+def _in_seconds(si: tuple[float, SIExp]) -> tuple[float, tuple[int, ...]]:
+ scale, exp = si
+ exps = dict(zip(EXPONENT_KEYS, exp.exps))
+ years = exps.pop("year")
+ exps["s"] += years
+ return scale * SECONDS_PER_YEAR**years, tuple(exps.values())
+
+
+def unit_factor(src: Unit, dst: Unit) -> float:
+ """NaN where the two do not differ by a factor: text, dates, dBm, temperature scales."""
+ if src == dst:
+ return 1.0
+ a, b = UNITS[src].si, UNITS[dst].si
+ if a is None or b is None or a[1].nonlinear or b[1].nonlinear:
+ return math.nan
+ (a_scale, a_exps), (b_scale, b_exps) = _in_seconds(a), _in_seconds(b)
+ return a_scale / b_scale if a_exps == b_exps else math.nan
diff --git a/components/wmbus_common/drivers/loader/xmq.py b/components/wmbus_common/drivers/loader/xmq.py
new file mode 100644
index 00000000..c3be2b52
--- /dev/null
+++ b/components/wmbus_common/drivers/loader/xmq.py
@@ -0,0 +1,115 @@
+"""The XMQ subset upstream's drivers use; a repeated key becomes a list of dicts."""
+
+import re
+from dataclasses import dataclass, field
+from typing import ClassVar
+
+
+@dataclass(frozen=True)
+class Token:
+ text: str
+ column: int = field(default=0, compare=False)
+ pattern: ClassVar[re.Pattern]
+
+ @classmethod
+ def at(cls, text: str, position: int) -> "Token":
+ column = position - (text.rfind("\n", 0, position) + 1)
+ for kind in cls.__subclasses__():
+ if match := kind.pattern.match(text, position):
+ return kind(match.group(), column)
+
+
+class Blank(Token):
+ pattern = re.compile(r"[ \t\r\n]+|//[^\n]*\n?|/\*.*?\*/", re.DOTALL)
+
+
+class Quoted(Token):
+ """'' is the empty string; a run of N>=3 quotes wraps content ending in the same run."""
+
+ pattern = re.compile(r"""('{3,}|"{3,}|'|")(.*?)\1""", re.DOTALL)
+
+ @property
+ def value(self) -> str:
+ # Continuation lines lose their common indent, at most up to the quote's column.
+ first, *rest = self.pattern.match(self.text).group(2).split("\n")
+ indents = [len(line) - len(line.lstrip(" ")) for line in rest if line.strip()]
+ strip = min(min(indents, default=0), self.column + 1)
+ return "\n".join([first] + [line[strip:] for line in rest])
+
+
+class Punct(Token):
+ pattern = re.compile(r"[{}()=,]")
+
+
+class Bare(Token):
+ pattern = re.compile(r"[^ \t\r\n{}()=]+")
+
+ @property
+ def value(self) -> str:
+ return self.text
+
+
+def tokenize(text: str):
+ position = 0
+ while position < len(text):
+ token = Token.at(text, position)
+ position += len(token.text)
+ if not isinstance(token, Blank):
+ yield token
+
+
+def _add(parent: dict, key: str, value) -> None:
+ if key not in parent:
+ parent[key] = value
+ elif isinstance(parent[key], list):
+ parent[key].append(value)
+ else:
+ parent[key] = [parent[key], value]
+
+
+class _Parser:
+ def __init__(self, text: str):
+ self.tokens = list(tokenize(text))
+ self.pos = 0
+
+ def peek(self) -> Token | None:
+ return self.tokens[self.pos] if self.pos < len(self.tokens) else None
+
+ def take(self) -> Token:
+ token = self.peek()
+ self.pos += 1
+ return token
+
+ def parse_block(self) -> dict:
+ node: dict = {}
+ while (token := self.take()) not in (None, Punct("}")):
+ name = token.value
+ attrs = self.parse_attributes() if self.peek() == Punct("(") else {}
+
+ match self.peek():
+ case Punct("{"):
+ self.take()
+ _add(node, name, {**attrs, **self.parse_block()})
+ case Punct("="):
+ self.take()
+ value = self.take().value
+ _add(node, name, {**attrs, "_content": value} if attrs else value)
+ case _:
+ _add(node, name, attrs or "")
+ return node
+
+ def parse_attributes(self) -> dict:
+ self.take() # (
+ attrs = {}
+ while (token := self.take()) != Punct(")"):
+ if token == Punct(","):
+ continue
+ attrs[token.value] = ""
+ if self.peek() == Punct("="):
+ self.take()
+ attrs[token.value] = self.take().value
+ return attrs
+
+
+def parse_xmq(content: str) -> dict:
+ return _Parser(content).parse_block()
diff --git a/components/wmbus_common/drivers/loader/xmq_loader.py b/components/wmbus_common/drivers/loader/xmq_loader.py
new file mode 100644
index 00000000..f637a2d5
--- /dev/null
+++ b/components/wmbus_common/drivers/loader/xmq_loader.py
@@ -0,0 +1,875 @@
+"""Transpiles an XMQ driver to the C++ tables meter.cpp reads.
+
+A key outside the schemas, or a construct the runtime does not implement, raises;
+what upstream's own loader checks (references, masks, units) is taken as valid.
+"""
+
+from __future__ import annotations
+
+import itertools
+import math
+import operator
+import re
+from dataclasses import dataclass, field, replace
+from enum import StrEnum
+from functools import cache, reduce
+from pathlib import Path
+
+from esphome.config_validation import boolean, ensure_list, hex_int
+from esphome.core import HexInt
+from esphome.cpp_generator import ArrayInitializer, Expression, StructInitializer
+from esphome.cpp_types import nullptr, std_ns
+from voluptuous import All, Any, Coerce, Optional, Remove, Required, Schema
+
+from . import formula as F
+from .cpp import TEMPLATES, Aggregate, Tables, double, wmbus_ns
+from .ixml import compile_to_tables
+from .units import (
+ DEFAULT_UNIT,
+ UNITS,
+ DifSignedness,
+ MeasurementType,
+ Quantity,
+ Unit,
+ VifCombinable,
+ VifRange,
+ VifScaling,
+ table,
+ unit_factor,
+)
+from .xmq import parse_xmq
+
+CSV_LIST = All(str, lambda x: [s.strip() for s in x.split(",") if s.strip()])
+
+
+def first(value):
+ """Upstream reads the first of a repeated key."""
+ return value[0] if isinstance(value, list) else value
+
+
+# XMQ's own vocabularies; every value is also the name of the C++ enumerator.
+
+
+class TestBit(StrEnum):
+ Set = "Set"
+ NotSet = "NotSet"
+
+ @classmethod
+ def _missing_(cls, value):
+ # Upstream reads it with strcasecmp, so both spellings are valid.
+ wanted = str(value).strip().casefold()
+ return next((bit for bit in cls if bit.casefold() == wanted), None)
+
+
+class FieldKind(StrEnum):
+ Numeric = "Numeric"
+ Text = "Text"
+
+
+class ReadableString(StrEnum):
+ Normal = "Normal"
+ Reversed = "Reversed"
+
+
+class TextForm(StrEnum):
+ Hex = "Hex"
+ Readable = "Readable"
+ Reversed = "Reversed"
+ Date = "Date"
+ DateTime = "DateTime"
+
+
+class MapType(StrEnum):
+ BitToString = "BitToString"
+ IndexToString = "IndexToString"
+ DecimalsToString = "DecimalsToString"
+
+
+class Attribute(StrEnum):
+ """DEPRECATED and HIDE are read and ignored; HIDE only shapes upstream's JSON."""
+
+ Required = "REQUIRED"
+ Deprecated = "DEPRECATED"
+ Status = "STATUS"
+ IncludeTplStatus = "INCLUDE_TPL_STATUS"
+ InjectIntoStatus = "INJECT_INTO_STATUS"
+ Hide = "HIDE"
+
+
+class PayloadQuirk(StrEnum):
+ """A driver's transform_payload; the name is the C++ enumerator."""
+
+ DiehlPrios = "diehl_prios"
+ Sanxing609B = "buggy_sanxing_609B"
+ TryQundisDecode = "try_qundis_decode"
+
+
+ANY_RANGE_MEMBERS = {
+ VifRange.AnyVolumeVIF: [VifRange.Volume],
+ VifRange.AnyEnergyVIF: [VifRange.EnergyWh, VifRange.EnergyMJ, VifRange.EnergyMWh, VifRange.EnergyGJ],
+ VifRange.AnyPowerVIF: [VifRange.PowerW, VifRange.PowerJh],
+}
+DATE_RANGES = {VifRange.Date, VifRange.DateTime}
+# The ranges upstream reads as text rather than as hex.
+READABLE_RANGES = {
+ VifRange.EnhancedIdentification, VifRange.FabricationNo, VifRange.HardwareVersion, VifRange.FirmwareVersion,
+ VifRange.Medium, VifRange.Manufacturer, VifRange.ModelVersion, VifRange.SoftwareVersion, VifRange.Customer,
+ VifRange.Location, VifRange.SpecialSupplierInformation, VifRange.ParameterSet,
+}
+
+
+def range_unit(vif_range: VifRange) -> Unit:
+ return Unit(table("vif_ranges")[vif_range]["unit"])
+
+
+def vif_spans(vif_range: VifRange) -> list[tuple[int, int]]:
+ if vif_range is VifRange.Any:
+ return []
+ members = ANY_RANGE_MEMBERS.get(vif_range, [vif_range])
+ return [(table("vif_ranges")[r]["from"], table("vif_ranges")[r]["to"]) for r in members]
+
+
+def combinable_span(combinable) -> tuple[int, int]:
+ if isinstance(combinable, str):
+ span = table("vif_combinables")[VifCombinable(combinable)]
+ return span["from"], span["to"]
+ return int(combinable), int(combinable)
+
+
+def text_form(d: "FieldDef") -> TextForm:
+ vr = d.matcher.vif_range
+ if vr in DATE_RANGES:
+ return TextForm(vr)
+ if d.readable_string is ReadableString.Reversed:
+ return TextForm.Reversed
+ if d.readable_string is ReadableString.Normal or vr in READABLE_RANGES:
+ return TextForm.Readable
+ return TextForm.Hex
+
+
+def frac(s: str):
+ num, _, denom = s.partition("/")
+ return float(num) / float(denom)
+
+
+def base16(value) -> HexInt:
+ """Upstream reads these with strtol(s, NULL, 16): "10" is 0x10."""
+ if isinstance(value, int):
+ return HexInt(value)
+ return HexInt(int(value, 16))
+
+
+MATCHER_SCHEMA = {
+ Optional("measurement_type"): Coerce(MeasurementType),
+ Optional("difvifkey"): str,
+ Optional("vif_range"): Coerce(VifRange),
+ # Read only at field level upstream; a few drivers carry one here anyway.
+ Remove("vif_scaling"): str,
+ Optional("tariff_nr"): All(CSV_LIST, [Coerce(int)]),
+ Optional("subunit_nr"): All(CSV_LIST, [Coerce(int)]),
+ Optional("storage_nr"): All(CSV_LIST, [Coerce(int)]),
+ Optional("index_nr"): Coerce(int),
+ Optional("add_combinable"): ensure_list(Coerce(VifCombinable)),
+ Optional("add_combinable_raw"): ensure_list(hex_int),
+}
+# Upstream ignores these beside a field; `change` only feeds Home Assistant discovery.
+IGNORED_FIELD_KEYS = {
+ Remove("default_message"): str,
+ Remove("null_string"): str,
+ Remove("change"): str,
+ Remove("deprecated_by"): str,
+}
+
+
+MAP_SCHEMA = Schema(
+ {
+ Required("name"): All(first, str),
+ Optional("value"): base16,
+ Optional("bit"): Coerce(int),
+ Optional("info"): str,
+ Required("test"): Coerce(TestBit),
+ }
+)
+
+LOOKUP_SCHEMA = Schema(
+ {
+ Required("name"): All(first, str),
+ Required("map_type"): Coerce(MapType),
+ Required("mask_bits"): base16,
+ Optional("default_message", default=""): str,
+ Optional("map", default=[]): ensure_list(MAP_SCHEMA),
+ Optional("mark_reserved_bits"): boolean,
+ Optional("pre_shift_right"): Coerce(int),
+ }
+)
+
+FIELD_SCHEMA = Schema(
+ {
+ Required("name"): str,
+ Required("quantity"): Coerce(Quantity),
+ Optional("info"): str,
+ Optional("attributes", default=""): All(CSV_LIST, [Coerce(Attribute)]),
+ Optional("display_unit"): Coerce(Unit),
+ Optional("override_vif_unit"): Coerce(Unit),
+ Optional("match"): MATCHER_SCHEMA,
+ Optional("calculate"): str,
+ Optional("vif_scaling"): Coerce(VifScaling),
+ Optional("dif_signedness"): Coerce(DifSignedness),
+ Optional("force_scale"): Any(Coerce(float), frac),
+ Optional("null_value"): Coerce(float),
+ Optional("lookup"): LOOKUP_SCHEMA,
+ Optional("readable_string"): Coerce(ReadableString),
+ Optional("transform_payload"): str,
+ Optional("ixml"): str,
+ Optional("match_entire_payload"): boolean,
+ Optional("match_entire_frame"): boolean,
+ **IGNORED_FIELD_KEYS,
+ }
+)
+
+def manufacturer_code(flag: str) -> int:
+ a, b, c = (ord(ch) - 64 for ch in flag)
+ return a * 1024 + b * 32 + c
+
+
+def parse_mvt(v: list[str]) -> tuple[HexInt, HexInt, HexInt]:
+ """mvt = APA,05,06; * is 0xff, "any"."""
+ m, ver, typ = v
+ if len(m) == 3 and m.isalpha() and m.isupper():
+ mfct = manufacturer_code(m)
+ else:
+ mfct = int(m, 16)
+
+ def part(value):
+ return 0xFF if value.strip() == "*" else int(value, 16)
+
+ return HexInt(mfct), HexInt(part(ver)), HexInt(part(typ))
+
+
+# Upstream fixes the rule's name and type; XMQ supplies the mask and the maps.
+MFCT_TPL_STATUS_SCHEMA = Schema(
+ {
+ Optional("mask_bits", default=0xFF): base16,
+ Optional("default_message", default="OK"): str,
+ Optional("map", default=[]): ensure_list(MAP_SCHEMA),
+ }
+)
+
+COMPACT_FRAME_FORMATS_SCHEMA = Schema({Required("difvif"): ensure_list(str)})
+
+
+LIBRARY_FILE = Path(__file__).resolve().parents[1] / "library.xmq"
+
+
+def as_list(val) -> list:
+ return val if isinstance(val, list) else ([] if val is None else [val])
+
+
+@cache
+def library_fields() -> dict[str, dict]:
+ """By template id and alias, for `library { use = ... }`."""
+ raw = parse_xmq(LIBRARY_FILE.read_text(encoding="utf-8"))
+ fields = {}
+ for template in as_list(raw["library"]["template"]):
+ definition = {"quantity": "Text"} | {k: v for k, v in template.items() if k not in ("id", "aliases")}
+ for name in [template["id"], *template.get("aliases", "").split(",")]:
+ if name.strip():
+ fields[name.strip()] = definition
+ return fields
+
+
+def use_names(use: str) -> list[str]:
+ """`use = a,b` names two library fields, `use = 'a|help'` one whatever the help says."""
+ names, bar, _help = use.partition("|")
+ return [names.strip()] if bar else [name.strip() for name in names.split(",") if name.strip()]
+
+
+def map_key(entry: dict) -> int:
+ """Upstream merges inherited map entries by this key."""
+ return 1 << int(entry["bit"]) if "bit" in entry else base16(entry["value"])
+
+
+def inherit_lookup(f: dict, template: dict) -> dict:
+ """The field's own lookup keys and map entries win; mark_reserved_bits is not inherited."""
+ own = f.get("lookup", {})
+ lent = template.get("lookup", {})
+ lookup = {k: v for k, v in lent.items() if k not in ("map", "mark_reserved_bits")} | own
+ own_map = as_list(own.get("map"))
+ mapped = {map_key(m) for m in own_map}
+ lookup["map"] = own_map + [m for m in as_list(lent.get("map")) if map_key(m) not in mapped]
+ return {k: v for k, v in f.items() if k != "template"} | {"lookup": lookup}
+
+
+def expand_library_and_templates(data: dict) -> dict:
+ driver = data["driver"]
+ templates = {t["name"]: t for t in as_list((driver.get("templates") or {}).get("template_field"))}
+
+ fields = []
+ for use in as_list((driver.get("library") or {}).get("use")):
+ for name in use_names(str(use)):
+ fields.append(library_fields()[name])
+ for f in as_list((driver.get("fields") or {}).get("field")):
+ if "template" in f:
+ f = inherit_lookup(f, templates[f["template"]])
+ fields.append(f)
+
+ rest = {k: v for k, v in driver.items() if k not in ("library", "templates", "fields")}
+ return data | {"driver": rest | ({"fields": {"field": fields}} if fields else {})}
+
+
+SCHEMA = All(
+ expand_library_and_templates,
+ Schema(
+ {
+ Required("driver"): {
+ Required("name"): str,
+ Remove("meter_type"): str,
+ Remove("meter_type_text"): str,
+ Remove("deprecated_by"): str,
+ Optional("info"): str,
+ Remove("manufacturer"): str,
+ Remove("model"): str,
+ Optional("aliases", default=""): CSV_LIST,
+ Optional("link_mode"): str,
+ Optional("force_media_type"): str,
+ Optional("transform_payload"): Coerce(PayloadQuirk),
+ Optional("mfct_tpl_status_bits"): MFCT_TPL_STATUS_SCHEMA,
+ Optional("compact_frame_formats"): COMPACT_FRAME_FORMATS_SCHEMA,
+ Required("default_fields"): str,
+ Required("detect"): {Required("mvt"): ensure_list(CSV_LIST, parse_mvt)},
+ Optional("fields"): {Required("field"): ensure_list(FIELD_SCHEMA)},
+ Remove("flags"): object,
+ Remove("tests"): object,
+ Remove("test"): object,
+ }
+ }
+ ),
+)
+
+
+Range = tuple[int, int]
+
+
+@dataclass
+class Matcher:
+ """Counter ranges are inclusive; None means any."""
+
+ active: bool = False
+ dif_vif_key: str = ""
+ measurement_type: MeasurementType = MeasurementType.Any
+ vif_range: VifRange = VifRange.Any
+ combinables: list[VifCombinable] = field(default_factory=list)
+ combinables_raw: list[HexInt] = field(default_factory=list)
+ storage: Range | None = (0, 0)
+ tariff: Range | None = (0, 0)
+ subunit: Range | None = (0, 0)
+ index_nr: int = 1
+
+ def range_of(self, counter: F.Counter) -> Range | None:
+ return getattr(self, counter.prefix)
+
+ def expects_multiple(self) -> bool:
+ return any(r is not None and r[0] != r[1] for r in (self.storage, self.tariff, self.subunit))
+
+ def counter_range(self, counter: F.Counter) -> range:
+ low, high = self.range_of(counter)
+ return range(low, high + 1)
+
+ def pinned(self, counter: F.Counter, value: int) -> "Matcher":
+ return replace(self, **{counter.prefix: (value, value)})
+
+
+def build_matcher(match: dict | None) -> Matcher:
+ if match is None:
+ return Matcher()
+
+ m = Matcher(active=True)
+
+ if key := match.get("difvifkey"):
+ # set(DifVifKey) ends the build: the remaining keys are ignored.
+ m.dif_vif_key = key
+ return m
+
+ m.measurement_type = match.get("measurement_type", MeasurementType.Any)
+ m.vif_range = match.get("vif_range", VifRange.Any)
+ if index_nr := match.get("index_nr"):
+ m.index_nr = index_nr
+
+ for counter in ("storage", "tariff", "subunit"):
+ values = match.get(f"{counter}_nr")
+ if not values:
+ continue
+ if len(values) > 1:
+ setattr(m, counter, (values[0], values[1]))
+ elif values[0] == -1 and counter != "subunit":
+ # AnyStorageNr/AnyTariffNr; upstream has no such case for subunit.
+ setattr(m, counter, None)
+ else:
+ setattr(m, counter, (values[0], values[0]))
+
+ m.combinables = list(match.get("add_combinable", []))
+ m.combinables_raw = list(match.get("add_combinable_raw", []))
+ return m
+
+
+@dataclass
+class Lookup:
+ name: str
+ map_type: MapType
+ mask: HexInt
+ default_message: str
+ entries: list[tuple[HexInt, str, TestBit]] # (from, to, test)
+ pre_shift_right: int = 0
+
+
+def map_value(entry: dict) -> HexInt:
+ if "bit" in entry:
+ return HexInt(1 << entry["bit"])
+ return entry["value"]
+
+
+def build_lookup(name: str, map_type: MapType, lookup: dict) -> Lookup:
+ """A mask of 0 means the map's bits."""
+ entries = [(map_value(e), e["name"], e["test"]) for e in lookup["map"]]
+ covered = reduce(operator.or_, (value for value, *_ in entries), 0)
+ mask = lookup["mask_bits"] or covered
+ if lookup.get("mark_reserved_bits"):
+ entries += [(HexInt(1 << bit), f"RESERVED_BIT_{bit}", TestBit.Set)
+ for bit in range(64) if mask & ~covered & (1 << bit)]
+ return Lookup(
+ name=name,
+ map_type=map_type,
+ mask=HexInt(mask),
+ default_message=lookup.get("default_message", ""),
+ entries=entries,
+ pre_shift_right=int(lookup.get("pre_shift_right", 0)),
+ )
+
+
+@dataclass
+class FieldDef:
+ name: str # vname; may carry a {template}
+ kind: FieldKind
+ quantity: Quantity
+ display_unit: Unit
+ vif_scaling: VifScaling = VifScaling.Auto
+ dif_signedness: DifSignedness = DifSignedness.Signed
+ attributes: list[Attribute] = field(default_factory=list)
+ scale: float = 1.0
+ source_unit: Unit | None = None # override_vif_unit
+ null_value: float | None = None
+ matcher: Matcher = field(default_factory=Matcher)
+ lookup: Lookup | None = None
+ readable_string: ReadableString | None = None
+ calculate: str | None = None
+ # An ixml carrier runs over every record and keeps the last text.
+ every_match: bool = False
+
+
+def field_def(d: dict) -> FieldDef:
+ quantity = d["quantity"]
+ attributes = d["attributes"]
+ match = d.get("match")
+ calculate = d.get("calculate")
+
+ if quantity is not Quantity.Text:
+ display_unit = d.get("display_unit") or DEFAULT_UNIT[quantity]
+ if calculate:
+ return FieldDef(
+ d["name"], FieldKind.Numeric, quantity, display_unit,
+ attributes=attributes, matcher=build_matcher(match), calculate=calculate,
+ )
+ # override_vif_unit: the record holds this unit whatever its VIF says.
+ source_unit = d.get("override_vif_unit")
+ if match is not None:
+ return FieldDef(
+ d["name"], FieldKind.Numeric, quantity, display_unit,
+ vif_scaling=VifScaling["None"] if source_unit else d.get("vif_scaling", VifScaling.Auto),
+ dif_signedness=d.get("dif_signedness", DifSignedness.Signed),
+ attributes=attributes, scale=float(d.get("force_scale", 1.0)), source_unit=source_unit,
+ null_value=d.get("null_value"), matcher=build_matcher(match),
+ )
+ return FieldDef(d["name"], FieldKind.Numeric, quantity, display_unit,
+ vif_scaling=VifScaling["None"], attributes=attributes)
+
+ f = FieldDef(d["name"], FieldKind.Text, quantity, Unit.TXT, vif_scaling=VifScaling["None"],
+ attributes=attributes, matcher=build_matcher(match))
+ if lookup := d.get("lookup"):
+ f.lookup = build_lookup(lookup["name"], lookup["map_type"], lookup)
+ f.readable_string = d.get("readable_string")
+ f.every_match = "ixml" in d
+ return f
+
+
+@dataclass
+class DecoderDef:
+ """An ixml field; the records it derives are what the fields match."""
+
+ name: str
+ index: int
+ source: str
+ scope: str
+ matcher: Matcher
+ transform: tuple[int, int, int] | None # transform_payload = tpl_aes_cbc_iv,offset,length,tpl_acc_offset
+ required: bool
+
+
+def decoder_def(d: dict, index: int) -> DecoderDef:
+ scope = "Frame" if d.get("match_entire_frame") else "Payload" if d.get("match_entire_payload") else "Entry"
+ matcher = build_matcher(d.get("match"))
+ transform = None
+ if text := d.get("transform_payload"):
+ _, *args = (part.strip() for part in text.split(","))
+ transform = tuple(int(a) for a in args) if args else (0, 0, 0)
+ return DecoderDef(d["name"], index, d["ixml"], scope, matcher, transform, Attribute.Required in d["attributes"])
+
+
+def declared_fields(driver: dict) -> list[dict]:
+ return as_list((driver.get("fields") or {}).get("field"))
+
+
+def collect_definitions(driver: dict) -> list[FieldDef]:
+ """Upstream's registration order; an ixml field over the payload or frame only derives records."""
+ return [
+ field_def(d) for d in declared_fields(driver)
+ if not ("ixml" in d and (d.get("match_entire_payload") or d.get("match_entire_frame")))
+ ]
+
+
+def collect_decoders(driver: dict) -> list[DecoderDef]:
+ return [decoder_def(d, index) for index, d in enumerate(declared_fields(driver)) if "ixml" in d]
+
+
+def select_fields(fields: list[Expanded], include: set[str] | None) -> list[Expanded]:
+ """Adds what the selection needs: formula inputs and, for STATUS, the INJECT_INTO_STATUS fields."""
+ if include is None:
+ return fields
+
+ selected = {i for i, f in enumerate(fields) if f.json_name in include}
+ while True:
+ wanted = set()
+ for i in selected:
+ d = fields[i].definition
+ if d.calculate:
+ for vname, quantity in F.referenced_fields(d.calculate):
+ wanted |= {j for j, e in enumerate(fields)
+ if e.name == vname and e.definition.quantity == quantity}
+ if Attribute.Status in d.attributes:
+ wanted |= {j for j, e in enumerate(fields)
+ if Attribute.InjectIntoStatus in e.definition.attributes}
+ if wanted <= selected:
+ break
+ selected |= wanted
+ return [f for i, f in enumerate(fields) if i in selected]
+
+
+@dataclass
+class Expanded:
+ definition: FieldDef
+ name: str # vname
+ matcher: Matcher
+ extract_all: bool
+ slot: int = -1
+
+ @property
+ def json_name(self) -> str:
+ if self.definition.kind is not FieldKind.Numeric:
+ return self.name
+ return f"{self.name}_{UNITS[self.definition.display_unit].symbol}"
+
+
+def expand(defs: list[FieldDef]) -> list[Expanded]:
+ expanded = []
+ for d in defs:
+ counters = sorted(F.template_counters(d.name))
+ if not counters:
+ expanded.append(Expanded(d, d.name, d.matcher, d.every_match or d.matcher.expects_multiple()))
+ continue
+ ranges = [d.matcher.counter_range(c) for c in counters]
+ for values in itertools.product(*ranges):
+ env = dict(zip(counters, values))
+ matcher = d.matcher
+ for counter, value in env.items():
+ matcher = matcher.pinned(counter, value)
+ expanded.append(Expanded(d, F.expand_template(d.name, env), matcher, d.every_match or d.matcher.expects_multiple()))
+ return expanded
+
+
+def concrete_fields(driver: dict) -> list[Expanded]:
+ return expand(collect_definitions(driver))
+
+
+def assign_slots(fields: list[Expanded]) -> int:
+ """Same kind and JSON name share a slot, as they shared a key in upstream's value map."""
+ slots: dict[tuple[str, str], int] = {}
+ for f in fields:
+ f.slot = slots.setdefault((f.definition.kind, f.json_name), len(slots))
+ return len(slots)
+
+
+FIELD_KIND = wmbus_ns.enum("FieldKind", is_class=True)
+VIF_SCALING = wmbus_ns.enum("VifScaling", is_class=True)
+DIF_SIGNEDNESS = wmbus_ns.enum("DifSignedness", is_class=True)
+MEASUREMENT_TYPE = wmbus_ns.enum("MeasurementType", is_class=True)
+MATCH_SCOPE = wmbus_ns.enum("MatchScope", is_class=True)
+TEXT_FORM = wmbus_ns.enum("TextForm", is_class=True)
+TIME_FORMAT = wmbus_ns.enum("TimeFormat", is_class=True)
+STATUS_ROLE = wmbus_ns.enum("StatusRole", is_class=True)
+TEST_BIT = wmbus_ns.enum("TestBit", is_class=True)
+MAP_TYPE = wmbus_ns.enum("MapType", is_class=True)
+PAYLOAD_QUIRK = wmbus_ns.enum("PayloadQuirk", is_class=True)
+COUNTER_RANGE = wmbus_ns.struct("CounterRange")
+PAYLOAD_TRANSFORM = wmbus_ns.struct("PayloadTransform")
+
+
+def counter_range(m: Matcher, counter: F.Counter) -> Expression:
+ """std::optional is no aggregate, so the range names its type."""
+ r = m.range_of(counter)
+ if r is None:
+ return std_ns.nullopt
+ return StructInitializer(COUNTER_RANGE, ("from", r[0]), ("to", r[1]))
+
+
+def span_row(span: tuple[int, int]) -> Expression:
+ return Aggregate(("from", HexInt(span[0])), ("to", HexInt(span[1])))
+
+
+class SpanTables:
+ def __init__(self, tables: Tables, prefix: str):
+ self.tables = tables
+ self.prefix = prefix
+ self.made: dict[tuple, Expression] = {}
+
+ def span(self, spans: list[tuple[int, int]]) -> Expression:
+ key = tuple(spans)
+ if key not in self.made:
+ self.made[key] = self.tables.span("VifSpan", f"{self.prefix}_{len(self.made)}", [span_row(s) for s in spans])
+ return self.made[key]
+
+
+def matcher_spec(m: Matcher, ranges: SpanTables, combinables: SpanTables) -> Expression:
+ listed = [c for c in m.combinables if c != VifCombinable.Any] + list(m.combinables_raw)
+ return Aggregate(
+ ("dif_vif_key", m.dif_vif_key),
+ ("combinables", combinables.span([combinable_span(c) for c in listed])),
+ ("any_combinable", VifCombinable.Any in m.combinables),
+ ("measurement_type", getattr(MEASUREMENT_TYPE, m.measurement_type)),
+ ("vif_ranges", ranges.span(vif_spans(m.vif_range))),
+ ("storage", counter_range(m, F.Counter.STORAGE_COUNTER)),
+ ("tariff", counter_range(m, F.Counter.TARIFF_COUNTER)),
+ ("subunit", counter_range(m, F.Counter.SUBUNIT_COUNTER)),
+ ("index_nr", m.index_nr),
+ ("active", m.active),
+ )
+
+
+def lookup_spec(tables: Tables, symbol: str, lookups: list[Lookup]) -> Expression:
+ rules = [
+ Aggregate(
+ ("name", lk.name),
+ ("type", getattr(MAP_TYPE, lk.map_type)),
+ ("mask", lk.mask),
+ ("default_message", lk.default_message),
+ ("map", tables.span("LookupMap", f"{symbol}_map_{i}", [
+ Aggregate(("from", frm), ("to", to), ("test", getattr(TEST_BIT, test)))
+ for frm, to, test in lk.entries
+ ])),
+ ("pre_shift_right", lk.pre_shift_right),
+ )
+ for i, lk in enumerate(lookups)
+ ]
+ spec = Aggregate(("rules", tables.span("LookupRule", f"{symbol}_rules", rules)))
+ return tables.define("LookupSpec", symbol, spec)
+
+
+def tpl_status_rules(mfct_bits: dict | None) -> list[Lookup]:
+ """Driver bits reaching bits 0-4 replace the standard rules, else cover the top three (UNKNOWN_XX without them)."""
+ mfct = build_lookup("TPL_STS", MapType.BitToString, mfct_bits) if mfct_bits else None
+ if mfct is not None and mfct.mask & 0x1F:
+ return [mfct]
+ return [
+ Lookup("TPL_STS", MapType.IndexToString, HexInt(0x03), "", [
+ (HexInt(0), "", TestBit.Set), (HexInt(1), "BUSY", TestBit.Set),
+ (HexInt(2), "ERROR", TestBit.Set), (HexInt(3), "ALARM", TestBit.Set),
+ ]),
+ Lookup("TPL_STS", MapType.BitToString, HexInt(0x1C), "", [
+ (HexInt(0x04), "POWER_LOW", TestBit.Set), (HexInt(0x08), "PERMANENT_ERROR", TestBit.Set),
+ (HexInt(0x10), "TEMPORARY_ERROR", TestBit.Set),
+ ]),
+ replace(mfct, mask=HexInt(mfct.mask & 0xE0)) if mfct else Lookup("UNKNOWN", MapType.BitToString, HexInt(0xE0), "", []),
+ ]
+
+
+def json_date(d: FieldDef) -> Expression:
+ """Upstream's JSON writes a numeric field in a date unit as text."""
+ match d.display_unit if d.kind is FieldKind.Numeric else None:
+ case Unit.DateLT:
+ return TIME_FORMAT.Date
+ case Unit.DateTimeLT:
+ return TIME_FORMAT.DateTime
+ case Unit.DateTimeUTC:
+ return TIME_FORMAT.TimestampUTC
+ return std_ns.nullopt
+
+
+def status_role(attributes: list[Attribute]) -> Expression:
+ if Attribute.Status in attributes:
+ return STATUS_ROLE.Holds
+ if Attribute.InjectIntoStatus in attributes:
+ return STATUS_ROLE.Injects
+ return getattr(STATUS_ROLE, "None")
+
+
+def render_decoders(tables: Tables, driver_name: str, decoders: list[DecoderDef],
+ ranges: SpanTables, combinables: SpanTables) -> list[Expression]:
+ rows = []
+ for d in decoders:
+ transform = std_ns.nullopt
+ if d.transform:
+ offset, length, tpl_acc_offset = d.transform
+ transform = StructInitializer(
+ PAYLOAD_TRANSFORM, ("offset", offset), ("length", length), ("tpl_acc_offset", tpl_acc_offset)
+ )
+ rows.append(Aggregate(
+ ("name", d.name),
+ ("grammar", compile_to_tables(d.source, tables, f"ixml_{driver_symbol(driver_name)}_{d.index}")),
+ ("scope", getattr(MATCH_SCOPE, d.scope)),
+ ("matcher", matcher_spec(d.matcher, ranges, combinables)),
+ ("transform", transform),
+ ("required", d.required),
+ ))
+ return rows
+
+
+def render_fields(tables: Tables, fields: list[Expanded], ranges: SpanTables,
+ combinables: SpanTables) -> list[Expression]:
+ rows = []
+ lookups: dict[int, Expression] = {}
+ calculators: dict[int, Expression] = {}
+ any_factors: dict[Unit, Expression] = {}
+
+ # The field stored in the formula's unit wins over one of the same quantity,
+ # so enercal's total_kwh and total_m3 stay apart.
+ by_unit: dict[tuple[str, Unit], Expanded] = {}
+ by_quantity: dict[tuple[str, Quantity], Expanded] = {}
+ for f in fields:
+ if f.definition.kind is FieldKind.Numeric and "{" not in f.definition.name:
+ by_unit.setdefault((f.name, f.definition.display_unit), f)
+ by_quantity.setdefault((f.name, f.definition.quantity), f)
+
+ def resolve(vname: str, unit: Unit) -> F.FieldRef:
+ ref = by_unit.get((vname, unit)) or by_quantity[(vname, UNITS[unit].quantity)]
+ return F.FieldRef(ref.slot, ref.definition.display_unit)
+
+ for index, f in enumerate(fields):
+ d = f.definition
+
+ # A definition expanded into several fields shares one lookup and one formula.
+ lookup = nullptr
+ if d.lookup is not None:
+ if id(d) not in lookups:
+ lookups[id(d)] = lookup_spec(tables, f"lookup_{index}", [d.lookup])
+ lookup = lookups[id(d)]
+
+ calculate = nullptr
+ if d.calculate is not None:
+ if id(d) not in calculators:
+ compiled = F.compile_formula(d.calculate, resolve, d.display_unit)
+ calculators[id(d)] = tables.formula(f"calculate_{index}", compiled)
+ calculate = calculators[id(d)]
+
+ # vif_scale yields the range's unit; the factor to the display unit folds
+ # into scale, or per record from a table for an Any* range.
+ scale = d.scale
+ factors = ArrayInitializer()
+ vr = f.matcher.vif_range
+ if d.source_unit is not None:
+ scale *= unit_factor(d.source_unit, d.display_unit)
+ elif d.kind is FieldKind.Numeric and f.matcher.active and not d.calculate and not f.matcher.dif_vif_key:
+ if vr in ANY_RANGE_MEMBERS:
+ if d.display_unit not in any_factors:
+ rows_for_unit = []
+ for r in ANY_RANGE_MEMBERS[vr]:
+ factor = unit_factor(range_unit(r), d.display_unit)
+ if not math.isnan(factor):
+ rows_for_unit.append(Aggregate(("range", span_row(vif_spans(r)[0])), ("factor", double(factor))))
+ any_factors[d.display_unit] = tables.span("RangeFactor", f"any_{UNITS[d.display_unit].symbol}", rows_for_unit)
+ factors = any_factors[d.display_unit]
+ elif vr is not VifRange.Any and d.quantity is not Quantity.PointInTime:
+ scale *= unit_factor(range_unit(vr), d.display_unit)
+
+ rows.append(Aggregate(
+ ("name", f.json_name),
+ ("kind", getattr(FIELD_KIND, d.kind)),
+ ("vif_scaling", getattr(VIF_SCALING, d.vif_scaling)),
+ ("dif_signedness", getattr(DIF_SIGNEDNESS, d.dif_signedness)),
+ ("status_role", status_role(d.attributes)),
+ ("include_tpl_status", Attribute.IncludeTplStatus in d.attributes),
+ ("scale", double(scale)),
+ ("any_factors", factors),
+ ("null_value", std_ns.nullopt if d.null_value is None else double(d.null_value)),
+ ("matcher", matcher_spec(f.matcher, ranges, combinables)),
+ ("extract_all_matches", f.extract_all),
+ ("lookup", lookup),
+ ("date_record", d.kind is FieldKind.Numeric and f.matcher.vif_range in DATE_RANGES),
+ ("json_date", json_date(d)),
+ ("text_form", getattr(TEXT_FORM, text_form(d) if d.kind is FieldKind.Text else TextForm.Hex)),
+ ("calculate", calculate),
+ ("slot", f.slot),
+ ))
+
+ return rows
+
+
+def driver_symbol(name: str) -> str:
+ return re.sub(r"[^0-9A-Za-z_]", "_", name)
+
+
+def generate_from_definition(driver: dict, origin: str, include_fields: set[str] | None) -> str:
+ tables = Tables()
+ expanded = select_fields(concrete_fields(driver), include_fields)
+ value_slots = assign_slots(expanded)
+
+ # A decoder is generated whatever the selection: its records are what the fields match.
+ ranges = SpanTables(tables, "vif_ranges")
+ combinables = SpanTables(tables, "combinables")
+ decoder_rows = render_decoders(tables, driver["name"], collect_decoders(driver), ranges, combinables)
+ rows = render_fields(tables, expanded, ranges, combinables)
+
+ compact = []
+ if formats := driver.get("compact_frame_formats"):
+ compact = [
+ tables.span("uint8_t", f"compact_format_{i}", [HexInt(b) for b in bytes.fromhex(difvif)])
+ for i, difvif in enumerate(formats["difvif"])
+ ]
+
+ tpl_status = nullptr
+ if any(Attribute.IncludeTplStatus in f.definition.attributes for f in expanded):
+ tpl_status = lookup_spec(tables, "tpl_status", tpl_status_rules(driver.get("mfct_tpl_status_bits")))
+
+ # The spans are taken first, so the tables are declared in this order.
+ fields = tables.span("FieldSpec", "fields", rows)
+ decoders = tables.span("DecoderSpec", "decoders", decoder_rows)
+ compact_formats = tables.span("std::span", "compact_formats", compact)
+ detect = tables.span("DetectSpec", "detect", [
+ Aggregate(("mfct", mfct), ("version", version), ("type", type_))
+ for mfct, version, type_ in driver["detect"]["mvt"]
+ ])
+
+ quirk = driver.get("transform_payload")
+ spec = Aggregate(
+ ("name", driver["name"]),
+ ("detect", detect),
+ ("fields", fields),
+ ("decoders", decoders),
+ ("value_slots", value_slots),
+ ("compact_formats", compact_formats),
+ ("tpl_status", tpl_status),
+ ("payload_quirk", getattr(PAYLOAD_QUIRK, quirk.name) if quirk else std_ns.nullopt),
+ ("force_media_type", driver.get("force_media_type", nullptr)),
+ )
+
+ return TEMPLATES.get_template("driver.cpp.j2").render(
+ origin=origin, symbol=driver_symbol(driver["name"]), declarations=tables.declarations, spec=spec
+ )
+
diff --git a/components/wmbus_common/drivers/src/abbb23.xmq b/components/wmbus_common/drivers/src/abbb23.xmq
new file mode 100644
index 00000000..e15c0bf0
--- /dev/null
+++ b/components/wmbus_common/drivers/src/abbb23.xmq
@@ -0,0 +1,1120 @@
+// Copyright (C) 2019-2023 Fredrik Öhrström (gpl-3.0-or-later)
+driver {
+ name = abbb23
+ meter_type = ElectricityMeter
+ default_fields = name,id,total_energy_consumption_kwh,timestamp
+ detect {
+ mvt = ABB,20,02
+ }
+ fields {
+ field {
+ name = status
+ quantity = Text
+ info = 'Status, error, warning and alarm flags.'
+ attributes = STATUS,INCLUDE_TPL_STATUS
+ }
+ field {
+ name = total_energy_consumption
+ quantity = Energy
+ info = 'Total cumulative active imported energy.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyEnergyVIF
+ add_combinable_raw = 0
+ }
+ }
+ field {
+ name = 'total_energy_consumption_tariff_{tariff_counter}'
+ quantity = Energy
+ info = 'Total cumulative active imported energy per tariff.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyEnergyVIF
+ tariff_nr = 1,4
+ add_combinable_raw = 0
+ }
+ }
+ field {
+ name = total_energy_production
+ quantity = Energy
+ info = 'Total cumulative active exported energy.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyEnergyVIF
+ tariff_nr = 0
+ subunit_nr = 1
+ add_combinable_raw = 0
+ }
+ }
+ field {
+ name = 'total_energy_production_tariff_{tariff_counter}'
+ quantity = Energy
+ info = 'Total cumulative active exported energy per tariff.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyEnergyVIF
+ tariff_nr = 1,4
+ subunit_nr = 1
+ add_combinable_raw = 0
+ }
+ }
+ field {
+ name = active_tariff
+ quantity = Dimensionless
+ info = 'Active tariff.'
+ vif_scaling = None
+ display_unit = nr
+ match {
+ difvifkey = 01FF9300
+ }
+ }
+ field {
+ name = ct_numerator
+ quantity = Dimensionless
+ info = 'Current transformer ratio (numerator).'
+ vif_scaling = None
+ display_unit = factor
+ match {
+ difvifkey = 04FFA015
+ }
+ }
+ field {
+ name = vt_numerator
+ quantity = Dimensionless
+ info = 'Voltage transformer ratio (numerator).'
+ vif_scaling = None
+ display_unit = factor
+ match {
+ difvifkey = 04FFA115
+ }
+ }
+ field {
+ name = ct_denominator
+ quantity = Dimensionless
+ info = 'Current transformer ratio (denominator).'
+ vif_scaling = None
+ display_unit = factor
+ match {
+ difvifkey = 04FFA215
+ }
+ }
+ field {
+ name = vt_denominator
+ quantity = Dimensionless
+ info = 'Voltage transformer ratio (denominator).'
+ vif_scaling = None
+ display_unit = factor
+ match {
+ difvifkey = 04FFA315
+ }
+ }
+ field {
+ name = error_flags
+ quantity = Text
+ info = 'Error flags.'
+ attributes = INJECT_INTO_STATUS
+ match {
+ measurement_type = Instantaneous
+ difvifkey = 07FFA600
+ }
+ lookup {
+ name = ERROR_FLAGS
+ map_type = BitToString
+ mask_bits = 0xffffffffffffffff
+ default_message = OK
+ }
+ }
+ field {
+ name = warning_flags
+ quantity = Text
+ info = 'Warning flags.'
+ attributes = INJECT_INTO_STATUS
+ match {
+ measurement_type = Instantaneous
+ difvifkey = 07FFA700
+ }
+ lookup {
+ name = WARNING_FLAGS
+ map_type = BitToString
+ mask_bits = 0xffffffffffffffff
+ default_message = OK
+ }
+ }
+ field {
+ name = information_flags
+ quantity = Text
+ info = 'Information flags.'
+ match {
+ measurement_type = Instantaneous
+ difvifkey = 07FFA800
+ }
+ lookup {
+ name = INFORMATION_FLAGS
+ map_type = BitToString
+ mask_bits = 0xffffffffffffffff
+ default_message = ''
+ }
+ }
+ field {
+ name = alarm_flags
+ quantity = Text
+ info = 'Alarm flags.'
+ attributes = INJECT_INTO_STATUS
+ match {
+ measurement_type = Instantaneous
+ difvifkey = 07FFA900
+ }
+ lookup {
+ name = ALARM_FLAGS
+ map_type = BitToString
+ mask_bits = 0xfffffffffffffff
+ default_message = OK
+ }
+ }
+ field {
+ name = unknown_vif_FFAD
+ quantity = Text
+ info = 'Unknown byte.'
+ match {
+ measurement_type = Instantaneous
+ difvifkey = 01FFAD00
+ }
+ lookup {
+ name = UNKNOWN
+ map_type = BitToString
+ mask_bits = 0xff
+ default_message = OK
+ }
+ }
+ field {
+ name = firmware_version
+ quantity = Text
+ info = 'Firmware version.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = FirmwareVersion
+ add_combinable_raw = 0
+ }
+ }
+ field {
+ name = product_no
+ quantity = Text
+ readable_string = Reversed
+ info = 'The meter device product number.'
+ match {
+ difvifkey = 0DFFAA00
+ }
+ }
+ field {
+ name = power_fail
+ quantity = Dimensionless
+ info = 'Power fail counter.'
+ vif_scaling = None
+ match {
+ difvifkey = 04FF9800
+ }
+ }
+ field {
+ name = active_consumption
+ quantity = Power
+ info = 'Instantaneous total active imported power.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyPowerVIF
+ add_combinable_raw = 0
+ }
+ }
+ field {
+ name = active_consumption_l1
+ quantity = Power
+ info = 'Instantaneous active imported power for L1 phase.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyPowerVIF
+ add_combinable_raw = 0x7f01
+ }
+ }
+ field {
+ name = active_consumption_l2
+ quantity = Power
+ info = 'Instantaneous active imported power for L2 phase.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyPowerVIF
+ add_combinable_raw = 0x7f02
+ }
+ }
+ field {
+ name = active_consumption_l3
+ quantity = Power
+ info = 'Instantaneous active imported power for L3 phase.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyPowerVIF
+ add_combinable_raw = 0x7f03
+ }
+ }
+ field {
+ name = reactive_consumption
+ quantity = Power
+ info = 'Instantaneous total reactive imported power.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyPowerVIF
+ subunit_nr = 2
+ add_combinable_raw = 0
+ }
+ }
+ field {
+ name = reactive_consumption_l1
+ quantity = Power
+ info = 'Instantaneous reactive imported power for L1 phase.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyPowerVIF
+ subunit_nr = 2
+ add_combinable_raw = 0x7f01
+ }
+ }
+ field {
+ name = reactive_consumption_l2
+ quantity = Power
+ info = 'Instantaneous reactive imported power for L2 phase.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyPowerVIF
+ subunit_nr = 2
+ add_combinable_raw = 0x7f02
+ }
+ }
+ field {
+ name = reactive_consumption_l3
+ quantity = Power
+ info = 'Instantaneous reactive imported power for L3 phase.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyPowerVIF
+ subunit_nr = 2
+ add_combinable_raw = 0x7f03
+ }
+ }
+ field {
+ name = apparent_consumption
+ quantity = Power
+ info = 'Instantaneous total apparent imported power.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyPowerVIF
+ subunit_nr = 4
+ add_combinable_raw = 0
+ }
+ }
+ field {
+ name = apparent_consumption_l1
+ quantity = Power
+ info = 'Instantaneous apparent imported power for L1 phase.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyPowerVIF
+ subunit_nr = 4
+ add_combinable_raw = 0x7f01
+ }
+ }
+ field {
+ name = apparent_consumption_l2
+ quantity = Power
+ info = 'Instantaneous apparent imported power for L2 phase.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyPowerVIF
+ subunit_nr = 4
+ add_combinable_raw = 0x7f02
+ }
+ }
+ field {
+ name = apparent_consumption_l3
+ quantity = Power
+ info = 'Instantaneous apparent imported power for L3 phase.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyPowerVIF
+ subunit_nr = 4
+ add_combinable_raw = 0x7f03
+ }
+ }
+ field {
+ name = voltage_l1_n
+ quantity = Voltage
+ info = 'Instantaneous voltage between L1 and neutral.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = Voltage
+ add_combinable_raw = 0x7f01
+ }
+ }
+ field {
+ name = voltage_l2_n
+ quantity = Voltage
+ info = 'Instantaneous voltage between L2 and neutral.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = Voltage
+ add_combinable_raw = 0x7f02
+ }
+ }
+ field {
+ name = voltage_l3_n
+ quantity = Voltage
+ info = 'Instantaneous voltage between L3 and neutral.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = Voltage
+ add_combinable_raw = 0x7f03
+ }
+ }
+ field {
+ name = voltage_l1_l2
+ quantity = Voltage
+ info = 'Instantaneous voltage between L1 and L2.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = Voltage
+ add_combinable_raw = 0x7f05
+ }
+ }
+ field {
+ name = voltage_l2_l3
+ quantity = Voltage
+ info = 'Instantaneous voltage between L2 and L3.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = Voltage
+ add_combinable_raw = 0x7f06
+ }
+ }
+ field {
+ name = voltage_l3_l1
+ quantity = Voltage
+ info = 'Instantaneous voltage between L3 and L1.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = Voltage
+ add_combinable_raw = 0x7f07
+ }
+ }
+ field {
+ name = current_l1
+ quantity = Amperage
+ info = 'Instantaneous current in the L1 phase.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = Amperage
+ add_combinable_raw = 0x7f01
+ }
+ }
+ field {
+ name = current_l2
+ quantity = Amperage
+ info = 'Instantaneous current in the L2 phase.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = Amperage
+ add_combinable_raw = 0x7f02
+ }
+ }
+ field {
+ name = current_l3
+ quantity = Amperage
+ info = 'Instantaneous current in the L3 phase.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = Amperage
+ add_combinable_raw = 0x7f03
+ }
+ }
+ field {
+ name = frequency
+ quantity = Frequency
+ info = 'Frequency of AC.'
+ vif_scaling = None
+ force_scale = 0.01
+ display_unit = hz
+ match {
+ difvifkey = 0AFFD900
+ }
+ }
+ field {
+ name = power
+ quantity = Dimensionless
+ info = 'Power factor.'
+ vif_scaling = None
+ force_scale = 0.001
+ display_unit = factor
+ match {
+ difvifkey = 02FFE000
+ }
+ }
+ field {
+ name = power_l1
+ quantity = Dimensionless
+ info = 'Power factor for phase L1.'
+ vif_scaling = None
+ force_scale = 0.001
+ display_unit = factor
+ match {
+ difvifkey = 02FFE0FF8100
+ }
+ }
+ field {
+ name = power_l2
+ quantity = Dimensionless
+ info = 'Power factor for phase L2.'
+ vif_scaling = None
+ force_scale = 0.001
+ display_unit = factor
+ match {
+ difvifkey = 02FFE0FF8200
+ }
+ }
+ field {
+ name = power_l3
+ quantity = Dimensionless
+ info = 'Power factor for phase L3.'
+ vif_scaling = None
+ force_scale = 0.001
+ display_unit = factor
+ match {
+ difvifkey = 02FFE0FF8300
+ }
+ }
+ field {
+ name = power_phase_angle
+ quantity = Angle
+ info = 'Total power phase angle.'
+ vif_scaling = None
+ force_scale = 0.1
+ display_unit = deg
+ match {
+ difvifkey = 02FFD200
+ }
+ }
+ field {
+ name = phase_angle_power_l1
+ quantity = Angle
+ info = 'Power phase angle for phase L1.'
+ vif_scaling = None
+ force_scale = 0.1
+ display_unit = deg
+ match {
+ difvifkey = 02FFD2FF8100
+ }
+ }
+ field {
+ name = phase_angle_power_l2
+ quantity = Angle
+ info = 'Power phase angle for phase L2.'
+ vif_scaling = None
+ force_scale = 0.1
+ display_unit = deg
+ match {
+ difvifkey = 02FFD2FF8200
+ }
+ }
+ field {
+ name = phase_angle_power_l3
+ quantity = Angle
+ info = 'Power phase angle for phase L3.'
+ vif_scaling = None
+ force_scale = 0.1
+ display_unit = deg
+ match {
+ difvifkey = 02FFD2FF8300
+ }
+ }
+ field {
+ name = total_reactive_energy_consumption
+ quantity = Reactive_Energy
+ info = 'Total cumulative reactive kvarh imported energy.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyEnergyVIF
+ subunit_nr = 2
+ }
+ }
+ field {
+ name = 'total_reactive_energy_consumption_tariff_{tariff_counter}'
+ quantity = Energy
+ info = 'Total cumulative reactive kvarh imported energy per tariff.'
+ display_unit = kvarh
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyEnergyVIF
+ subunit_nr = 2
+ tariff_nr = 1,4
+ }
+ }
+ field {
+ name = total_reactive_energy_production
+ quantity = Reactive_Energy
+ info = 'Total cumulative reactive kvarh exported energy.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyEnergyVIF
+ subunit_nr = 3
+ }
+ }
+ field {
+ name = 'total_reactive_energy_production_tariff_{tariff_counter}'
+ quantity = Energy
+ info = 'Total cumulative reactive kvarh exported energy per tariff.'
+ display_unit = kvarh
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyEnergyVIF
+ subunit_nr = 3
+ tariff_nr = 1,4
+ }
+ }
+ field {
+ name = current_quadrant
+ quantity = Dimensionless
+ info = 'The quadrant in which the current is measured.'
+ vif_scaling = None
+ display_unit = nr
+ match {
+ difvifkey = 01FF9700
+ }
+ }
+ field {
+ name = current_quadrant_l1
+ quantity = Dimensionless
+ info = 'The quadrant in which the current is measured for phase L1.'
+ vif_scaling = None
+ display_unit = nr
+ match {
+ difvifkey = 01FF97FF8100
+ }
+ }
+ field {
+ name = current_quadrant_l2
+ quantity = Dimensionless
+ info = 'The quadrant in which the current is measured for phase L2.'
+ vif_scaling = None
+ display_unit = nr
+ match {
+ difvifkey = 01FF97FF8200
+ }
+ }
+ field {
+ name = current_quadrant_l3
+ quantity = Dimensionless
+ info = 'The quadrant in which the current is measured for phase L3.'
+ vif_scaling = None
+ display_unit = nr
+ match {
+ difvifkey = 01FF97FF8300
+ }
+ }
+ field {
+ name = 'digital_output_{subunit_counter}'
+ quantity = Text
+ info = 'The state for output register 1-2.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = DigitalOutput
+ storage_nr = 0
+ tariff_nr = 0
+ subunit_nr = 1,2
+ }
+ lookup {
+ name = OUTPUT
+ map_type = BitToString
+ mask_bits = 0xff
+ }
+ }
+ field {
+ name = 'digital_input_{subunit_counter-2counter}'
+ quantity = Text
+ info = 'The state for input register 1-2.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = DigitalInput
+ subunit_nr = 3,4
+ }
+ lookup {
+ name = INPUT
+ map_type = BitToString
+ mask_bits = 0xff
+ }
+ }
+ field {
+ name = 'digital_historic_input_{subunit_counter-2counter}'
+ quantity = Text
+ info = 'The state for input register 3-4.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = DigitalInput
+ storage_nr = 1
+ subunit_nr = 3,4
+ }
+ lookup {
+ name = INPUT
+ map_type = BitToString
+ mask_bits = 0xff
+ }
+ }
+ field {
+ name = 'digital_input_{subunit_counter-2counter}'
+ quantity = Dimensionless
+ info = 'Number of times input 1-2 counted a 1.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = CumulationCounter
+ subunit_nr = 3,4
+ }
+ }
+ field {
+ name = resettable_energy_consumption
+ quantity = Energy
+ info = 'Resettable cumulative active imported energy.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyEnergyVIF
+ add_combinable_raw = 0x7f72
+ }
+ }
+ field {
+ name = resettable_energy_production
+ quantity = Energy
+ info = 'Resettable cumulative active exported energy.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyEnergyVIF
+ subunit_nr = 1
+ add_combinable_raw = 0x7f72
+ }
+ }
+ field {
+ name = resettable_reactive_energy_consumption
+ quantity = Energy
+ info = 'Resettable cumulative reactive imported energy.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyEnergyVIF
+ subunit_nr = 2
+ add_combinable_raw = 0x7f72
+ }
+ }
+ field {
+ name = resettable_reactive_energy_production
+ quantity = Energy
+ info = 'Resettable cumulative reactive exported energy.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyEnergyVIF
+ subunit_nr = 3
+ add_combinable_raw = 0x7f72
+ }
+ }
+ field {
+ name = energy_co2
+ quantity = Mass
+ info = 'Energy in co2.'
+ vif_scaling = None
+ force_scale = 0.01
+ display_unit = kg
+ match {
+ difvifkey = 0EFFF9C400
+ }
+ }
+ field {
+ name = co2_conversion
+ quantity = Dimensionless
+ info = 'CO2 conversion factor (kg * 10-3 /kWh).'
+ vif_scaling = None
+ display_unit = factor
+ match {
+ difvifkey = 04FFA400
+ }
+ }
+ field {
+ name = energy_currency
+ quantity = Dimensionless
+ info = 'Energy in currency.'
+ vif_scaling = None
+ match {
+ difvifkey = 0EFFF9C900
+ }
+ }
+ field {
+ name = currency_conversion
+ quantity = Dimensionless
+ info = 'Currency conversion factor (curr * 10-3 /kWh).'
+ vif_scaling = None
+ display_unit = factor
+ match {
+ difvifkey = 04FFA500
+ }
+ }
+ field {
+ name = total_apparent_energy_consumption
+ quantity = Apparent_Energy
+ info = 'Total cumulative apparent kvah imported energy.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyEnergyVIF
+ subunit_nr = 4
+ }
+ }
+ field {
+ name = total_apparent_energy_production
+ quantity = Apparent_Energy
+ info = 'Total cumulative apparent kvah exported energy.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyEnergyVIF
+ subunit_nr = 5
+ }
+ }
+ field {
+ name = total_energy_consumption_l1
+ quantity = Energy
+ info = 'Total imported active energy for L1 phase.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyEnergyVIF
+ add_combinable_raw = 0x7f01
+ }
+ }
+ field {
+ name = total_energy_consumption_l2
+ quantity = Energy
+ info = 'Total imported active energy for L2 phase.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyEnergyVIF
+ add_combinable_raw = 0x7f02
+ }
+ }
+ field {
+ name = total_energy_consumption_l3
+ quantity = Energy
+ info = 'Total imported active energy for L3 phase.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyEnergyVIF
+ add_combinable_raw = 0x7f03
+ }
+ }
+ field {
+ name = total_reactive_energy_consumption_l1
+ quantity = Energy
+ info = 'Total imported reactive energy for L1 phase.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyEnergyVIF
+ subunit_nr = 2
+ add_combinable_raw = 0x7f01
+ }
+ }
+ field {
+ name = total_reactive_energy_consumption_l2
+ quantity = Energy
+ info = 'Total imported reactive energy for L2 phase.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyEnergyVIF
+ subunit_nr = 2
+ add_combinable_raw = 0x7f02
+ }
+ }
+ field {
+ name = total_reactive_energy_consumption_l3
+ quantity = Energy
+ info = 'Total imported reactive energy for L3 phase.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyEnergyVIF
+ subunit_nr = 2
+ add_combinable_raw = 0x7f03
+ }
+ }
+ field {
+ name = total_apparent_energy_consumption_l1
+ quantity = Energy
+ info = 'Total imported apparent energy for L1 phase.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyEnergyVIF
+ subunit_nr = 4
+ add_combinable_raw = 0x7f01
+ }
+ }
+ field {
+ name = total_apparent_energy_consumption_l2
+ quantity = Energy
+ info = 'Total imported apparent energy for L2 phase.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyEnergyVIF
+ subunit_nr = 4
+ add_combinable_raw = 0x7f02
+ }
+ }
+ field {
+ name = total_apparent_energy_consumption_l3
+ quantity = Energy
+ info = 'Total imported apparent energy for L3 phase.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyEnergyVIF
+ subunit_nr = 4
+ add_combinable_raw = 0x7f03
+ }
+ }
+ field {
+ name = total_energy_production_l1
+ quantity = Energy
+ info = 'Total exported active energy for L1 phase.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyEnergyVIF
+ subunit_nr = 1
+ add_combinable_raw = 0x7f01
+ }
+ }
+ field {
+ name = total_energy_production_l2
+ quantity = Energy
+ info = 'Total exported active energy for L2 phase.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyEnergyVIF
+ subunit_nr = 1
+ add_combinable_raw = 0x7f02
+ }
+ }
+ field {
+ name = total_energy_production_l3
+ quantity = Energy
+ info = 'Total exported active energy for L3 phase.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyEnergyVIF
+ subunit_nr = 1
+ add_combinable_raw = 0x7f03
+ }
+ }
+ field {
+ name = total_reactive_energy_production_l1
+ quantity = Energy
+ info = 'Total exported reactive energy for L1 phase.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyEnergyVIF
+ subunit_nr = 3
+ add_combinable_raw = 0x7f01
+ }
+ }
+ field {
+ name = total_reactive_energy_production_l2
+ quantity = Energy
+ info = 'Total exported reactive energy for L2 phase.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyEnergyVIF
+ subunit_nr = 3
+ add_combinable_raw = 0x7f02
+ }
+ }
+ field {
+ name = total_reactive_energy_production_l3
+ quantity = Energy
+ info = 'Total exported reactive energy for L3 phase.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyEnergyVIF
+ subunit_nr = 3
+ add_combinable_raw = 0x7f03
+ }
+ }
+ field {
+ name = total_apparent_energy_production_l1
+ quantity = Energy
+ info = 'Total exported apparent energy for L1 phase.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyEnergyVIF
+ subunit_nr = 5
+ add_combinable_raw = 0x7f01
+ }
+ }
+ field {
+ name = total_apparent_energy_production_l2
+ quantity = Energy
+ info = 'Total exported apparent energy for L2 phase.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyEnergyVIF
+ subunit_nr = 5
+ add_combinable_raw = 0x7f02
+ }
+ }
+ field {
+ name = total_apparent_energy_production_l3
+ quantity = Energy
+ info = 'Total exported apparent energy for L3 phase.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyEnergyVIF
+ subunit_nr = 5
+ add_combinable_raw = 0x7f03
+ }
+ }
+ field {
+ name = total_net_energy
+ quantity = Energy
+ info = 'Active net energy total.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyEnergyVIF
+ subunit_nr = 6
+ }
+ }
+ field {
+ name = total_net_energy_l1
+ quantity = Energy
+ info = 'Active net energy total for phase L1.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyEnergyVIF
+ subunit_nr = 6
+ add_combinable_raw = 0x7f01
+ }
+ }
+ field {
+ name = total_net_energy_l2
+ quantity = Energy
+ info = 'Active net energy total for phase L2.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyEnergyVIF
+ subunit_nr = 6
+ add_combinable_raw = 0x7f02
+ }
+ }
+ field {
+ name = total_net_energy_l3
+ quantity = Energy
+ info = 'Active net energy total for phase L3.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyEnergyVIF
+ subunit_nr = 6
+ add_combinable_raw = 0x7f03
+ }
+ }
+ field {
+ name = total_net_reactive_energy
+ quantity = Energy
+ info = 'Active net reactive energy total.'
+ display_unit = kvarh
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyEnergyVIF
+ subunit_nr = 7
+ }
+ }
+ field {
+ name = total_net_reactive_energy_l1
+ quantity = Energy
+ info = 'Active net reactive energy total for phase L1.'
+ display_unit = kvarh
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyEnergyVIF
+ subunit_nr = 7
+ add_combinable_raw = 0x7f01
+ }
+ }
+ field {
+ name = total_net_reactive_energy_l2
+ quantity = Energy
+ info = 'Active net reactive energy total for phase L2.'
+ display_unit = kvarh
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyEnergyVIF
+ subunit_nr = 7
+ add_combinable_raw = 0x7f02
+ }
+ }
+ field {
+ name = total_net_reactive_energy_l3
+ quantity = Energy
+ info = 'Active net reactive energy total for phase L3.'
+ display_unit = kvarh
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyEnergyVIF
+ subunit_nr = 7
+ add_combinable_raw = 0x7f03
+ }
+ }
+ field {
+ name = total_net_apparent_energy
+ quantity = Energy
+ info = 'Active net apparent energy total.'
+ display_unit = kvah
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyEnergyVIF
+ subunit_nr = 8
+ }
+ }
+ field {
+ name = total_net_apparent_energy_l1
+ quantity = Energy
+ info = 'Active net apparent energy total for phase L1.'
+ display_unit = kvah
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyEnergyVIF
+ subunit_nr = 8
+ add_combinable_raw = 0x7f01
+ }
+ }
+ field {
+ name = total_net_apparent_energy_l2
+ quantity = Energy
+ info = 'Active net apparent energy total for phase L2.'
+ display_unit = kvah
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyEnergyVIF
+ subunit_nr = 8
+ add_combinable_raw = 0x7f02
+ }
+ }
+ field {
+ name = total_net_apparent_energy_l3
+ quantity = Energy
+ info = 'Active net apparent energy total for phase L3.'
+ display_unit = kvah
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyEnergyVIF
+ subunit_nr = 8
+ add_combinable_raw = 0x7f03
+ }
+ }
+ }
+ tests {
+ test {
+ args = 'ABBmeter abbb23 33221100 NOKEY'
+ telegram = 844442040011223320027A3E000020_0E840017495200000004FFA0150000000004FFA1150000000004FFA2150000000004FFA3150000000007FFA600000000000000000007FFA700000000000000000007FFA800000000000000000007FFA90000000000000000000DFD8E0007302E38322E31420DFFAA000B3030312D313131203332421F
+ json = '{"_":"telegram","media":"electricity","driver":"abbb23","name":"ABBmeter","id":"33221100","ct_denominator_factor":0,"ct_numerator_factor":0,"total_energy_consumption_kwh":5249.17,"vt_denominator_factor":0,"vt_numerator_factor":0,"alarm_flags":"OK","error_flags":"OK","firmware_version":"B1.28.0","information_flags":"","product_no":"001-111 32B","status":"OK","warning_flags":"OK","timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'ABBmeter;33221100;5249.17;1111-11-11 11:11.11'
+ }
+ }
+}
diff --git a/components/wmbus_common/drivers/src/actislink.xmq b/components/wmbus_common/drivers/src/actislink.xmq
new file mode 100644
index 00000000..33b1b7b8
--- /dev/null
+++ b/components/wmbus_common/drivers/src/actislink.xmq
@@ -0,0 +1,23 @@
+// Copyright (C) 2026 Fredrik Öhrström (gpl-3.0-or-later)
+driver {
+ name = actislink
+ meter_type = WaterMeter
+ default_fields = name,id,total_m3,timestamp
+ manufacturer = Sensus
+ model = iperl
+ detect {
+ mvt = ALK,04,07
+ }
+ library {
+ use = total_m3
+ }
+ tests {
+ test {
+ args = 'GURKA actislink ANYID 0102030405060708'
+ comment = 'Not sure if this is a real telegram or generated for the DES development.'
+ telegram = 1E468B050100000004077A01001002BBD1EAFD0222F871CB70DA16C78AE098
+ json = '{"_": "telegram","id": "00000001","media": "water","driver": "actislink","name": "GURKA","timestamp": "1111-11-11T11:11:11Z","total_m3": 1}'
+ fields = 'GURKA;00000001;1;1111-11-11 11:11.11'
+ }
+ }
+}
diff --git a/components/wmbus_common/drivers/src/aerius.xmq b/components/wmbus_common/drivers/src/aerius.xmq
new file mode 100644
index 00000000..f5b0f756
--- /dev/null
+++ b/components/wmbus_common/drivers/src/aerius.xmq
@@ -0,0 +1,69 @@
+// Copyright (C) 2017-2023 Fredrik Öhrström (gpl-3.0-or-later)
+driver {
+ name = aerius
+ meter_type = GasMeter
+ default_fields = name,id,total_m3,timestamp
+ detect {
+ mvt = DME,30,03
+ }
+ fields {
+ field {
+ name = total
+ quantity = Volume
+ info = 'The total gas consumption recorded by this meter.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = Volume
+ add_combinable = ValueAtBaseCondC
+ }
+ }
+ field {
+ name = flow
+ quantity = Flow
+ info = 'The current gas flow.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = VolumeFlow
+ }
+ }
+ field {
+ name = temperature
+ quantity = Temperature
+ info = 'The current temperature.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = FlowTemperature
+ }
+ }
+ field {
+ name = target
+ quantity = PointInTime
+ info = 'Date time when previous billing period ended.'
+ display_unit = datetime
+ match {
+ measurement_type = Instantaneous
+ vif_range = DateTime
+ storage_nr = 3
+ }
+ }
+ field {
+ name = target
+ quantity = Volume
+ info = 'The total gas consumption recorded when the previous billing period ended.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = Volume
+ storage_nr = 3
+ add_combinable = ValueAtBaseCondC
+ }
+ }
+ }
+ tests {
+ test {
+ args = 'aerius_gas aerius 99657098 NOKEY'
+ telegram = 2E44A5119870659930037A060020052F2F_0C933E842784060A3B00000A5A5901C4016D3B37DF2CCC01933E24032606
+ json = '{"_":"telegram","media":"gas","driver":"aerius","name":"aerius_gas","id":"99657098","total_m3":6842.784,"flow_m3h":0,"temperature_c":15.9,"target_datetime":"2022-12-31 23:59","target_m3":6260.324,"timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'aerius_gas;99657098;6842.784;1111-11-11 11:11.11'
+ }
+ }
+}
diff --git a/components/wmbus_common/drivers/src/amiplus.xmq b/components/wmbus_common/drivers/src/amiplus.xmq
new file mode 100644
index 00000000..7da8474a
--- /dev/null
+++ b/components/wmbus_common/drivers/src/amiplus.xmq
@@ -0,0 +1,350 @@
+// Copyright (C) 2019-2025 Fredrik Öhrström (gpl-3.0-or-later)
+driver {
+ name = amiplus
+ meter_type = ElectricityMeter
+ default_fields = name,id,total_energy_consumption_kwh,current_power_consumption_kw,total_energy_production_kwh,current_power_production_kw,voltage_at_phase_1_v,voltage_at_phase_2_v,voltage_at_phase_3_v,total_energy_consumption_tariff_1_kwh,total_energy_consumption_tariff_2_kwh,total_energy_consumption_tariff_3_kwh,total_energy_production_tariff_1_kwh,total_energy_production_tariff_2_kwh,total_energy_production_tariff_3_kwh,timestamp
+ detect {
+ mvt = APA,02,02
+ mvt = DEV,02,37
+ mvt = DEV,00,02
+ mvt = DEV,01,02
+ mvt = NES,03,02
+ // Apator Otus 1/3 — frames with APA decoded here, frames with APT unknown.
+ mvt = APA,01,02
+ mvt = APT,01,02
+ // Apator Gama 350.
+ mvt = EGM,01,02
+ }
+ fields {
+ field {
+ name = total_energy_consumption
+ quantity = Energy
+ info = 'The total energy consumption recorded by this meter.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyEnergyVIF
+ }
+ }
+ field {
+ name = current_power_consumption
+ quantity = Power
+ info = 'Current power consumption.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ measurement_type = Instantaneous
+ vif_range = PowerW
+ }
+ }
+ field {
+ name = total_energy_production
+ quantity = Energy
+ info = 'The total energy production recorded by this meter.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyEnergyVIF
+ add_combinable = BackwardFlow
+ }
+ }
+ field {
+ name = current_power_production
+ quantity = Power
+ info = 'Current power production.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ measurement_type = Instantaneous
+ vif_range = PowerW
+ add_combinable = BackwardFlow
+ }
+ }
+ field {
+ name = voltage_at_phase_1
+ quantity = Voltage
+ info = 'Voltage at phase L1.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ measurement_type = Instantaneous
+ vif_range = Voltage
+ add_combinable = AtPhase1
+ }
+ }
+ field {
+ name = voltage_at_phase_2
+ quantity = Voltage
+ info = 'Voltage at phase L2.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ measurement_type = Instantaneous
+ vif_range = Voltage
+ add_combinable = AtPhase2
+ }
+ }
+ field {
+ name = voltage_at_phase_3
+ quantity = Voltage
+ info = 'Voltage at phase L3.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ measurement_type = Instantaneous
+ vif_range = Voltage
+ add_combinable = AtPhase3
+ }
+ }
+ field {
+ name = device
+ quantity = PointInTime
+ info = 'Device date time.'
+ display_unit = datetime
+ match {
+ measurement_type = Instantaneous
+ vif_range = DateTime
+ }
+ }
+ field {
+ name = total_energy_consumption_tariff_1
+ quantity = Energy
+ info = 'The total energy consumption recorded by this meter on tariff 1.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyEnergyVIF
+ tariff_nr = 1
+ }
+ }
+ field {
+ name = total_energy_consumption_tariff_2
+ quantity = Energy
+ info = 'The total energy consumption recorded by this meter on tariff 2.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyEnergyVIF
+ tariff_nr = 2
+ }
+ }
+ field {
+ name = total_energy_consumption_tariff_3
+ quantity = Energy
+ info = 'The total energy consumption recorded by this meter on tariff 3.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyEnergyVIF
+ tariff_nr = 3
+ }
+ }
+ field {
+ name = total_energy_consumption_tariff_4
+ quantity = Energy
+ info = 'The total energy consumption recorded by this meter on tariff 4.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyEnergyVIF
+ tariff_nr = 4
+ }
+ }
+ field {
+ name = total_energy_production_tariff_1
+ quantity = Energy
+ info = 'The total energy production recorded by this meter on tariff 1.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyEnergyVIF
+ add_combinable = BackwardFlow
+ tariff_nr = 1
+ }
+ }
+ field {
+ name = total_energy_production_tariff_2
+ quantity = Energy
+ info = 'The total energy production recorded by this meter on tariff 2.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyEnergyVIF
+ add_combinable = BackwardFlow
+ tariff_nr = 2
+ }
+ }
+ field {
+ name = total_energy_production_tariff_3
+ quantity = Energy
+ info = 'The total energy production recorded by this meter on tariff 3.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyEnergyVIF
+ add_combinable = BackwardFlow
+ tariff_nr = 3
+ }
+ }
+ field {
+ name = total_energy_production_tariff_4
+ quantity = Energy
+ info = 'The total energy production recorded by this meter on tariff 4.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyEnergyVIF
+ add_combinable = BackwardFlow
+ tariff_nr = 4
+ }
+ }
+ field {
+ name = max_power_consumption
+ quantity = Power
+ info = 'The maximum demand indicator (maximum 15-min average power consumption recorded this month).'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ measurement_type = Maximum
+ vif_range = AnyPowerVIF
+ }
+ }
+ field {
+ name = absolute_max_power_consumption
+ quantity = Power
+ info = 'The maximum demand indicator for all times?'
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyPowerVIF
+ add_combinable = UpperLimit
+ add_combinable = AccumulationOfAbsoluteValue
+ }
+ }
+ field {
+ name = total_reactive_energy_inductive
+ quantity = Reactive_Energy
+ info = 'The total inductive reactive energy recorded by this meter.'
+ vif_scaling = None
+ dif_signedness = Signed
+ display_unit = kvarh
+ force_scale = 0.001
+ match {
+ difvifkey = 0EFB8273
+ }
+ }
+ field {
+ name = total_reactive_energy_capacitive
+ quantity = Reactive_Energy
+ info = 'The total capacitive reactive energy recorded by this meter.'
+ vif_scaling = None
+ dif_signedness = Signed
+ display_unit = kvarh
+ force_scale = 0.001
+ match {
+ difvifkey = 0EFB82F33C
+ }
+ }
+ field {
+ name = reactive_power_inductive
+ quantity = Reactive_Power
+ info = 'Current inductive reactive power.'
+ vif_scaling = None
+ dif_signedness = Signed
+ display_unit = kvar
+ force_scale = 0.001
+ match {
+ difvifkey = 0BFB14
+ }
+ }
+ field {
+ name = reactive_power_capacitive
+ quantity = Reactive_Power
+ info = 'Current capacitive reactive power.'
+ vif_scaling = None
+ dif_signedness = Signed
+ display_unit = kvar
+ force_scale = 0.001
+ match {
+ difvifkey = 0BFB943C
+ }
+ }
+ }
+ tests {
+ test {
+ args = 'MyElectricity1 amiplus 10101010 NOKEY'
+ telegram = 4E4401061010101002027A00004005_2F2F0E035040691500000B2B300300066D00790C7423400C78371204860BABC8FC100000000E833C8074000000000BAB3C0000000AFDC9FC0136022F2F2F2F2F
+ json = '{"_":"telegram","media":"electricity","driver":"amiplus","name":"MyElectricity1","id":"10101010","absolute_max_power_consumption_kw":0,"total_energy_consumption_kwh":15694.05,"current_power_consumption_kw":0.33,"total_energy_production_kwh":7.48,"current_power_production_kw":0,"voltage_at_phase_1_v":236,"device_datetime":"2019-03-20 12:57","timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'MyElectricity1;10101010;15694.05;0.33;7.48;0;236;null;null;null;null;null;null;null;null;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'MyElectricity2 amiplus 00254358 NOKEY'
+ comment = 'amiplus/apator electricity meter with three phase voltages'
+ telegram = 5E44B6105843250000027A2A005005_2F2F0C7835221400066D404708AC2A400E032022650900000E833C0000000000001B2B9647000B2B5510000BAB3C0000000AFDC9FC0135020AFDC9FC0245020AFDC9FC0339020BABC8FC100000002F2F
+ json = '{"_":"telegram","media":"electricity","driver":"amiplus","name":"MyElectricity2","id":"00254358","absolute_max_power_consumption_kw":0,"total_energy_consumption_kwh":9652.22,"current_power_consumption_kw":1.055,"total_energy_production_kwh":0,"current_power_production_kw":0,"voltage_at_phase_1_v":235,"voltage_at_phase_2_v":245,"voltage_at_phase_3_v":239,"max_power_consumption_kw":4.796,"device_datetime":"2021-10-12 08:07","timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'MyElectricity2;00254358;9652.22;1.055;0;0;235;245;239;null;null;null;null;null;null;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'MyElectricity3 amiplus 86064864 NOKEY'
+ comment = 'amiplus/apator electricity meter with three phase voltages and 2 tariffs.'
+ telegram = 804401066448068602027A000070052F2F_066D1E5C11DA21400C78644806868E10036110012500008E20038106531800008E10833C9949000000008E20833C8606000000001B2B5228020B2B3217000BAB3C0000000AFDC9FC0131020AFDC9FC0225020AFDC9FC0331020BABC8FC100000002F2F2F2F2F2F2F2F2F2F2F2F2FDE47
+ json = '{"_":"telegram","media":"electricity","driver":"amiplus","name":"MyElectricity3","id":"86064864","absolute_max_power_consumption_kw":0,"current_power_consumption_kw":1.732,"current_power_production_kw":0,"voltage_at_phase_1_v":231,"voltage_at_phase_2_v":225,"voltage_at_phase_3_v":231,"device_datetime":"2022-01-26 17:28","total_energy_consumption_tariff_1_kwh":25011.061,"total_energy_consumption_tariff_2_kwh":18530.681,"total_energy_production_tariff_1_kwh":4.999,"total_energy_production_tariff_2_kwh":0.686,"max_power_consumption_kw":22.852,"timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'MyElectricity3;86064864;null;1.732;null;0;231;225;231;25011.061;18530.681;null;4.999;0.686;null;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'MyElectricity4 amiplus 55090884 NOKEY'
+ comment = 'amiplus/apator electricity meter with single phase voltage - Otus 1'
+ telegram = 7E4401068408095501027A7C1070052F2F_066DDE5E150D39800C78840809550AFDC9FC0139028E30833C0000000000008E20833C0000000000008E10833C4301000000000BABC8FC100000008E10035336420200008E20030000000000008E30030000000000000B2B9502000BAB3C0000002F2F2F2F2F2F2F2F2F2F2F2F2F
+ json = '{"_":"telegram","media":"electricity","driver":"amiplus","name":"MyElectricity4","id":"55090884","absolute_max_power_consumption_kw":0,"current_power_consumption_kw":0.295,"current_power_production_kw":0,"total_energy_consumption_tariff_1_kwh":2423.653,"total_energy_consumption_tariff_2_kwh":0,"total_energy_consumption_tariff_3_kwh":0,"total_energy_production_tariff_1_kwh":0.143,"total_energy_production_tariff_2_kwh":0,"total_energy_production_tariff_3_kwh":0,"voltage_at_phase_1_v":239,"device_datetime":"2024-09-13 21:30","timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'MyElectricity4;55090884;null;0.295;null;0;239;null;null;2423.653;0;0;0.143;0;0;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'MyElectricity4 amiplus 00320787 NOKEY'
+ telegram = 3e44b6108707320001027a380030052f2f0C7830253390066D6872141239400E031891690000000E833C9265010000000B2B2602000BAB3C0000002F2F2F2F
+ json = '{"_":"telegram","current_power_consumption_kw": 0.226,"current_power_production_kw": 0,"device_datetime": "2024-09-18 20:50","id": "00320787","media": "electricity","driver": "amiplus","name": "MyElectricity4","timestamp": "1111-11-11T11:11:11Z","total_energy_consumption_kwh": 699.118,"total_energy_production_kwh": 16.592}'
+ fields = 'MyElectricity4;00320787;699.118;0.226;16.592;0;null;null;null;null;null;null;null;null;null;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'MyElectricity5 amiplus 56914504 NOKEY'
+ telegram = 9e4401060445915601027a3d0390052f2f066dc076091935800c78044591560e032088300000008e10032088300000008e20030000000000008e30030000000000008e8010030000000000000e833c2702000000008e10833c2702000000008e20833c0000000000008e30833c0000000000008e8010833c0000000000000afdc8fc0136240afdc8fc0262240afdc8fc0389222f2f2f2f2f2f2f2f2f2f2f2f
+ json = '{"_":"telegram","media":"electricity","driver":"amiplus","name":"MyElectricity5","id":"56914504","total_energy_consumption_tariff_4_kwh":0,"total_energy_production_tariff_4_kwh":0,"total_energy_consumption_kwh":308.82,"total_energy_consumption_tariff_1_kwh":308.82,"total_energy_consumption_tariff_2_kwh":0,"total_energy_consumption_tariff_3_kwh":0,"total_energy_production_kwh":0.227,"total_energy_production_tariff_1_kwh":0.227,"total_energy_production_tariff_2_kwh":0,"total_energy_production_tariff_3_kwh":0,"voltage_at_phase_1_v":243.6,"voltage_at_phase_2_v":246.2,"voltage_at_phase_3_v":228.9,"device_datetime":"2024-05-25 09:54","timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'MyElectricity5;56914504;308.82;null;0.227;null;243.6;246.2;228.9;308.82;0;0;0.227;0;0;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'MyElectricity6 amiplus 00086426 NOKEY'
+ telegram = 8F44B3382664080003027A090080052F2F_066D37090E2232050C78266408000AFDC9FC0142020AFDC9FC0240020AFDC9FC0338028E30833C0000000000008E20833C0000000000008E10833C0000000000000BABC8FC100000008E10030750030000008E20035379060000008E30030000000000000B2B1307000BAB3C0000002F2F2F2F2F2F2F2F2F2F2F2F2F2F2F2F
+ json = '{"_": "telegram","current_power_consumption_kw": 0.713,"current_power_production_kw": 0,"device_datetime": "2025-02-02 14:09","id": "00086426","absolute_max_power_consumption_kw": 0,"media": "electricity","driver": "amiplus","name": "MyElectricity6","timestamp": "1111-11-11T11:11:11Z","total_energy_consumption_tariff_1_kwh": 35.007,"total_energy_consumption_tariff_2_kwh": 67.953,"total_energy_consumption_tariff_3_kwh": 0,"total_energy_production_tariff_1_kwh": 0,"total_energy_production_tariff_2_kwh": 0,"total_energy_production_tariff_3_kwh": 0,"voltage_at_phase_1_v": 242,"voltage_at_phase_2_v": 240,"voltage_at_phase_3_v": 238}'
+ fields = 'MyElectricity6;00086426;null;0.713;null;0;242;240;238;35.007;67.953;0;0;0;0;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'MyElectricity7 amiplus 56859196 NOKEY'
+ comment = 'Apator Otus 3 reporting reactive energy and reactive power (registers under mfct specific FB VIFEs).'
+ telegram = CE4401069691855601027AC600C005_2F2F066D4073124234800C78969185560E032050260600008E10032050260600000E833C9299930700008E10833C9299930700000EFB82736461630100008E10FB82736461630100000EFB82F33C9248050500008E10FB82F33C9248050500000B2B6800000BAB3C0300000BFB142303000BFB943C1803000AFDC8FC0197220AFDC8FC0231230AFDC8FC035123146DADAA423414ED3C80AB42340BFDDAFC011400000BFDDAFC023401000BFDDAFC030302002F2F2F2F2F2F2F2F2F2F2F2F2F2F
+ json = '{"_":"telegram","media":"electricity","driver":"amiplus","name":"MyElectricity7","id":"56859196","current_power_consumption_kw":0.068,"current_power_production_kw":0.003,"reactive_power_capacitive_kvar":0.318,"reactive_power_inductive_kvar":0.323,"device_datetime":"2026-04-02 18:51","total_energy_consumption_kwh":6265.02,"total_energy_consumption_tariff_1_kwh":6265.02,"total_energy_production_kwh":7939.992,"total_energy_production_tariff_1_kwh":7939.992,"total_reactive_energy_capacitive_kvarh":5054.892,"total_reactive_energy_inductive_kvarh":1636.164,"voltage_at_phase_1_v":229.7,"voltage_at_phase_2_v":233.1,"voltage_at_phase_3_v":235.1,"timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'MyElectricity7;56859196;6265.02;0.068;7939.992;0.003;229.7;233.1;235.1;6265.02;null;null;7939.992;null;null;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'MyElectricity8 amiplus 30584551 fba95687613b73604c320a16aa8892ea'
+ comment = 'Apator Gama 350 (issue 2036) reporting reactive power. This meter does not send the reactive energy registers.'
+ telegram = be44ed145145583001027ae700b005_a3c60e5d52ab6b2c2cfdd8368f408830f50f6d698041d61a76fd4e01988dde5d601f0e425682c43566156c932fc9d5661d01832707d90236c6ea2fddd6579a1533fe1b637082c2458939e96cc2cbd46410e6880a7990f7219a4b2c546b21eb2cddfda202aaf448ba01ab92e07ac68dd339d860b483fecdbd25851d938c002a3fe654875a0e6d8fa6692de3a66afc2dc3cff669d6a581ae7903f299e037ea81a7ae842fd4fe4ba4c9f5f01b60277a6861
+ json = '{"_":"telegram","media":"electricity","driver":"amiplus","name":"MyElectricity8","id":"30584551","absolute_max_power_consumption_kw":70,"total_energy_consumption_tariff_4_kwh":0,"total_energy_production_tariff_4_kwh":0,"current_power_consumption_kw":0.445,"current_power_production_kw":0,"reactive_power_capacitive_kvar":0.434,"reactive_power_inductive_kvar":0,"device_datetime":"2026-07-28 00:40","total_energy_consumption_kwh":15948.218,"total_energy_consumption_tariff_1_kwh":15948.218,"total_energy_consumption_tariff_2_kwh":0,"total_energy_consumption_tariff_3_kwh":0,"total_energy_production_kwh":0.023,"total_energy_production_tariff_1_kwh":0.023,"total_energy_production_tariff_2_kwh":0,"total_energy_production_tariff_3_kwh":0,"voltage_at_phase_1_v":236,"voltage_at_phase_2_v":235,"voltage_at_phase_3_v":238,"timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'MyElectricity8;30584551;15948.218;0.445;0.023;0;236;235;238;15948.218;0;0;0.023;0;0;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'MyElectricity9 amiplus 12345678 NOKEY'
+ comment = 'Elgama Elektronika Gama 350 with TM-16 radio module reporting 4 tariffs for both consumption and production, reactive power and the power upper limit register.'
+ telegram = BE44ED147856341201027A2700B0052F2F_066D7432945737800C78785634120E038301210200008E10038301210200008E20030000000000008E30030000000000008E8010030000000000000E833C0000000000008E10833C0000000000008E20833C0000000000008E30833C0000000000008E8010833C0000000000000B2B8901000BAB3C0000000BFB143200000BFB943C4901000AFDC9FC0126020AFDC9FC0225020AFDC9FC0326020BABC8FC100000072F2F2F2F2F2F2F2F2F2F2F2F
+ json = '{"_":"telegram","media":"electricity","driver":"amiplus","name":"MyElectricity9","id":"12345678","current_power_consumption_kw":0.189,"current_power_production_kw":0,"device_datetime":"2026-07-23 20:50","absolute_max_power_consumption_kw":70,"reactive_power_capacitive_kvar":0.149,"reactive_power_inductive_kvar":0.032,"total_energy_consumption_kwh":2210.183,"total_energy_consumption_tariff_1_kwh":2210.183,"total_energy_consumption_tariff_2_kwh":0,"total_energy_consumption_tariff_3_kwh":0,"total_energy_consumption_tariff_4_kwh":0,"total_energy_production_kwh":0,"total_energy_production_tariff_1_kwh":0,"total_energy_production_tariff_2_kwh":0,"total_energy_production_tariff_3_kwh":0,"total_energy_production_tariff_4_kwh":0,"voltage_at_phase_1_v":226,"voltage_at_phase_2_v":225,"voltage_at_phase_3_v":226,"timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'MyElectricity9;12345678;2210.183;0.189;0;0;226;225;226;2210.183;0;0;0;0;0;1111-11-11 11:11.11'
+ }
+ }
+}
diff --git a/components/wmbus_common/drivers/src/apator08.xmq b/components/wmbus_common/drivers/src/apator08.xmq
new file mode 100644
index 00000000..a3354de0
--- /dev/null
+++ b/components/wmbus_common/drivers/src/apator08.xmq
@@ -0,0 +1,49 @@
+// Copyright (C) 2020-2026 Fredrik Öhrström (gpl-3.0-or-later)
+driver {
+ name = apator08
+ meter_type = WaterMeter
+ default_fields = name,id,total_m3,timestamp
+ manufacturer = Apator
+ model = 08
+ detect {
+ mvt = APT,03,03
+ mvt = APT,0F,0F
+ }
+ fields {
+ field {
+ name = total
+ quantity = Volume
+ force_scale = 1/3
+ match {
+ measurement_type = Instantaneous
+ vif_range = Volume
+ }
+ }
+ field {
+ name = mfct_specific_data
+ quantity = Text
+ match_entire_payload = true
+ ixml = "decode = total, byte*.
+ total = quad, @DV_0413.
+ -hex = ['A'-'F';'0'-'9'].
+ -byte = hex, hex.
+ -quad = byte, byte, byte, byte.
+ DV_0413>dvk = +'0413'."
+ }
+ }
+ tests {
+ test {
+ args = 'test_apator082 apator08 00149c06 NOKEY'
+ comment = 'The apator08 reports a nonstandard medium, thus unknown below.'
+ telegram = 1C441486069C14000F0FA042F214000040030000000005FF0472BF1400
+ json = '{"_":"telegram","media":"unknown","driver":"apator08","name":"test_apator082","id":"00149c06","total_m3":457.579333,"timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'test_apator082;00149c06;457.579333;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'Vatten apator08 004444dd NOKEY'
+ telegram = 73441486DD4444000303A0B9E527004C4034B31CED0106FF01D093270065F022009661230054D02300EC49240018B424005F012500936D2500FFD525000E3D26001EAC26000B2027000300000000371D0B2000000000000024000000000000280000000000002C0033150C010D2F000000000000
+ json = '{"_":"telegram","media":"gas","driver":"apator08","name":"Vatten","id":"004444dd","total_m3":871.571,"timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'Vatten;004444dd;871.571;1111-11-11 11:11.11'
+ }
+ }
+}
diff --git a/components/wmbus_common/drivers/src/apator162.xmq b/components/wmbus_common/drivers/src/apator162.xmq
new file mode 100644
index 00000000..1ceefd13
--- /dev/null
+++ b/components/wmbus_common/drivers/src/apator162.xmq
@@ -0,0 +1,225 @@
+// Copyright (C) 2026 Fredrik Öhrström (gpl-3.0-or-later)
+driver {
+ name = apator162
+ meter_type = WaterMeter
+ default_fields = name,id,total_m3,timestamp
+ manufacturer = Apator
+ model = 162
+ detect {
+ mvt = APA,05,06
+ mvt = APA,05,07
+ }
+ library {
+ use = total_m3
+ }
+ fields {
+ field {
+ name = mfct_specific_data
+ quantity = Text
+ attributes = HIDE
+ match_entire_payload = true
+ /* An IXML grammar for decoding apator telegrams.
+ This grammar handles the overall structure but so far
+ only extracts the total volume in liters.
+ Sometimes the apator payload leads with a nice 0x0f which
+ means mft specific data. Sometimes the apator payload (older meters?)
+ lead with 0x80 and then we have a good crash test for the difvif parser. :-)
+ (Hint it hasn't crashed so far.) */
+ ixml = "decode = -byte, status, field*, pad_FF?, checksum.
+ -hex = ['A'-'F';'0'-'9'].
+ -byte = hex, hex.
+ -word = byte, byte.
+ -triplet = byte, byte, byte.
+ -quad = byte, byte, byte, byte.
+
+ status = byte, byte, byte, byte, byte, byte, byte.
+
+ -field = date | faults | total | flow | detectors | voltage | energy | days | woot_44 |
+ woot_71 | woot_72 | woot_73 | woot_74 | woot_75 | woot_76 | woot_77 | woot_78 | woot_79 |
+ woot_7A | woot_7B | woot_7C | woot_8x | woot_8xx | woot_8A | woot_8B | woot_8C | woot_8E |
+ woot_A0 | woot_A1 | woot_A2 | woot_A3 | woot_A4 | woot_A5 | woot_A6 | woot_A9 | woot_AF |
+ woot_Ax | woot_B0 |
+ woot_B1 | woot_B2 | woot_B3 | woot_B4 | woot_B5 | woot_Bx | woot_Cx | woot_Dx | woot_F0.
+
+ date = -'00', quad.
+ faults = -'01', triplet.
+ total = -'10', quad, @DV_0413.
+ flow = -'11', word.
+ detectors = -'40', triplet, triplet.
+ voltage = -'41', word.
+ energy = -'42', quad.
+ days = -'43', word.
+ woot_44 = -'44', triplet.
+
+ woot_71 = -'71', byte, quad, quad.
+ woot_72 = -'72', byte, quad, quad, quad.
+ woot_73 = -'73', byte, quad, quad, quad, quad.
+ woot_74 = -'74', byte, quad, quad, quad, quad, quad.
+ woot_75 = -'75', byte, quad, quad, quad, quad, quad, quad.
+ woot_76 = -'76', byte, quad, quad, quad, quad, quad, quad, quad.
+ woot_77 = -'77', byte, quad, quad, quad, quad, quad, quad, quad, quad.
+ woot_78 = -'78', byte, quad, quad, quad, quad, quad, quad, quad, quad, quad.
+ woot_79 = -'79', byte, quad, quad, quad, quad, quad, quad, quad, quad, quad, quad.
+ woot_7A = -'7A', byte, quad, quad, quad, quad, quad, quad, quad, quad, quad, quad, quad.
+ woot_7B = -'7B', byte, quad, quad, quad, quad, quad, quad, quad, quad, quad, quad, quad, quad.
+ woot_7C = -'7C', byte, quad, quad, quad, quad, quad, quad, quad, quad, quad, quad, quad, quad, quad.
+
+ woot_8x = eights, quad, quad, word.
+ -eights = -'80' | -'81' | -'82' | -'83' | -'84' | -'86' | -'87'.
+
+ woot_8xx = eightsx, quad, quad, triplet.
+ -eightsx = -'85' | -'88' | -'8F'.
+
+ woot_8A = -'8A', quad, quad, byte.
+
+ woot_8B = -'8B', triplet, triplet.
+ woot_8C = -'8C', triplet, triplet.
+
+ woot_8E = -'8E', quad, triplet.
+
+ woot_Ax = (-'A7' | -'A8' | -'AA' | -'AB' | -'AC' | -'AD'), word.
+ woot_A0 = -'A0', quad.
+ woot_A1 = -'A1', quad.
+ woot_A2 = -'A2', byte.
+ woot_A3 = -'A3', triplet, triplet, byte.
+ woot_A4 = -'A4', quad.
+ woot_A5 = -'A5', byte.
+ woot_A6 = -'A6', triplet.
+ woot_A9 = -'A9', byte.
+ woot_AF = -'AF', byte.
+
+ woot_B0 = -'B0', quad, byte.
+ woot_B1 = -'B1', quad, quad.
+ woot_B2 = -'B2', quad, quad, quad, quad.
+ woot_B3 = -'B3', quad, quad.
+ woot_B4 = -'B4', word.
+ woot_B5 = -'B5', quad, quad, quad, quad.
+
+ woot_Bx = (-'B6' | -'B7' | -'B8' | -'B9' | -'BA' | -'BB' | -'BC' | -'BD' | -'BE' | -'BF'), triplet.
+ woot_Cx = (-'C0' | -'C1' | -'C2' | -'C3' | -'C4' | -'C5' | -'C6' | -'C7'), triplet.
+
+ woot_Dx = (-'D0' | -'D3'), triplet.
+
+ woot_F0 = -'F0', quad.
+
+ pad_FF = 'FF'+.
+ checksum = word, word?.
+
+ DV_0413>dvk = +'0413'."
+ }
+ }
+ tests {
+ test {
+ args = 'Wasser apator162 20202020 NOKEY'
+ telegram = 6E4401062020202005077A9A006085|2F2F0F0A734393CC0000435B0183001A54E06F630291342510|030F00007B013E0B00003E0B00003E0B00003E0B00003E0B00003E0B00003E0B0000650000003D0000003D0000003D00000000000000A0910CB003FFFFFFFFFFFFFFFFFFFFA62B
+ json = '{"_":"telegram","media":"water","driver":"apator162","name":"Wasser","id":"20202020","total_m3":3.843,"timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'Wasser;20202020;3.843;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'W2 apator162 02035240 NOKEY'
+ comment = 'Test telegram with no FF padding, last 2 bytes are checksum.'
+ telegram = 2E4401064052030205077A2D0020852F2F0F334DA39A00000071011ABF0D00039A0D00A01053850310CECC0D008943
+ json = '{"_": "telegram","id": "02035240","media": "water","driver": "apator162","name": "W2","timestamp": "1111-11-11T11:11:11Z","total_m3": 904.398}'
+ fields = 'W2;02035240;904.398;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'W3 apator162 02035240 NOKEY'
+ comment = 'Test telegram with no FF padding, last 2 bytes are checksum.'
+ telegram = 2E4401064052030205077A320020852F2F0F354DA39A00000071011ABF0D00039A0D00A01053850310CECC0D005E6D
+ json = '{"_": "telegram","id": "02035240","media": "water","driver": "apator162","name": "W3","timestamp": "1111-11-11T11:11:11Z","total_m3": 904.398}'
+ fields = 'W3;02035240;904.398;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'MyTapWatera apator162 21202020 NOKEY'
+ telegram = 4E4401062020202105077A130040852F2F0F6D4C389300020043840210351F040075012C0B040048D603003E630300CD2C03001EF402000ACE0200A098A39603FFFFFFFFFFFFFFFFFFFFFFFFFF1977
+ json = '{"_":"telegram","media":"water","driver":"apator162","name":"MyTapWatera","id":"21202020","total_m3":270.133,"timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'MyTapWatera;21202020;270.133;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'Wasser5 apator162 00533603 NOKEY'
+ comment = 'Real meter telegram with woot_7C monthly history field (regression test for github issue).'
+ telegram = 6E4401060336530005077A210060852F2F0F141F539A12000243EE0E84E3BDD9B6593A040500001082C800007C01B2C4000064C10000B3BD00000DB900000EB500009FB0000054AB0000EDA30000A39D0000BF980000E394000038900000488B0000A034CB8C04FFFFFFFFFFFFFA8A
+ json = '{"_":"telegram","media":"water","driver":"apator162","name":"Wasser5","id":"00533603","total_m3":51.33,"timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'Wasser5;00533603;51.33;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'MyTapWaterb apator162 22202020 NOKEY'
+ telegram = 4E4401062020202205077A4B004085|2F2F0FE566B99390000087C0B24B732679FF75350010|FCFB00004155594265086A0043B4017301DFF600006AE70000BFD5000051BC0000A0F56C2602FFFF1B1B
+ json = '{"_":"telegram","media":"water","driver":"apator162","name":"MyTapWaterb","id":"22202020","total_m3":64.508,"timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'MyTapWaterb;22202020;64.508;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'MyTapWaterc apator162 23202020 NOKEY'
+ telegram = 4E4401062020202305077A9D004085|2F2F0F81902C9300000010|B82F010041555942BD2882004319027301BC2601005C180100CB0A0100DFF60000A0F56C2602FFFFFFFFFFFFFFFFFFFFFFFFFF5B7C
+ json = '{"_":"telegram","media":"water","driver":"apator162","name":"MyTapWaterc","id":"23202020","total_m3":77.752,"timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'MyTapWaterc;23202020;77.752;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'MyTapWaterd apator162 24202020 NOKEY'
+ telegram = 4E4401062020202405077A6C0040852F2F|0F73B3E19410000084E15381E553810101000010|FA41010041555942BF4E8A00433B027301AD380100BC2601005C180100CB0A0100A0F56C2602FFFFD0D7
+ json = '{"_":"telegram","media":"water","driver":"apator162","name":"MyTapWaterd","id":"24202020","total_m3":82.426,"timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'MyTapWaterd;24202020;82.426;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'MyTapWatere apator162 25202020 NOKEY'
+ telegram = 4E4401062020202505077AEF0040852F2F|0F|071122|94|100200|43|6103|84|8B745953486C09100000|10|81920200|75|01F1800200E5640200534A02003431020080150200D9000200|A0|DC939703|FFFFA434
+ json = '{"_":"telegram","media":"water","driver":"apator162","name":"MyTapWatere","id":"25202020","total_m3":168.577,"timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'MyTapWatere;25202020;168.577;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'MyTapWatere apator162 26202020 NOKEY'
+ telegram = 6E4401062020202605077AAC0060852F2F|0F|0C4442|94|1A0000|43|B502|83|000A549B4159029C290F|10|AB440000|7B|012C440000BE3E00008838000072340000493000009B2C00001D2C0000822B00007428000010250000B7200000261C0000|A0|A4D9A103|FFFFFFFFFFFFFFFFFFFF7823
+ json = '{"_":"telegram","media":"water","driver":"apator162","name":"MyTapWatere","id":"26202020","total_m3":17.579,"timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'MyTapWatere;26202020;17.579;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'MyTapWatere apator162 26202020 NOKEY'
+ telegram = 6E4401062020202605077AAD0060852F2F|0F|0E4442|94|1A0000|43|B502|84|4265594C655901010000|10|AB440000|7B|012C440000BE3E00008838000072340000493000009B2C00001D2C0000822B00007428000010250000B7200000261C0000|A0|A4D9A103|FFFFFFFFFFFFFFFFFFFF6C1B
+ json = '{"_":"telegram","media":"water","driver":"apator162","name":"MyTapWatere","id":"26202020","total_m3":17.579,"timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'MyTapWatere;26202020;17.579;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'MyTapWatere apator162 26202020 NOKEY'
+ telegram = 6E4401062020202605077AAE0060852F2F|0F|0F4442|94|1A0000|43|B502|81|D87F57D87F5701010000|10|AB440000|7B|012C440000BE3E00008838000072340000493000009B2C00001D2C0000822B00007428000010250000B7200000261C0000|A0|A4D9A103|FFFFFFFFFFFFFFFFFFFF5F22
+ json = '{"_":"telegram","media":"water","driver":"apator162","name":"MyTapWatere","id":"26202020","total_m3":17.579,"timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'MyTapWatere;26202020;17.579;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'MyTapWaterf apator162 03410514 NOKEY'
+ telegram = 3E4401061405410305077A190030852F2F|0F|86B4B8|95|290200|40|C6C1|B4|F0F3F3|41|5559|42|FA701000|F0|01010000|10|BC780000|FFFFFFFFFFFFFFFFFFFFFF2483
+ json = '{"_":"telegram","media":"water","driver":"apator162","name":"MyTapWaterf","id":"03410514","total_m3":30.908,"timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'MyTapWaterf;03410514;30.908;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'MyTapWaterg apator162 27202020 NOKEY'
+ telegram = 6E4401062020202705077A3D0060852F2F|0F|151794|94|0A0200|43|0403|81|D87F57D87F5701010000|10|783E0000|7B01223C00009137000098320000392D000010290000F02600004C2400003422000004220000CB21000017200000C51C0000|A0|9AD9A103|FFFFFFFFFFFFFFFFFFFF367E
+ json = '{"_":"telegram","media":"water","driver":"apator162","name":"MyTapWaterg","id":"27202020","total_m3":15.992,"timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'MyTapWaterg;27202020;15.992;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'NewAndOld apator162 00148686 NOKEY'
+ comment = 'New apator162 telegram which can be decoded.'
+ telegram = 4E4401068686140005077A350040852F2F_0F005B599600000010AA55000041545A42850BD800437D037301C5500000564B00009E4600006A410000A01778EC03FFFFFFFFFFFFFFFFFFFFFFFFFFE393
+ json = '{"_":"telegram","media":"water","driver":"apator162","name":"NewAndOld","id":"00148686","total_m3":21.93,"timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'NewAndOld;00148686;21.93;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'NewAndOld apator162 00148686 NOKEY'
+ telegram = 4E4401068686140005077A350040852F2F_0F005B599600000010AA66000041545A42850BD800437D037301C5500000564B00009E4600006A410000A01778EC03FFFFFFFFFFFFFFFFFFFFFFFFFFE393
+ json = '{"_":"telegram","media":"water","driver":"apator162","name":"NewAndOld","id":"00148686","total_m3":26.282,"timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'NewAndOld;00148686;26.282;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'has80 apator162 04040404 NOKEY'
+ telegram = 704401060404040405077A0E0060852F2F_0F766DFB96010000430600808F67DB8F67DB01010000102F1F01007B01000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000A05F5C1804FFFFFFFFFFFFFFFFFFFF26BCD649
+ json = '{"_":"telegram","media":"water","driver":"apator162","name":"has80","id":"04040404","total_m3":73.519,"timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'has80;04040404;73.519;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'has79 apator162 04960212 NOKEY'
+ telegram = 5E4401061202960405077A790050852F2F0F78A599983B0200435000805771165771160103000010653A00007919321E0000620000006200000000000000000000000000000000000000000000000000000000000000A0422C6004FFFF2FBF
+ json = '{"_":"telegram","media":"water","driver":"apator162","name":"has79","id":"04960212","total_m3":14.949,"timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'has79;04960212;14.949;1111-11-11 11:11.11'
+ }
+ }
+}
diff --git a/components/wmbus_common/drivers/src/apator172.xmq b/components/wmbus_common/drivers/src/apator172.xmq
new file mode 100644
index 00000000..a0d0b573
--- /dev/null
+++ b/components/wmbus_common/drivers/src/apator172.xmq
@@ -0,0 +1,49 @@
+// Copyright (C) 2022-2026 Fredrik Öhrström (gpl-3.0-or-later)
+driver {
+ name = apator172
+ meter_type = WaterMeter
+ default_fields = name,id,total_m3,timestamp
+ // The meter sends 0x11 as media type, replace with water which makes more sense.
+ force_media_type = water
+ detect {
+ mvt = APT,04,11
+ }
+ fields {
+ field {
+ name = payload_decoder
+ quantity = Text
+ info = 'Decode 4-byte counter from mfct specific payload. The counter stores 1/3 of a m3.'
+ attributes = HIDE
+ match_entire_payload = true
+ ixml = "decode = total, byte*.
+ total = quad, @DV_0416.
+ -hex = ['A'-'F';'0'-'9'].
+ -byte = hex, hex.
+ -quad = byte, byte, byte, byte.
+ DV_0416>dvk = +'0416'."
+ }
+ field {
+ name = total
+ quantity = Volume
+ info = 'The total water consumption recorded by this meter.'
+ force_scale = 1/3
+ match {
+ difvifkey = 0416
+ }
+ }
+ }
+ tests {
+ test {
+ args = 'Vattur apator172 0014a807 NOKEY'
+ telegram = 1C44148607A814000411A0_1D5400000840030000000005FF05D83D0000
+ json = '{"_":"telegram","media":"water","driver":"apator172","name":"Vattur","id":"0014a807","total_m3":7177.666667,"timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'Vattur;0014a807;7177.666667;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'Vattur apator172 0014a807 NOKEY'
+ telegram = 1C44148607A814000411A0_215400000840030000000005FF05D83D0000
+ json = '{"_":"telegram","media":"water","driver":"apator172","name":"Vattur","id":"0014a807","total_m3":7179,"timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'Vattur;0014a807;7179;1111-11-11 11:11.11'
+ }
+ }
+}
diff --git a/components/wmbus_common/drivers/src/apatoreitn.xmq b/components/wmbus_common/drivers/src/apatoreitn.xmq
new file mode 100644
index 00000000..af314342
--- /dev/null
+++ b/components/wmbus_common/drivers/src/apatoreitn.xmq
@@ -0,0 +1,272 @@
+// Copyright (C) 2026 Fredrik Öhrström (gpl-3.0-or-later)
+driver {
+ name = apatoreitn
+ meter_type = HeatCostAllocationMeter
+ default_fields = name,id,current_hca,previous_hca,current_date,season_start_date,esb_date,temp_room_avg_c,temp_room_prev_avg_c,timestamp
+ manufacturer = Apator
+ detect {
+ mvt = APA,04,08
+ mvt = APT,04,08
+ }
+ fields {
+ field {
+ name = current
+ quantity = HCA
+ info = 'Energy consumption so far in this billing period.'
+ match {
+ difvifkey = 026E
+ }
+ }
+ field {
+ name = previous
+ quantity = HCA
+ info = 'Energy consumption in previous billing period.'
+ match {
+ difvifkey = 426E
+ }
+ }
+ field {
+ name = temp_room_avg_raw
+ quantity = Dimensionless
+ attributes = HIDE
+ vif_scaling = None
+ dif_signedness = Unsigned
+ match {
+ difvifkey = 02FB01
+ }
+ }
+ field {
+ name = temp_room_prev_avg_raw
+ quantity = Dimensionless
+ attributes = HIDE
+ vif_scaling = None
+ dif_signedness = Unsigned
+ match {
+ difvifkey = 02FB02
+ }
+ }
+ field {
+ name = current_date_raw
+ quantity = Dimensionless
+ attributes = HIDE
+ match {
+ difvifkey = 02FD3A
+ }
+ }
+ field {
+ name = season_start_date_lo
+ quantity = Dimensionless
+ attributes = HIDE
+ vif_scaling = None
+ dif_signedness = Unsigned
+ match {
+ difvifkey = 01FB00
+ }
+ }
+ field {
+ name = esb_date_raw
+ quantity = Dimensionless
+ attributes = HIDE
+ null_value = 0
+ match {
+ difvifkey = 8200FD3A
+ }
+ }
+ field {
+ name = season_start_date_raw
+ quantity = Dimensionless
+ attributes = HIDE
+ calculate = '(160counter * 256counter) + season_start_date_lo_counter'
+ }
+ field {
+ name = current_day
+ quantity = Dimensionless
+ attributes = HIDE
+ calculate = 'current_date_raw_counter % 32counter'
+ }
+ field {
+ name = current_month
+ quantity = Dimensionless
+ attributes = HIDE
+ calculate = '(current_date_raw_counter >> 5counter) % 16counter'
+ }
+ field {
+ name = current_year
+ quantity = Dimensionless
+ attributes = HIDE
+ calculate = '(current_date_raw_counter >> 9counter) % 32counter'
+ }
+ field {
+ name = season_start_day
+ quantity = Dimensionless
+ attributes = HIDE
+ calculate = 'season_start_date_raw_counter % 32counter'
+ }
+ field {
+ name = season_start_month
+ quantity = Dimensionless
+ attributes = HIDE
+ calculate = '(season_start_date_raw_counter >> 5counter) % 16counter'
+ }
+ field {
+ name = season_start_year
+ quantity = Dimensionless
+ attributes = HIDE
+ calculate = '(season_start_date_raw_counter >> 9counter) % 32counter'
+ }
+ field {
+ name = esb_day
+ quantity = Dimensionless
+ attributes = HIDE
+ calculate = 'esb_date_raw_counter % 32counter'
+ }
+ field {
+ name = esb_month
+ quantity = Dimensionless
+ attributes = HIDE
+ calculate = '(esb_date_raw_counter >> 5counter) % 16counter'
+ }
+ field {
+ name = esb_year
+ quantity = Dimensionless
+ attributes = HIDE
+ calculate = '(esb_date_raw_counter >> 9counter) % 32counter'
+ }
+ field {
+ name = temp_room_avg
+ quantity = Temperature
+ info = 'Average room temperature in current season.'
+ calculate = '((temp_room_avg_raw_counter >> 8counter) + ((temp_room_avg_raw_counter % 256counter) / 256counter)) * 1c'
+ }
+ field {
+ name = temp_room_prev_avg
+ quantity = Temperature
+ info = 'Average room temperature in previous season.'
+ calculate = '((temp_room_prev_avg_raw_counter >> 8counter) + ((temp_room_prev_avg_raw_counter % 256counter) / 256counter)) * 1c'
+ }
+ field {
+ name = current
+ quantity = PointInTime
+ display_unit = date
+ info = 'Current date, as reported by meter.'
+ calculate = "'2000-01-01 00:00:00' +
+ (((current_year_counter * 12counter) + current_month_counter - 1counter) * 1month) +
+ ((current_day_counter - 1counter) * 24h)"
+ }
+ field {
+ name = season_start
+ quantity = PointInTime
+ display_unit = date
+ info = 'Season start date.'
+ calculate = "'2000-01-01 00:00:00' +
+ (((season_start_year_counter * 12counter) + season_start_month_counter - 1counter) * 1month) +
+ ((season_start_day_counter - 1counter) * 24h)"
+ }
+ field {
+ name = esb
+ quantity = PointInTime
+ display_unit = date
+ null_string = ''
+ info = 'Electronic seal protection break date.'
+ calculate = "'2000-01-01 00:00:00' +
+ (((esb_year_counter * 12counter) + esb_month_counter - 1counter) * 1month) +
+ ((esb_day_counter - 1counter) * 24h)"
+ }
+ field {
+ name = status
+ quantity = Text
+ info = 'Seal integrity status: OK = intact, SEAL_BROKEN = ESB date is set.'
+ match {
+ difvifkey = 8200FD3A
+ }
+ lookup {
+ name = STATUS
+ map_type = BitToString
+ mask_bits = 0xffff
+ default_message = OK
+ map {
+ name = SEAL_BROKEN
+ value = 0xffff
+ test = Set
+ }
+ }
+ }
+ field {
+ name = mfct_specific_data
+ quantity = Text
+ attributes = HIDE
+ match_entire_payload = true
+ ixml = "decode = frame_direct | frame_direct_with_a0 | frame_b6.
+ frame_direct = SeasonStartDateLo,
+ pad,
+ PrevHCA,
+ ESBDate,
+ CurrHCA,
+ CurrentDate,
+ TempPrevAvg,
+ TempAvg.
+ frame_direct_with_a0 = -'A0', frame_direct.
+ frame_b6 = frame_b6_00 | frame_b6_01 | frame_b6_02 | frame_b6_03 |
+ frame_b6_04 | frame_b6_05 | frame_b6_06 | frame_b6_07 |
+ frame_b6_08 | frame_b6_09 | frame_b6_0A | frame_b6_0B |
+ frame_b6_0C | frame_b6_0D | frame_b6_0E | frame_b6_0F.
+ frame_b6_00 = -'00', -'A0', frame_direct.
+ frame_b6_01 = -'01', byte, -'A0', frame_direct.
+ frame_b6_02 = -'02', byte, byte, -'A0', frame_direct.
+ frame_b6_03 = -'03', byte, byte, byte, -'A0', frame_direct.
+ frame_b6_04 = -'04', byte, byte, byte, byte, -'A0', frame_direct.
+ frame_b6_05 = -'05', byte, byte, byte, byte, byte, -'A0', frame_direct.
+ frame_b6_06 = -'06', byte, byte, byte, byte, byte, byte, -'A0', frame_direct.
+ frame_b6_07 = -'07', byte, byte, byte, byte, byte, byte, byte, -'A0', frame_direct.
+ frame_b6_08 = -'08', byte, byte, byte, byte, byte, byte, byte, byte, -'A0', frame_direct.
+ frame_b6_09 = -'09', byte, byte, byte, byte, byte, byte, byte, byte, byte, -'A0', frame_direct.
+ frame_b6_0A = -'0A', byte, byte, byte, byte, byte, byte, byte, byte, byte, byte, -'A0', frame_direct.
+ frame_b6_0B = -'0B', byte, byte, byte, byte, byte, byte, byte, byte, byte, byte, byte, -'A0', frame_direct.
+ frame_b6_0C = -'0C', byte, byte, byte, byte, byte, byte, byte, byte, byte, byte, byte, byte, -'A0', frame_direct.
+ frame_b6_0D = -'0D', byte, byte, byte, byte, byte, byte, byte, byte, byte, byte, byte, byte, byte, -'A0', frame_direct.
+ frame_b6_0E = -'0E', byte, byte, byte, byte, byte, byte, byte, byte, byte, byte, byte, byte, byte, byte, -'A0', frame_direct.
+ frame_b6_0F = -'0F', byte, byte, byte, byte, byte, byte, byte, byte, byte, byte, byte, byte, byte, byte, byte, -'A0', frame_direct.
+
+ pad = byte, byte.
+ SeasonStartDateLo = byte, @DV_season_start_date_lo.
+ PrevHCA = word, @DV_previous_hca.
+ ESBDate = word, @DV_esb_date_raw.
+ CurrHCA = word, @DV_current_hca.
+ CurrentDate = word, @DV_current_date_raw.
+ TempPrevAvg = word, @DV_temp_room_prev_avg.
+ TempAvg = word, @DV_temp_room_avg.
+
+ -hex = ['A'-'F';'0'-'9'].
+ -byte = hex, hex.
+ -word = byte, byte.
+
+ DV_current_hca>dvk = +'026E'.
+ DV_previous_hca>dvk = +'426E'.
+ DV_current_date_raw>dvk = +'02FD3A'.
+ DV_season_start_date_lo>dvk = +'01FB00'.
+ DV_esb_date_raw>dvk = +'8200FD3A'.
+ DV_temp_room_avg>dvk = +'02FB01'.
+ DV_temp_room_prev_avg>dvk = +'02FB02'."
+ }
+ }
+ tests {
+ test {
+ args = 'HCA1 apatoreitn 37373731 NOKEY'
+ telegram = 19440186313737370408A0A1000059001C270100322DE413B415
+ json = '{"_":"telegram","media":"heat cost allocation","driver":"apatoreitn","name":"HCA1","id":"37373731","current_hca":1,"previous_hca":89,"current_date":"2022-09-18","season_start_date":"2016-05-01","esb_date":"2019-08-28","status":"SEAL_BROKEN","temp_room_avg_c":21.703125,"temp_room_prev_avg_c":19.890625,"timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'HCA1;37373731;1;89;2022-09-18;2016-05-01;2019-08-28;21.703125;19.890625;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'HCA2 apatoreitn 37373732 NOKEY'
+ telegram = 25441486323737370408B60AFFFFF5450186F41B9D58A0A100007809000000001F2D6416C819
+ json = '{"_":"telegram","media":"heat cost allocation","driver":"apatoreitn","name":"HCA2","id":"37373732","current_hca":0,"previous_hca":2424,"current_date":"2022-08-31","season_start_date":"2016-05-01","esb_date":null,"status":"OK","temp_room_avg_c":25.78125,"temp_room_prev_avg_c":22.390625,"timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'HCA2;37373732;0;2424;2022-08-31;2016-05-01;null;25.78125;22.390625;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'HCA3 apatoreitn 37373733 NOKEY'
+ telegram = 29441486333737370408B60EFFFFF1460186EC1B934EE91BA57BA0A1000059009C250100322DE413B415
+ json = '{"_":"telegram","media":"heat cost allocation","driver":"apatoreitn","name":"HCA3","id":"37373733","current_hca":1,"previous_hca":89,"current_date":"2022-09-18","season_start_date":"2016-05-01","esb_date":"2018-12-28","status":"SEAL_BROKEN","temp_room_avg_c":21.703125,"temp_room_prev_avg_c":19.890625,"timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'HCA3;37373733;1;89;2022-09-18;2016-05-01;2018-12-28;21.703125;19.890625;1111-11-11 11:11.11'
+ }
+ }
+}
diff --git a/components/wmbus_common/drivers/src/apatorna1.xmq b/components/wmbus_common/drivers/src/apatorna1.xmq
new file mode 100644
index 00000000..dc454adb
--- /dev/null
+++ b/components/wmbus_common/drivers/src/apatorna1.xmq
@@ -0,0 +1,485 @@
+// Copyright (C) 2026 Fredrik Öhrström (gpl-3.0-or-later)
+driver {
+ name = apatorna1
+ meter_type = WaterMeter
+ default_fields = name,id,total_m3,timestamp
+ manufacturer = Apator
+ detect {
+ mvt = APA,14,07
+ }
+ fields {
+ field {
+ name = packed_total_raw
+ quantity = Dimensionless
+ attributes = HIDE
+ vif_scaling = None
+ dif_signedness = Unsigned
+ match {
+ difvifkey = 04FB00
+ }
+ }
+ field {
+ name = meter_datetime_b0
+ quantity = Dimensionless
+ attributes = HIDE
+ vif_scaling = None
+ dif_signedness = Unsigned
+ match {
+ difvifkey = 01FB01
+ }
+ }
+ field {
+ name = meter_datetime_b1
+ quantity = Dimensionless
+ attributes = HIDE
+ vif_scaling = None
+ dif_signedness = Unsigned
+ match {
+ difvifkey = 01FB02
+ }
+ }
+ field {
+ name = meter_datetime_b2
+ quantity = Dimensionless
+ attributes = HIDE
+ vif_scaling = None
+ dif_signedness = Unsigned
+ match {
+ difvifkey = 01FB03
+ }
+ }
+ field {
+ name = meter_datetime_b3
+ quantity = Dimensionless
+ attributes = HIDE
+ vif_scaling = None
+ dif_signedness = Unsigned
+ match {
+ difvifkey = 01FB04
+ }
+ }
+ field {
+ name = historic_age_low_raw
+ quantity = Dimensionless
+ attributes = HIDE
+ vif_scaling = None
+ dif_signedness = Unsigned
+ match {
+ difvifkey = 01FB05
+ }
+ }
+ field {
+ name = historic_age_meta_raw
+ quantity = Dimensionless
+ attributes = HIDE
+ vif_scaling = None
+ dif_signedness = Unsigned
+ match {
+ difvifkey = 01FB06
+ }
+ }
+ field {
+ name = historic_delta_b0_raw
+ quantity = Dimensionless
+ attributes = HIDE
+ vif_scaling = None
+ dif_signedness = Unsigned
+ match {
+ difvifkey = 01FB07
+ }
+ }
+ field {
+ name = historic_delta_b1_raw
+ quantity = Dimensionless
+ attributes = HIDE
+ vif_scaling = None
+ dif_signedness = Unsigned
+ match {
+ difvifkey = 01FB08
+ }
+ }
+ field {
+ name = historic_delta_b2_raw
+ quantity = Dimensionless
+ attributes = HIDE
+ vif_scaling = None
+ dif_signedness = Unsigned
+ match {
+ difvifkey = 01FB09
+ }
+ }
+ field {
+ name = frame_status_raw
+ quantity = Dimensionless
+ attributes = HIDE
+ vif_scaling = None
+ dif_signedness = Unsigned
+ match {
+ difvifkey = 01FB10
+ }
+ }
+ field {
+ name = current_status_raw
+ quantity = Dimensionless
+ attributes = HIDE
+ vif_scaling = None
+ dif_signedness = Unsigned
+ match {
+ difvifkey = 01FB11
+ }
+ }
+ field {
+ name = historic_status_raw
+ quantity = Dimensionless
+ attributes = HIDE
+ vif_scaling = None
+ dif_signedness = Unsigned
+ match {
+ difvifkey = 01FB12
+ }
+ }
+ field {
+ name = packed_total_multiplier
+ quantity = Dimensionless
+ attributes = HIDE
+ calculate = '((((packed_total_raw_counter >> 4counter) % 4counter) == 0counter) * 1counter) +
+ ((((packed_total_raw_counter >> 4counter) % 4counter) == 1counter) * 10counter) +
+ ((((packed_total_raw_counter >> 4counter) % 4counter) == 2counter) * 100counter) +
+ ((((packed_total_raw_counter >> 4counter) % 4counter) == 3counter) * 1000counter)'
+ }
+ field {
+ name = total
+ quantity = Volume
+ info = 'The total water consumption recorded by this meter.'
+ change = Increasing
+ calculate = '((((packed_total_raw_counter >> 8counter) * 16counter) + (packed_total_raw_counter % 16counter)) * packed_total_multiplier_counter / 1000counter) * 1m3'
+ }
+ field {
+ name = meter
+ quantity = PointInTime
+ info = 'Date and time reported inside the encrypted manufacturer payload.'
+ display_unit = datetime
+ calculate = "'2000-01-01 00:00:00' +
+ (((meter_datetime_b3_counter << 24counter) +
+ (meter_datetime_b2_counter << 16counter) +
+ (meter_datetime_b1_counter << 8counter) +
+ meter_datetime_b0_counter) * 1s)"
+ }
+ field {
+ name = historic_age
+ quantity = Time
+ info = 'Age of the last historical reading carried in the telegram.'
+ display_unit = h
+ calculate = '((((historic_age_meta_raw_counter >> 2counter) * 256counter) + historic_age_low_raw_counter) * 1h)'
+ }
+ field {
+ name = historic
+ quantity = Volume
+ info = 'The last historical water consumption recorded by this meter.'
+ change = Increasing
+ calculate = 'total_m3 - (((((historic_delta_b2_raw_counter << 18counter) +
+ (historic_delta_b1_raw_counter << 10counter) +
+ (historic_delta_b0_raw_counter << 2counter) +
+ (historic_age_meta_raw_counter % 4counter)) * packed_total_multiplier_counter) / 1000counter) * 1m3)'
+ }
+ field {
+ name = historic
+ quantity = PointInTime
+ info = 'Date and time of the last historical reading carried in the telegram.'
+ display_unit = datetime
+ calculate = 'meter_datetime - historic_age_h'
+ }
+ field {
+ name = frame_status
+ quantity = Text
+ info = 'Status flags stored in the unencrypted manufacturer payload byte.'
+ match {
+ difvifkey = 01FB10
+ }
+ lookup {
+ name = FRAME_STATUS_FLAGS
+ map_type = BitToString
+ mask_bits = 0xff
+ default_message = OK
+ map {
+ name = SUMMER_TIME
+ value = 0x01
+ test = Set
+ }
+ map {
+ name = HISTORICAL_INDICATION
+ value = 0x02
+ test = Set
+ }
+ map {
+ name = MAX_SPEED_EXCEEDED
+ value = 0x04
+ test = Set
+ }
+ map {
+ name = DEVICE_ACCESS_ERROR
+ value = 0x08
+ test = Set
+ }
+ map {
+ name = TWO_WAY_COM_BLOCK
+ value = 0x10
+ test = Set
+ }
+ map {
+ name = INSTALL_FRAME
+ value = 0x20
+ test = Set
+ }
+ map {
+ name = WRONG_TEMPERATURE
+ value = 0x40
+ test = Set
+ }
+ map {
+ name = SERVICE_EVENT
+ value = 0x80
+ test = Set
+ }
+ }
+ }
+ field {
+ name = current_status
+ quantity = Text
+ info = 'Current alarm and tamper flags from the decrypted payload.'
+ match {
+ difvifkey = 01FB11
+ }
+ lookup {
+ name = CURRENT_STATUS_FLAGS
+ map_type = BitToString
+ mask_bits = 0xff
+ default_message = OK
+ map {
+ name = MINIMUM_FLOW
+ value = 0x80
+ test = Set
+ }
+ map {
+ name = MAXIMUM_FLOW
+ value = 0x40
+ test = Set
+ }
+ map {
+ name = REVERSE_FLOW
+ value = 0x20
+ test = Set
+ }
+ map {
+ name = MEASUREMENT_UNCHANGED
+ value = 0x10
+ test = Set
+ }
+ map {
+ name = LEAK
+ value = 0x08
+ test = Set
+ }
+ map {
+ name = DEVICE_DISCONNECTED
+ value = 0x04
+ test = Set
+ }
+ map {
+ name = MAGNETIC_FIELD
+ value = 0x02
+ test = Set
+ }
+ map {
+ name = BATTERY_LOW
+ value = 0x01
+ test = Set
+ }
+ }
+ }
+ field {
+ name = historic_status
+ quantity = Text
+ info = 'Historic alarm and tamper flags from the decrypted payload.'
+ match {
+ difvifkey = 01FB12
+ }
+ lookup {
+ name = HISTORIC_STATUS_FLAGS
+ map_type = BitToString
+ mask_bits = 0xff
+ default_message = OK
+ map {
+ name = MINIMUM_FLOW
+ value = 0x80
+ test = Set
+ }
+ map {
+ name = MAXIMUM_FLOW
+ value = 0x40
+ test = Set
+ }
+ map {
+ name = REVERSE_FLOW
+ value = 0x20
+ test = Set
+ }
+ map {
+ name = MEASUREMENT_UNCHANGED
+ value = 0x10
+ test = Set
+ }
+ map {
+ name = LEAK
+ value = 0x08
+ test = Set
+ }
+ map {
+ name = DEVICE_DISCONNECTED
+ value = 0x04
+ test = Set
+ }
+ map {
+ name = MAGNETIC_FIELD
+ value = 0x02
+ test = Set
+ }
+ map {
+ name = BATTERY_LIFETIME_THRESHOLD_EXCEEDED
+ value = 0x01
+ test = Set
+ }
+ }
+ }
+ field {
+ name = mfct_header_data
+ quantity = Text
+ attributes = HIDE
+ match_entire_payload = true
+ ixml = "decode = tpl_acc, frame_status, rest.
+ tpl_acc = -byte.
+ frame_status = byte, @DV_frame_status_raw.
+ rest = byte*.
+
+ -hex = ['A'-'F';'0'-'9'].
+ -byte = hex, hex.
+
+ DV_frame_status_raw>dvk = +'01FB10'."
+ }
+ field {
+ name = mfct_specific_data
+ quantity = Text
+ attributes = HIDE
+ match_entire_payload = true
+ transform_payload = tpl_aes_cbc_iv,2,16,0
+ ixml = "decode = -pad, packed_total, meter_datetime, historic_payload, current_status, historic_status.
+ packed_total = quad, @DV_packed_total_raw.
+ meter_datetime = dt_b0, dt_b1, dt_b2, dt_b3.
+ dt_b0 = byte, @DV_meter_datetime_b0.
+ dt_b1 = byte, @DV_meter_datetime_b1.
+ dt_b2 = byte, @DV_meter_datetime_b2.
+ dt_b3 = byte, @DV_meter_datetime_b3.
+ historic_payload = historic_age_low,
+ historic_age_meta,
+ historic_delta_b0,
+ historic_delta_b1,
+ historic_delta_b2.
+ historic_age_low = byte, @DV_historic_age_low_raw.
+ historic_age_meta = byte, @DV_historic_age_meta_raw.
+ historic_delta_b0 = byte, @DV_historic_delta_b0_raw.
+ historic_delta_b1 = byte, @DV_historic_delta_b1_raw.
+ historic_delta_b2 = byte, @DV_historic_delta_b2_raw.
+ current_status = byte, @DV_current_status_raw.
+ historic_status = byte, @DV_historic_status_raw.
+
+ -hex = ['A'-'F';'0'-'9'].
+ -byte = hex, hex.
+ -quad = byte, byte, byte, byte.
+ -pad = byte.
+
+ DV_packed_total_raw>dvk = +'04FB00'.
+ DV_meter_datetime_b0>dvk = +'01FB01'.
+ DV_meter_datetime_b1>dvk = +'01FB02'.
+ DV_meter_datetime_b2>dvk = +'01FB03'.
+ DV_meter_datetime_b3>dvk = +'01FB04'.
+ DV_historic_age_low_raw>dvk = +'01FB05'.
+ DV_historic_age_meta_raw>dvk = +'01FB06'.
+ DV_historic_delta_b0_raw>dvk = +'01FB07'.
+ DV_historic_delta_b1_raw>dvk = +'01FB08'.
+ DV_historic_delta_b2_raw>dvk = +'01FB09'.
+ DV_current_status_raw>dvk = +'01FB11'.
+ DV_historic_status_raw>dvk = +'01FB12'."
+ }
+ }
+ tests {
+ test {
+ args = 'ApNa1 apatorna1 04913581 00000000000000000000000000000000'
+ telegram = 1C440106813591041407A0B000266A705474DDB80D9A0EB9AE2EF29D96
+ json = '{"_":"telegram","media":"water","driver":"apatorna1","name":"ApNa1","id":"04913581","historic_m3":300.619,"historic_datetime":"2024-02-29 22:22","historic_age_h":545,"meter_datetime":"2024-03-23 15:22","total_m3":345.312,"current_status":"OK","frame_status":"OK","historic_status":"MINIMUM_FLOW","timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'ApNa1;04913581;345.312;1111-11-11 11:11.11'
+ }
+ test {
+ comment = 'Issue 1215 cluster 1 before dip'
+ args = 'ApNa1 apatorna1 04913581 00000000000000000000000000000000'
+ telegram = 1C440106813591041407A07F018D43B54E718E8718D5D2C8F939194847
+ json = '{"_":"telegram","media":"water","driver":"apatorna1","name":"ApNa1","id":"04913581","historic_m3":662.325,"historic_datetime":"2024-09-30 21:23","historic_age_h":351,"meter_datetime":"2024-10-15 12:23","total_m3":687.361,"current_status":"OK","frame_status":"SUMMER_TIME","historic_status":"OK","timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'ApNa1;04913581;687.361;1111-11-11 11:11.11'
+ }
+ test {
+ comment = 'Issue 1215 cluster 1 dip'
+ args = 'ApNa1 apatorna1 04913581 00000000000000000000000000000000'
+ telegram = 1C440106813591041407A17D01416562108AB98A0FE92FC06AA8122176
+ json = '{"_":"telegram","media":"water","driver":"apatorna1","name":"ApNa1","id":"04913581","historic_m3":662.325,"historic_datetime":"2024-09-30 21:22","historic_age_h":351,"meter_datetime":"2024-10-15 12:22","total_m3":687.104,"current_status":"OK","frame_status":"SUMMER_TIME","historic_status":"OK","timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'ApNa1;04913581;687.104;1111-11-11 11:11.11'
+ }
+ test {
+ comment = 'Issue 1215 cluster 1 recovery'
+ args = 'ApNa1 apatorna1 04913581 00000000000000000000000000000000'
+ telegram = 1C440106813591041407A174013655E9B2B8085E2F256908D7498C402C
+ json = '{"_":"telegram","media":"water","driver":"apatorna1","name":"ApNa1","id":"04913581","historic_m3":662.325,"historic_datetime":"2024-09-30 21:21","historic_age_h":351,"meter_datetime":"2024-10-15 12:21","total_m3":687.357,"current_status":"OK","frame_status":"SUMMER_TIME","historic_status":"OK","timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'ApNa1;04913581;687.357;1111-11-11 11:11.11'
+ }
+ test {
+ comment = 'Issue 1215 cluster 2 before dip'
+ args = 'ApNa1 apatorna1 04913581 00000000000000000000000000000000'
+ telegram = 1C440106813591041407A0FF019E3790584B0876226B94ADC34FFABDED
+ json = '{"_":"telegram","media":"water","driver":"apatorna1","name":"ApNa1","id":"04913581","historic_m3":662.325,"historic_datetime":"2024-09-30 21:04","historic_age_h":375,"meter_datetime":"2024-10-16 12:04","total_m3":689.403,"current_status":"OK","frame_status":"SUMMER_TIME","historic_status":"MINIMUM_FLOW","timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'ApNa1;04913581;689.403;1111-11-11 11:11.11'
+ }
+ test {
+ comment = 'Issue 1215 cluster 2 dip'
+ args = 'ApNa1 apatorna1 04913581 00000000000000000000000000000000'
+ telegram = 1C440106813591041407A0020134813C00486734067DB0111C30188137
+ json = '{"_":"telegram","media":"water","driver":"apatorna1","name":"ApNa1","id":"04913581","historic_m3":662.325,"historic_datetime":"2024-09-30 21:04","historic_age_h":375,"meter_datetime":"2024-10-16 12:04","total_m3":689.152,"current_status":"OK","frame_status":"SUMMER_TIME","historic_status":"MINIMUM_FLOW","timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'ApNa1;04913581;689.152;1111-11-11 11:11.11'
+ }
+ test {
+ comment = 'Issue 1215 cluster 2 recovery'
+ args = 'ApNa1 apatorna1 04913581 00000000000000000000000000000000'
+ telegram = 1C440106813591041407A00C019334727003B3DA268CF11117D85B6D71
+ json = '{"_":"telegram","media":"water","driver":"apatorna1","name":"ApNa1","id":"04913581","historic_m3":662.325,"historic_datetime":"2024-09-30 21:06","historic_age_h":375,"meter_datetime":"2024-10-16 12:06","total_m3":689.415,"current_status":"OK","frame_status":"SUMMER_TIME","historic_status":"MINIMUM_FLOW","timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'ApNa1;04913581;689.415;1111-11-11 11:11.11'
+ }
+ test {
+ comment = 'Issue 1215 cluster 3 before dip'
+ args = 'ApNa1 apatorna1 04913581 00000000000000000000000000000000'
+ telegram = 1C440106813591041407A0080174159B7E36D8E6421E3AED55C76BA54A
+ json = '{"_":"telegram","media":"water","driver":"apatorna1","name":"ApNa1","id":"04913581","historic_m3":662.325,"historic_datetime":"2024-09-30 21:18","historic_age_h":473,"meter_datetime":"2024-10-20 14:18","total_m3":696.575,"current_status":"OK","frame_status":"SUMMER_TIME","historic_status":"MINIMUM_FLOW","timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'ApNa1;04913581;696.575;1111-11-11 11:11.11'
+ }
+ test {
+ comment = 'Issue 1215 cluster 3 dip'
+ args = 'ApNa1 apatorna1 04913581 00000000000000000000000000000000'
+ telegram = 1C440106813591041407A10C010AEF3C1D41BE7F7A33B9EB469924397C
+ json = '{"_":"telegram","media":"water","driver":"apatorna1","name":"ApNa1","id":"04913581","historic_m3":662.325,"historic_datetime":"2024-09-30 21:18","historic_age_h":473,"meter_datetime":"2024-10-20 14:18","total_m3":696.32,"current_status":"OK","frame_status":"SUMMER_TIME","historic_status":"MINIMUM_FLOW","timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'ApNa1;04913581;696.32;1111-11-11 11:11.11'
+ }
+ test {
+ comment = 'Issue 1215 cluster 3 recovery'
+ args = 'ApNa1 apatorna1 04913581 00000000000000000000000000000000'
+ telegram = 1C440106813591041407A0130197DAF2A1C02F9B77B14ACD51CB20C41E
+ json = '{"_":"telegram","media":"water","driver":"apatorna1","name":"ApNa1","id":"04913581","historic_m3":662.325,"historic_datetime":"2024-09-30 21:20","historic_age_h":473,"meter_datetime":"2024-10-20 14:20","total_m3":696.576,"current_status":"OK","frame_status":"SUMMER_TIME","historic_status":"MINIMUM_FLOW","timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'ApNa1;04913581;696.576;1111-11-11 11:11.11'
+ }
+ }
+}
diff --git a/components/wmbus_common/drivers/src/aptmbusna.xmq b/components/wmbus_common/drivers/src/aptmbusna.xmq
new file mode 100644
index 00000000..50fe7903
--- /dev/null
+++ b/components/wmbus_common/drivers/src/aptmbusna.xmq
@@ -0,0 +1,177 @@
+// Copyright (C) 2026 Fredrik Öhrström (gpl-3.0-or-later)
+driver {
+ name = aptmbusna
+ meter_type = WaterMeter
+ default_fields = name,id,status,total_m3,target_m3,timestamp
+ detect {
+ mvt = APA,15,07
+ // APT-MBUS-NA-1 (-C)
+ }
+ library {
+ use = meter_datetime
+ use = fabrication_no
+ use = total_m3
+ use = target_m3
+ use = target_date
+ use = volume_flow_m3h
+ use = operating_time_h
+ }
+ fields {
+ // difvifkey = 04FF0A = optical_diagnostic, bits unknown.
+ // difvifkey = 02FF0B = oscillator_diagnostic, bits unknown.
+ // difvifkey = 03FF0C = power_supply_diagnostic, bits unknown.
+ field {
+ name = status
+ quantity = Text
+ info = 'Status and error flags.'
+ attributes = STATUS,INCLUDE_TPL_STATUS
+ match {
+ measurement_type = Instantaneous
+ vif_range = ErrorFlags
+ }
+ lookup {
+ name = ERROR_FLAGS
+ map_type = BitToString
+ mask_bits = 0xffffff
+ default_message = OK
+ // First byte
+ map {
+ name = P_MAGNETIC_FIELD
+ info = 'Magnetic field has been detected.'
+ value = 0x000001
+ test = Set
+ }
+ map {
+ name = P_STRONG_LIGHT
+ info = 'Strong light has been detected.'
+ value = 0x000002
+ test = Set
+ }
+ map {
+ name = P_MBUS_DISCONNECTED
+ info = 'The mbus has been disconnected.'
+ value = 0x000004
+ test = Set
+ }
+ map {
+ name = P_DETECTION_WITHOUT_CHANGES
+ info = Woot?
+ value = 0x000008
+ test = Set
+ }
+ map {
+ name = P_MINIMUM_FLOW
+ info = 'Minimum flow has been detected.'
+ value = 0x000010
+ test = Set
+ }
+ map {
+ name = P_MAXIMUM_FLOW
+ info = 'Maximum flow has been detected.'
+ value = 0x000020
+ test = Set
+ }
+ map {
+ name = P_REVERSE_FLOW
+ info = 'Reverse flow has been detected.'
+ value = 0x000040
+ test = Set
+ }
+ map {
+ name = P_LEAK
+ info = 'Leak has been detected.'
+ value = 0x0000800
+ test = Set
+ }
+ // 2nd byte
+ map {
+ name = P_PROCESSOR_RESET
+ info = 'Critical internal error has triggered a processor reset.'
+ value = 0x000100
+ test = Set
+ }
+ map {
+ name = P_CLOCK_HAND_ERROR
+ info = Woot?
+ value = 0x000200
+ test = Set
+ }
+ map {
+ name = BATTERY_LOW
+ info = 'Low battery warning.'
+ value = 0x000400
+ test = Set
+ }
+ map {
+ name = P_DETECTOR_ERROR
+ info = Woot?
+ value = 0x000800
+ test = Set
+ }
+ // 3rd byte
+ map {
+ name = MAGNETIC_FIELD
+ info = 'Magnetic field is being detected.'
+ value = 0x010000
+ test = Set
+ }
+ map {
+ name = STRONG_LIGHT
+ info = 'Strong light is being detected.'
+ value = 0x020000
+ test = Set
+ }
+ map {
+ name = MBUS_DISCONNECTED
+ info = 'The mbus is disconnected.'
+ value = 0x040000
+ test = Set
+ }
+ map {
+ name = DETECTION_WITHOUT_CHANGES
+ info = Woot?
+ value = 0x080000
+ test = Set
+ }
+ map {
+ name = MINIMUM_FLOW
+ info = 'Minimum flow is being detected.'
+ value = 0x100000
+ test = Set
+ }
+ map {
+ name = MAXIMUM_FLOW
+ info = 'Maximum flow is being detected.'
+ value = 0x200000
+ test = Set
+ }
+ map {
+ name = REVERSE_FLOW
+ info = 'Reverse flow is being detected.'
+ value = 0x400000
+ test = Set
+ }
+ map {
+ name = PROCESSOR_RESET
+ info = 'Critical internal error has triggered a processor reset.'
+ value = 0x800000
+ test = Set
+ }
+ }
+ }
+ }
+ tests {
+ test {
+ args = 'W aptmbusna 00683775 NOKEY'
+ telegram = 685656680800727537680001061507CCE800000C7875376800046D1DB458340413F73E0000023B0D00441385010000426C41340227090203FD171C030004FF0A0404000002FF0B000003FF0C1300B20F00042C1B1B00010200003716
+ json = '{"_": "telegram","fabrication_no": "00683775","id": "00683775","media": "water","driver": "aptmbusna","meter_datetime": "2026-04-24 20:29","name": "W","operating_time_h": 12504,"status": "PERMANENT_ERROR P_CLOCK_HAND_ERROR P_DETECTION_WITHOUT_CHANGES P_MBUS_DISCONNECTED P_MINIMUM_FLOW P_PROCESSOR_RESET UNKNOWN_E0","target_date": "2026-04-01","target_m3": 0.389,"timestamp": "1111-11-11T11:11:11Z","total_m3": 16.119,"volume_flow_m3h": 0.013}'
+ fields = 'W;00683775;PERMANENT_ERROR P_CLOCK_HAND_ERROR P_DETECTION_WITHOUT_CHANGES P_MBUS_DISCONNECTED P_MINIMUM_FLOW P_PROCESSOR_RESET UNKNOWN_E0;16.119;0.389;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'W aptmbusna 00345157 NOKEY'
+ telegram = 68565668086C7257513400010615072EF80000_0C7857513400046D0BAB5934041307590000023B00004413A2560000426C41340227CB0803FD175E0B0004FF0A0404010002FF0B000003FF0C9700B90F0004351BFF0001020100_A716
+ json = '{"_": "telegram","fabrication_no": "00345157","id": "00345157","media": "water","driver": "aptmbusna","meter_datetime": "2026-04-25 11:11","name": "W","operating_time_h": 54024,"status": "PERMANENT_ERROR P_CLOCK_HAND_ERROR P_DETECTION_WITHOUT_CHANGES P_LEAK P_MBUS_DISCONNECTED P_MINIMUM_FLOW P_PROCESSOR_RESET P_REVERSE_FLOW P_STRONG_LIGHT TEMPORARY_ERROR UNKNOWN_E0","target_date": "2026-04-01","target_m3": 22.178,"timestamp": "1111-11-11T11:11:11Z","total_m3": 22.791,"volume_flow_m3h": 0}'
+ fields = 'W;00345157;PERMANENT_ERROR P_CLOCK_HAND_ERROR P_DETECTION_WITHOUT_CHANGES P_LEAK P_MBUS_DISCONNECTED P_MINIMUM_FLOW P_PROCESSOR_RESET P_REVERSE_FLOW P_STRONG_LIGHT TEMPORARY_ERROR UNKNOWN_E0;22.791;22.178;1111-11-11 11:11.11'
+ }
+ }
+}
diff --git a/components/wmbus_common/drivers/src/aquastream.xmq b/components/wmbus_common/drivers/src/aquastream.xmq
index 78d159af..f4ad2596 100644
--- a/components/wmbus_common/drivers/src/aquastream.xmq
+++ b/components/wmbus_common/drivers/src/aquastream.xmq
@@ -25,33 +25,51 @@ driver {
mask_bits = 0xffff
default_message = OK
map {
- name = LOW_BATTERY
- info = 'Low Battery – 18 months before end of life'
- value = 0x04
+ name = TAMPER
+ info = 'Tampering alarm - No PMK detection (cannot read wheel position).'
+ value = 0x1
test = Set
}
map {
- name = TEMPORARY_ALARM
- info = 'Temporary alarm - Manipulation/module removed'
- value = 0x10
+ name = BURST
+ info = 'Flow rate higher than the set burst limit.'
+ value = 0x4
test = Set
}
map {
- name = LEAKAGE
- info = Leakage
- value = 0x20
+ name = LEAK
+ info = 'Detection of a leak.'
+ value = 0x8
test = Set
}
map {
- name = BURST
- info = Burst
- value = 0x30
+ name = NO_FLOW
+ info = 'No flow during the set time.'
+ value = 0x80
+ test = Set
+ }
+ map {
+ name = LOW_BATTERY
+ info = 'Low Battery – 18 months before end of life.'
+ value = 0x100
+ test = Set
+ }
+ map {
+ name = REVERSE
+ info = 'Negative flow rate beyond detection threshold (flow rate/time).'
+ value = 0x200
+ test = Set
+ }
+ map {
+ name = OVERFLOW
+ info = 'Flow rate higher than the set overflow limit.'
+ value = 0x400
test = Set
}
}
}
field {
- name = total_tariff1
+ name = total_backwards
quantity = Volume
match {
measurement_type = Instantaneous
@@ -75,7 +93,7 @@ driver {
test {
args = 'AqWater aquastream 05933237 NOKEY'
telegram = 4644B42557920410050E7237329305B42501075B0030252F2F_04130342000084101300000000046D0D30F62B441349180000426CFF2A02FD17000002FD74DB152F2F2F2F2F2F2F
- json = '{"_":"telegram","id": "05933237","media": "water","meter": "aquastream","meter_datetime": "2023-11-22 16:13","name": "AqWater","status": "OK","target_date": "2023-10-31","target_m3": 6.217,"timestamp": "1111-11-11T11:11:11Z","total_m3": 16.899,"total_tariff1_m3": 0,"battery_y": 15.328767}'
+ json = '{"_":"telegram","id": "05933237","media": "water","driver": "aquastream","meter_datetime": "2023-11-22 16:13","name": "AqWater","status": "OK","target_date": "2023-10-31","target_m3": 6.217,"timestamp": "1111-11-11T11:11:11Z","total_m3": 16.899,"total_backwards_m3": 0,"battery_y": 15.328767}'
fields = 'AqWater;05933237;16.899;1111-11-11 11:11.11'
}
}
diff --git a/components/wmbus_common/drivers/src/aventieshca.xmq b/components/wmbus_common/drivers/src/aventieshca.xmq
new file mode 100644
index 00000000..f329e143
--- /dev/null
+++ b/components/wmbus_common/drivers/src/aventieshca.xmq
@@ -0,0 +1,146 @@
+// Copyright (C) 2019-2022 Fredrik Öhrström (gpl-3.0-or-later)
+driver {
+ name = aventieshca
+ meter_type = HeatCostAllocationMeter
+ default_fields = name,id,current_consumption_hca,error_flags,timestamp
+ detect {
+ mvt = AAA,55,08
+ }
+ fields {
+ field {
+ name = status
+ quantity = Text
+ info = 'Meter status from error flags and tpl status field.'
+ attributes = STATUS,INCLUDE_TPL_STATUS
+ match {
+ measurement_type = Instantaneous
+ vif_range = ErrorFlags
+ }
+ lookup {
+ name = ERROR_FLAGS
+ map_type = BitToString
+ mask_bits = 0xffff
+ default_message = OK
+ map {
+ name = MEASUREMENT
+ value = 0x0001
+ test = Set
+ }
+ map {
+ name = SABOTAGE
+ value = 0x0002
+ test = Set
+ }
+ map {
+ name = BATTERY
+ value = 0x0004
+ test = Set
+ }
+ map {
+ name = CS
+ value = 0x0008
+ test = Set
+ }
+ map {
+ name = HF
+ value = 0x0010
+ test = Set
+ }
+ map {
+ name = RESET
+ value = 0x0020
+ test = Set
+ }
+ }
+ }
+ field {
+ name = error_flags
+ quantity = Text
+ info = Deprecated.
+ deprecated_by = 'status, update before 2026-12-01'
+ match {
+ measurement_type = Instantaneous
+ vif_range = ErrorFlags
+ }
+ lookup {
+ name = ERROR_FLAGS
+ map_type = BitToString
+ mask_bits = 0xffff
+ default_message = ''
+ map {
+ name = MEASUREMENT
+ value = 0x0001
+ test = Set
+ }
+ map {
+ name = SABOTAGE
+ value = 0x0002
+ test = Set
+ }
+ map {
+ name = BATTERY
+ value = 0x0004
+ test = Set
+ }
+ map {
+ name = CS
+ value = 0x0008
+ test = Set
+ }
+ map {
+ name = HF
+ value = 0x0010
+ test = Set
+ }
+ map {
+ name = RESET
+ value = 0x0020
+ test = Set
+ }
+ }
+ }
+ field {
+ name = current_consumption
+ quantity = HCA
+ info = 'The current heat cost allocation.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ measurement_type = Instantaneous
+ vif_range = HeatCostAllocation
+ }
+ }
+ field {
+ name = consumption_at_set_date
+ quantity = HCA
+ info = 'Heat cost allocation at the most recent billing period date.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ measurement_type = Instantaneous
+ vif_range = HeatCostAllocation
+ storage_nr = 1
+ }
+ }
+ field {
+ name = 'consumption_at_set_date_{storage_counter}'
+ quantity = HCA
+ info = 'The heat cost allocation at set date #.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ measurement_type = Instantaneous
+ vif_range = HeatCostAllocation
+ storage_nr = 2,17
+ }
+ }
+ }
+ tests {
+ test {
+ args = 'HCA aventieshca 60900126 NOKEY'
+ telegram = 76442104260190605508722601906021045508060060052F2F_0B6E660100426EA60082016EA600C2016E9E0082026E7E00C2026E5B0082036E4200C2036E770182046E5B01C2046E4C0182056E4701C2056E3E0182066E3B01C2066E3B0182076E3B01C2076E3B0182086E1301C2086E9C0002FD170000
+ json = '{"_":"telegram","media":"heat cost allocation","driver":"aventieshca","name":"HCA","id":"60900126","status":"OK","current_consumption_hca":166,"consumption_at_set_date_hca":166,"consumption_at_set_date_2_hca":166,"consumption_at_set_date_3_hca":158,"consumption_at_set_date_4_hca":126,"consumption_at_set_date_5_hca":91,"consumption_at_set_date_6_hca":66,"consumption_at_set_date_7_hca":375,"consumption_at_set_date_8_hca":347,"consumption_at_set_date_9_hca":332,"consumption_at_set_date_10_hca":327,"consumption_at_set_date_11_hca":318,"consumption_at_set_date_12_hca":315,"consumption_at_set_date_13_hca":315,"consumption_at_set_date_14_hca":315,"consumption_at_set_date_15_hca":315,"consumption_at_set_date_16_hca":275,"consumption_at_set_date_17_hca":156,"error_flags":"","error_flags_deprecated_by": "status, update before 2026-12-01","timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'HCA;60900126;166;;1111-11-11 11:11.11'
+ }
+ }
+}
diff --git a/components/wmbus_common/drivers/src/aventieswm.xmq b/components/wmbus_common/drivers/src/aventieswm.xmq
new file mode 100644
index 00000000..4ee7ec6f
--- /dev/null
+++ b/components/wmbus_common/drivers/src/aventieswm.xmq
@@ -0,0 +1,141 @@
+// Copyright (C) 2020-2022 Fredrik Öhrström (gpl-3.0-or-later)
+driver {
+ name = aventieswm
+ meter_type = WaterMeter
+ default_fields = name,id,total_m3,error_flags,timestamp
+ detect {
+ mvt = AAA,25,07
+ }
+ fields {
+ field {
+ name = status
+ quantity = Text
+ info = 'Meter status from error flags and tpl status field.'
+ attributes = STATUS,INCLUDE_TPL_STATUS
+ match {
+ measurement_type = Instantaneous
+ vif_range = ErrorFlags
+ }
+ lookup {
+ name = ERROR_FLAGS
+ map_type = BitToString
+ mask_bits = 0xffff
+ default_message = OK
+ map {
+ name = MEASUREMENT
+ value = 0x0001
+ test = Set
+ }
+ map {
+ name = SABOTAGE
+ value = 0x0002
+ test = Set
+ }
+ map {
+ name = BATTERY
+ value = 0x0004
+ test = Set
+ }
+ map {
+ name = CS
+ value = 0x0008
+ test = Set
+ }
+ map {
+ name = HF
+ value = 0x0010
+ test = Set
+ }
+ map {
+ name = RESET
+ value = 0x0020
+ test = Set
+ }
+ }
+ }
+ field {
+ name = total
+ quantity = Volume
+ change = Increasing
+ info = 'The total water consumption recorded by this meter.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ measurement_type = Instantaneous
+ vif_range = Volume
+ }
+ }
+ field {
+ name = 'consumption_at_set_date_{storage_counter}'
+ quantity = Volume
+ info = 'Water consumption at the # billing period date.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ measurement_type = Instantaneous
+ vif_range = Volume
+ storage_nr = 1,14
+ }
+ }
+ field {
+ name = error_flags
+ quantity = Text
+ info = Deprecated.
+ deprecated_by = 'status, update before 2026-12-01'
+ match {
+ measurement_type = Instantaneous
+ vif_range = ErrorFlags
+ }
+ lookup {
+ name = ERROR_FLAGS
+ map_type = BitToString
+ mask_bits = 0xffff
+ default_message = ''
+ map {
+ name = MEASUREMENT
+ value = 0x0001
+ test = Set
+ }
+ map {
+ name = SABOTAGE
+ value = 0x0002
+ test = Set
+ }
+ map {
+ name = BATTERY
+ value = 0x0004
+ test = Set
+ }
+ map {
+ name = CS
+ value = 0x0008
+ test = Set
+ }
+ map {
+ name = HF
+ value = 0x0010
+ test = Set
+ }
+ map {
+ name = RESET
+ value = 0x0020
+ test = Set
+ }
+ }
+ }
+ }
+ tests {
+ test {
+ args = 'Votten aventieswm 61070071 A004EB23329A477F1DD2D7820B56EB3D'
+ telegram = 76442104710007612507727100076121042507B5006005E2E95A3C2A1279A5415E6732679B43369FD5FDDDD783EEEBB48236D34E7C94AF0A18A5FDA5F7D64111EB42D4D891622139F2952F9D12A20088DFA4CF8123871123EE1F6C1DCEA414879DDB4E05E508F1826D7EFBA6964DF804C9261EA23BBF03
+ json = '{"_":"telegram","media":"water","driver":"aventieswm","name":"Votten","id":"61070071","total_m3":466.472,"consumption_at_set_date_1_m3":465.96,"consumption_at_set_date_2_m3":458.88,"consumption_at_set_date_3_m3":449.65,"consumption_at_set_date_4_m3":442.35,"consumption_at_set_date_5_m3":431.07,"consumption_at_set_date_6_m3":423.98,"consumption_at_set_date_7_m3":415.23,"consumption_at_set_date_8_m3":409.03,"consumption_at_set_date_9_m3":400.79,"consumption_at_set_date_10_m3":393.2,"consumption_at_set_date_11_m3":388.63,"consumption_at_set_date_12_m3":379.26,"consumption_at_set_date_13_m3":371.26,"consumption_at_set_date_14_m3":357.84,"status":"OK","error_flags":"","error_flags":"","error_flags_deprecated_by": "status, update before 2026-12-01","timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'Votten;61070071;466.472;;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'Vatten aventieswm 61070072 NOKEY'
+ telegram = 76442104720007612507727200076121042507B50060052F2F0413281E0700431404B60083011440B300C30114A5AF00830214CBAC00C3021463A8008303149EA500C3031433A200830414C79F00C304148F9C00830514989900C30514CF9700830614269400C30614069100830714C88B0002FD171111
+ json = '{"_":"telegram","media":"water","driver":"aventieswm","name":"Vatten","id":"61070072","total_m3":466.472,"consumption_at_set_date_1_m3":465.96,"consumption_at_set_date_2_m3":458.88,"consumption_at_set_date_3_m3":449.65,"consumption_at_set_date_4_m3":442.35,"consumption_at_set_date_5_m3":431.07,"consumption_at_set_date_6_m3":423.98,"consumption_at_set_date_7_m3":415.23,"consumption_at_set_date_8_m3":409.03,"consumption_at_set_date_9_m3":400.79,"consumption_at_set_date_10_m3":393.2,"consumption_at_set_date_11_m3":388.63,"consumption_at_set_date_12_m3":379.26,"consumption_at_set_date_13_m3":371.26,"consumption_at_set_date_14_m3":357.84,"status":"ERROR_FLAGS_1100 HF MEASUREMENT","error_flags":"ERROR_FLAGS_1100 HF MEASUREMENT","error_flags_deprecated_by": "status, update before 2026-12-01","timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'Vatten;61070072;466.472;ERROR_FLAGS_1100 HF MEASUREMENT;1111-11-11 11:11.11'
+ }
+ }
+}
diff --git a/components/wmbus_common/drivers/src/bfw240radio.xmq b/components/wmbus_common/drivers/src/bfw240radio.xmq
new file mode 100644
index 00000000..0c130140
--- /dev/null
+++ b/components/wmbus_common/drivers/src/bfw240radio.xmq
@@ -0,0 +1,383 @@
+/* Copyright (C) 2021-2026 Fredrik Öhrström (gpl-3.0-or-later)
+
+ date of telegram reception--------------------------------------------------------------------------------|
+ |
+ 18 historic monthly values (newest to the right, byte-reordering for 2.,4.,6.,...-oldest month)----| |
+ | |
+ ???------------------------| | |
+ | | |
+ current consumption---| | | |
+ | | | |
+ prev. cons.---vvvv vvvv vvvv vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv vvvvvv
+ feb: 2F2F6F1F 0144 0100 1470 000 000 000 000 000 000 000 000 000 000 000 000 000 000 370 09B 441 0AC 260221
+ mar: 2F2F481F 0144 0100 1470 000 000 000 000 000 000 000 000 000 000 000 000 000 037 9B0 144 AC0 100 040321
+ apr: 2F2F871F 0144 013C 1470 000 000 000 000 000 000 000 000 000 000 000 000 370 09B 441 0AC 001 13C 030421 */
+driver {
+ name = bfw240radio
+ meter_type = HeatCostAllocationMeter
+ default_fields = name,id,current_hca,prev_hca,timestamp
+ detect {
+ mvt = BFW,02,08
+ }
+ fields {
+ field {
+ name = payload
+ quantity = Text
+ attributes = HIDE,REQUIRED
+ match_entire_payload = true
+ ixml = "decode = byte, byte, byte, byte,
+ prev_bytes,
+ current_bytes,
+ byte, byte,
+ hist_18_17,
+ hist_16_15,
+ hist_14_13,
+ hist_12_11,
+ hist_10_09,
+ hist_08_07,
+ hist_06_05,
+ hist_04_03,
+ hist_02_01,
+ date_bytes.
+ -hex = ['A'-'F';'0'-'9'].
+ -byte = hex, hex.
+ -word = byte, byte.
+ -triplet = byte, byte, byte.
+ prev_bytes = word, @DV_prev_raw.
+ current_bytes = word, @DV_current_raw.
+ hist_18_17 = triplet, @DV_hist_18_17_raw.
+ hist_16_15 = triplet, @DV_hist_16_15_raw.
+ hist_14_13 = triplet, @DV_hist_14_13_raw.
+ hist_12_11 = triplet, @DV_hist_12_11_raw.
+ hist_10_09 = triplet, @DV_hist_10_09_raw.
+ hist_08_07 = triplet, @DV_hist_08_07_raw.
+ hist_06_05 = triplet, @DV_hist_06_05_raw.
+ hist_04_03 = triplet, @DV_hist_04_03_raw.
+ hist_02_01 = triplet, @DV_hist_02_01_raw.
+ date_bytes = triplet, @DV_date_raw.
+ DV_prev_raw>dvk = +'02FD3A'.
+ DV_current_raw>dvk = +'42FD3A'.
+ DV_hist_18_17_raw>dvk = +'03FD3A'.
+ DV_hist_16_15_raw>dvk = +'43FD3A'.
+ DV_hist_14_13_raw>dvk = +'8300FD3A'.
+ DV_hist_12_11_raw>dvk = +'C300FD3A'.
+ DV_hist_10_09_raw>dvk = +'8301FD3A'.
+ DV_hist_08_07_raw>dvk = +'C301FD3A'.
+ DV_hist_06_05_raw>dvk = +'8302FD3A'.
+ DV_hist_04_03_raw>dvk = +'C302FD3A'.
+ DV_hist_02_01_raw>dvk = +'8303FD3A'.
+ DV_date_raw>dvk = +'C303FD3A'."
+ }
+ field {
+ name = prev_raw
+ quantity = Dimensionless
+ attributes = HIDE
+ dif_signedness = Unsigned
+ match {
+ difvifkey = 02FD3A
+ }
+ }
+ field {
+ name = current_raw
+ quantity = Dimensionless
+ attributes = HIDE
+ dif_signedness = Unsigned
+ match {
+ difvifkey = 42FD3A
+ }
+ }
+ field {
+ name = hist_18_17_raw
+ quantity = Dimensionless
+ attributes = HIDE
+ dif_signedness = Unsigned
+ match {
+ difvifkey = 03FD3A
+ }
+ }
+ field {
+ name = hist_16_15_raw
+ quantity = Dimensionless
+ attributes = HIDE
+ dif_signedness = Unsigned
+ match {
+ difvifkey = 43FD3A
+ }
+ }
+ field {
+ name = hist_14_13_raw
+ quantity = Dimensionless
+ attributes = HIDE
+ dif_signedness = Unsigned
+ match {
+ difvifkey = 8300FD3A
+ }
+ }
+ field {
+ name = hist_12_11_raw
+ quantity = Dimensionless
+ attributes = HIDE
+ dif_signedness = Unsigned
+ match {
+ difvifkey = C300FD3A
+ }
+ }
+ field {
+ name = hist_10_09_raw
+ quantity = Dimensionless
+ attributes = HIDE
+ dif_signedness = Unsigned
+ match {
+ difvifkey = 8301FD3A
+ }
+ }
+ field {
+ name = hist_08_07_raw
+ quantity = Dimensionless
+ attributes = HIDE
+ dif_signedness = Unsigned
+ match {
+ difvifkey = C301FD3A
+ }
+ }
+ field {
+ name = hist_06_05_raw
+ quantity = Dimensionless
+ attributes = HIDE
+ dif_signedness = Unsigned
+ match {
+ difvifkey = 8302FD3A
+ }
+ }
+ field {
+ name = hist_04_03_raw
+ quantity = Dimensionless
+ attributes = HIDE
+ dif_signedness = Unsigned
+ match {
+ difvifkey = C302FD3A
+ }
+ }
+ field {
+ name = hist_02_01_raw
+ quantity = Dimensionless
+ attributes = HIDE
+ dif_signedness = Unsigned
+ match {
+ difvifkey = 8303FD3A
+ }
+ }
+ field {
+ name = date_raw
+ quantity = Dimensionless
+ attributes = HIDE
+ dif_signedness = Unsigned
+ match {
+ difvifkey = C303FD3A
+ }
+ }
+ field {
+ name = prev
+ quantity = HCA
+ info = 'Energy consumption at end of previous billing period.'
+ calculate = '(((prev_raw_counter % 256counter) * 256counter) + (prev_raw_counter >> 8counter)) * 1hca'
+ }
+ field {
+ name = current
+ quantity = HCA
+ info = 'Energy consumption so far in this billing period.'
+ calculate = '(((current_raw_counter % 256counter) * 256counter) + (current_raw_counter >> 8counter)) * 1hca'
+ }
+ field {
+ name = device_day_bcd
+ quantity = Dimensionless
+ attributes = HIDE
+ calculate = 'date_raw_counter % 256counter'
+ }
+ field {
+ name = device_month_bcd
+ quantity = Dimensionless
+ attributes = HIDE
+ calculate = '(date_raw_counter >> 8counter) % 256counter'
+ }
+ field {
+ name = device_year_bcd
+ quantity = Dimensionless
+ attributes = HIDE
+ calculate = 'date_raw_counter >> 16counter'
+ }
+ field {
+ name = device_day
+ quantity = Dimensionless
+ attributes = HIDE
+ calculate = '((device_day_bcd_counter >> 4counter) * 10counter) + (device_day_bcd_counter % 16counter)'
+ }
+ field {
+ name = device_month
+ quantity = Dimensionless
+ attributes = HIDE
+ calculate = '((device_month_bcd_counter >> 4counter) * 10counter) + (device_month_bcd_counter % 16counter)'
+ }
+ field {
+ name = device_year
+ quantity = Dimensionless
+ attributes = HIDE
+ calculate = '((device_year_bcd_counter >> 4counter) * 10counter) + (device_year_bcd_counter % 16counter)'
+ }
+ field {
+ name = device
+ quantity = PointInTime
+ display_unit = date
+ info = 'Device date when telegram was sent.'
+ calculate = "'2000-01-01' +
+ (((device_year_counter * 12counter) + device_month_counter - 1counter) * 1month) +
+ ((device_day_counter - 1counter) * 24h)"
+ }
+ field {
+ name = prev_01
+ quantity = HCA
+ info = 'Energy consumption 1 month ago.'
+ calculate = '((((hist_02_01_raw_counter >> 8counter) % 16counter) * 256counter) + (hist_02_01_raw_counter >> 16counter)) * 1hca'
+ }
+ field {
+ name = prev_02
+ quantity = HCA
+ info = 'Energy consumption 2 months ago.'
+ calculate = '(((((hist_02_01_raw_counter >> 8counter) % 256counter) >> 4counter) * 256counter) + (hist_02_01_raw_counter % 256counter)) * 1hca'
+ }
+ field {
+ name = prev_03
+ quantity = HCA
+ info = 'Energy consumption 3 months ago.'
+ calculate = '((((hist_04_03_raw_counter >> 8counter) % 16counter) * 256counter) + (hist_04_03_raw_counter >> 16counter)) * 1hca'
+ }
+ field {
+ name = prev_04
+ quantity = HCA
+ info = 'Energy consumption 4 months ago.'
+ calculate = '(((((hist_04_03_raw_counter >> 8counter) % 256counter) >> 4counter) * 256counter) + (hist_04_03_raw_counter % 256counter)) * 1hca'
+ }
+ field {
+ name = prev_05
+ quantity = HCA
+ info = 'Energy consumption 5 months ago.'
+ calculate = '((((hist_06_05_raw_counter >> 8counter) % 16counter) * 256counter) + (hist_06_05_raw_counter >> 16counter)) * 1hca'
+ }
+ field {
+ name = prev_06
+ quantity = HCA
+ info = 'Energy consumption 6 months ago.'
+ calculate = '(((((hist_06_05_raw_counter >> 8counter) % 256counter) >> 4counter) * 256counter) + (hist_06_05_raw_counter % 256counter)) * 1hca'
+ }
+ field {
+ name = prev_07
+ quantity = HCA
+ info = 'Energy consumption 7 months ago.'
+ calculate = '((((hist_08_07_raw_counter >> 8counter) % 16counter) * 256counter) + (hist_08_07_raw_counter >> 16counter)) * 1hca'
+ }
+ field {
+ name = prev_08
+ quantity = HCA
+ info = 'Energy consumption 8 months ago.'
+ calculate = '(((((hist_08_07_raw_counter >> 8counter) % 256counter) >> 4counter) * 256counter) + (hist_08_07_raw_counter % 256counter)) * 1hca'
+ }
+ field {
+ name = prev_09
+ quantity = HCA
+ info = 'Energy consumption 9 months ago.'
+ calculate = '((((hist_10_09_raw_counter >> 8counter) % 16counter) * 256counter) + (hist_10_09_raw_counter >> 16counter)) * 1hca'
+ }
+ field {
+ name = prev_10
+ quantity = HCA
+ info = 'Energy consumption 10 months ago.'
+ calculate = '(((((hist_10_09_raw_counter >> 8counter) % 256counter) >> 4counter) * 256counter) + (hist_10_09_raw_counter % 256counter)) * 1hca'
+ }
+ field {
+ name = prev_11
+ quantity = HCA
+ info = 'Energy consumption 11 months ago.'
+ calculate = '((((hist_12_11_raw_counter >> 8counter) % 16counter) * 256counter) + (hist_12_11_raw_counter >> 16counter)) * 1hca'
+ }
+ field {
+ name = prev_12
+ quantity = HCA
+ info = 'Energy consumption 12 months ago.'
+ calculate = '(((((hist_12_11_raw_counter >> 8counter) % 256counter) >> 4counter) * 256counter) + (hist_12_11_raw_counter % 256counter)) * 1hca'
+ }
+ field {
+ name = prev_13
+ quantity = HCA
+ info = 'Energy consumption 13 months ago.'
+ calculate = '((((hist_14_13_raw_counter >> 8counter) % 16counter) * 256counter) + (hist_14_13_raw_counter >> 16counter)) * 1hca'
+ }
+ field {
+ name = prev_14
+ quantity = HCA
+ info = 'Energy consumption 14 months ago.'
+ calculate = '(((((hist_14_13_raw_counter >> 8counter) % 256counter) >> 4counter) * 256counter) + (hist_14_13_raw_counter % 256counter)) * 1hca'
+ }
+ field {
+ name = prev_15
+ quantity = HCA
+ info = 'Energy consumption 15 months ago.'
+ calculate = '((((hist_16_15_raw_counter >> 8counter) % 16counter) * 256counter) + (hist_16_15_raw_counter >> 16counter)) * 1hca'
+ }
+ field {
+ name = prev_16
+ quantity = HCA
+ info = 'Energy consumption 16 months ago.'
+ calculate = '(((((hist_16_15_raw_counter >> 8counter) % 256counter) >> 4counter) * 256counter) + (hist_16_15_raw_counter % 256counter)) * 1hca'
+ }
+ field {
+ name = prev_17
+ quantity = HCA
+ info = 'Energy consumption 17 months ago.'
+ calculate = '((((hist_18_17_raw_counter >> 8counter) % 16counter) * 256counter) + (hist_18_17_raw_counter >> 16counter)) * 1hca'
+ }
+ field {
+ name = prev_18
+ quantity = HCA
+ info = 'Energy consumption 18 months ago.'
+ calculate = '(((((hist_18_17_raw_counter >> 8counter) % 256counter) >> 4counter) * 256counter) + (hist_18_17_raw_counter % 256counter)) * 1hca'
+ }
+ }
+ tests {
+ test {
+ args = 'bfw bfw240radio 00707788 NOKEY'
+ telegram = 3644D7088877700002087ADBC000002F2F9E1F03C10388152A00000000000000000000000000000204000404000EE2020AC1321D280221
+ json = '{"_":"telegram","media":"heat cost allocation","driver":"bfw240radio","name":"bfw","id":"00707788","current_hca":904,"prev_hca":961,"prev_01_hca":541,"prev_02_hca":961,"prev_03_hca":522,"prev_04_hca":226,"prev_05_hca":14,"prev_06_hca":4,"prev_07_hca":4,"prev_08_hca":4,"prev_09_hca":2,"prev_10_hca":0,"prev_11_hca":0,"prev_12_hca":0,"prev_13_hca":0,"prev_14_hca":0,"prev_15_hca":0,"prev_16_hca":0,"prev_17_hca":0,"prev_18_hca":0,"device_date":"2021-02-28","timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'bfw;00707788;904;961;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'bfw bfw240radio 00707788 NOKEY'
+ telegram = 3644D7088877700002087A8BC000002F2F011F03C1038D152A0000000000000000000000000200040400040E00E20A23C11D238D010321
+ json = '{"_":"telegram","media":"heat cost allocation","driver":"bfw240radio","name":"bfw","id":"00707788","current_hca":909,"prev_hca":961,"prev_01_hca":909,"prev_02_hca":541,"prev_03_hca":961,"prev_04_hca":522,"prev_05_hca":226,"prev_06_hca":14,"prev_07_hca":4,"prev_08_hca":4,"prev_09_hca":4,"prev_10_hca":2,"prev_11_hca":0,"prev_12_hca":0,"prev_13_hca":0,"prev_14_hca":0,"prev_15_hca":0,"prev_16_hca":0,"prev_17_hca":0,"prev_18_hca":0,"device_date":"2021-03-01","timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'bfw;00707788;909;961;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'bfww bfw240radio 00707076 NOKEY'
+ telegram = 3644D7087670700002087A9CC000002F2F6E1F000000000B36000000000000000000000000000000000000000000000000000000260221
+ json = '{"_":"telegram","media":"heat cost allocation","driver":"bfw240radio","name":"bfww","id":"00707076","current_hca":0,"prev_hca":0,"prev_01_hca":0,"prev_02_hca":0,"prev_03_hca":0,"prev_04_hca":0,"prev_05_hca":0,"prev_06_hca":0,"prev_07_hca":0,"prev_08_hca":0,"prev_09_hca":0,"prev_10_hca":0,"prev_11_hca":0,"prev_12_hca":0,"prev_13_hca":0,"prev_14_hca":0,"prev_15_hca":0,"prev_16_hca":0,"prev_17_hca":0,"prev_18_hca":0,"device_date":"2021-02-26","timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'bfww;00707076;0;0;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'bfww bfw240radio 00707076 NOKEY'
+ telegram = 3644D7087670700002087A27C000002F2F011F000000000B36000000000000000000000000000000000000000000000000000000010321
+ json = '{"_":"telegram","media":"heat cost allocation","driver":"bfw240radio","name":"bfww","id":"00707076","current_hca":0,"prev_hca":0,"prev_01_hca":0,"prev_02_hca":0,"prev_03_hca":0,"prev_04_hca":0,"prev_05_hca":0,"prev_06_hca":0,"prev_07_hca":0,"prev_08_hca":0,"prev_09_hca":0,"prev_10_hca":0,"prev_11_hca":0,"prev_12_hca":0,"prev_13_hca":0,"prev_14_hca":0,"prev_15_hca":0,"prev_16_hca":0,"prev_17_hca":0,"prev_18_hca":0,"device_date":"2021-03-01","timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'bfww;00707076;0;0;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'bfwww bfw240radio 00707447 NOKEY'
+ telegram = 3644D7084774700002087A80C000002F2F6F1F01440100147000000000000000000000000000000000000000000037009B4410AC260221
+ json = '{"_":"telegram","media":"heat cost allocation","driver":"bfw240radio","name":"bfwww","id":"00707447","current_hca":256,"prev_hca":324,"prev_01_hca":172,"prev_02_hca":324,"prev_03_hca":155,"prev_04_hca":55,"prev_05_hca":0,"prev_06_hca":0,"prev_07_hca":0,"prev_08_hca":0,"prev_09_hca":0,"prev_10_hca":0,"prev_11_hca":0,"prev_12_hca":0,"prev_13_hca":0,"prev_14_hca":0,"prev_15_hca":0,"prev_16_hca":0,"prev_17_hca":0,"prev_18_hca":0,"device_date":"2021-02-26","timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'bfwww;00707447;256;324;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'bfwww bfw240radio 00707447 NOKEY'
+ telegram = 3644D7084774700002087AE1C000002F2F481F0144010014700000000000000000000000000000000000000000379B0144AC0100040321
+ json = '{"_":"telegram","media":"heat cost allocation","driver":"bfw240radio","name":"bfwww","id":"00707447","current_hca":256,"prev_hca":324,"prev_01_hca":256,"prev_02_hca":172,"prev_03_hca":324,"prev_04_hca":155,"prev_05_hca":55,"prev_06_hca":0,"prev_07_hca":0,"prev_08_hca":0,"prev_09_hca":0,"prev_10_hca":0,"prev_11_hca":0,"prev_12_hca":0,"prev_13_hca":0,"prev_14_hca":0,"prev_15_hca":0,"prev_16_hca":0,"prev_17_hca":0,"prev_18_hca":0,"device_date":"2021-03-04","timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'bfwww;00707447;256;324;1111-11-11 11:11.11'
+ }
+ }
+}
diff --git a/components/wmbus_common/drivers/src/brunatawm.xmq b/components/wmbus_common/drivers/src/brunatawm.xmq
new file mode 100644
index 00000000..1174daf6
--- /dev/null
+++ b/components/wmbus_common/drivers/src/brunatawm.xmq
@@ -0,0 +1,31 @@
+// Copyright (C) 2026 Fredrik Öhrström (gpl-3.0-or-later)
+driver {
+ name = brunatawm
+ meter_type = WaterMeter
+ default_fields = name,id,status,total_m3,target_m3,timestamp
+ detect {
+ mvt = DWZ,01,06
+ }
+ library {
+ use = total_m3
+ use = target_m3
+ use = target_date
+ use = meter_date
+ }
+ fields {
+ field {
+ name = status
+ quantity = Text
+ info = 'Status and error flags.'
+ attributes = STATUS,INCLUDE_TPL_STATUS
+ }
+ }
+ tests {
+ test {
+ args = 'X brunatawm 21117187 NOKEY'
+ telegram = 3E44FA128771112101067ABA0020252F2F041377E50000026C5036426C3F3C44137AB900002F2F2F2F2F2F2F2F2F2F0F800C000109003DBF0100209A31414D
+ json = '{"_": "telegram","id": "21117187","media": "warm water","driver": "brunatawm","meter_date": "2026-06-16","name": "X","status": "OK","target_date": "2025-12-31","target_m3": 47.482,"timestamp": "1111-11-11T11:11:11Z","total_m3": 58.743}'
+ fields = 'X;21117187;OK;58.743;47.482;1111-11-11 11:11.11'
+ }
+ }
+}
diff --git a/components/wmbus_common/drivers/src/c5isf.xmq b/components/wmbus_common/drivers/src/c5isf.xmq
new file mode 100644
index 00000000..5a4d5820
--- /dev/null
+++ b/components/wmbus_common/drivers/src/c5isf.xmq
@@ -0,0 +1,291 @@
+/* Copyright (C) 2022-2026 Fredrik Öhrström (gpl-3.0-or-later)
+ Copyright (C) 2022 Alexander Streit (gpl-3.0-or-later) */
+driver {
+ name = c5isf
+ meter_type = HeatMeter
+ default_fields = name,id,total_energy_consumption_kwh,total_volume_m3,status,timestamp
+ detect {
+ mvt = ZRI,88,0d
+ mvt = ZRI,88,07
+ mvt = ZRI,88,04
+ }
+ fields {
+ // Common fields for T1A1, T1A2, T1B
+ field {
+ name = total_energy_consumption
+ quantity = Energy
+ info = 'The total heat energy consumption recorded by this meter.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyEnergyVIF
+ }
+ }
+ field {
+ name = total_cooling_consumption
+ quantity = Energy
+ info = 'The total cooling energy consumption recorded by this meter.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyEnergyVIF
+ tariff_nr = 1
+ }
+ }
+ field {
+ name = total_volume
+ quantity = Volume
+ info = 'The total heating media volume recorded by this meter.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = Volume
+ }
+ }
+ field {
+ name = status
+ quantity = Text
+ info = 'Status and error flags.'
+ attributes = STATUS,INCLUDE_TPL_STATUS
+ match {
+ measurement_type = Instantaneous
+ vif_range = ErrorFlags
+ }
+ lookup {
+ name = ERROR_FLAGS
+ map_type = DecimalsToString
+ mask_bits = 9999
+ default_message = OK
+ map {
+ name = VERIFICATION_EXPIRED
+ value = 2000
+ test = Set
+ }
+ map {
+ name = BATTERY_EXPIRED
+ value = 1000
+ test = Set
+ }
+ map {
+ name = WIRELESS_ERROR
+ value = 800
+ test = Set
+ }
+ map {
+ name = HARDWARE_ERROR3
+ value = 100
+ test = Set
+ }
+ map {
+ name = VALUE_OVERLOAD
+ value = 50
+ test = Set
+ }
+ map {
+ name = AIR_INSIDE
+ value = 40
+ test = Set
+ }
+ map {
+ name = REVERSE_FLOW
+ value = 30
+ test = Set
+ }
+ map {
+ name = DRY
+ value = 20
+ test = Set
+ }
+ map {
+ name = ERROR_MEASURING
+ value = 10
+ test = Set
+ }
+ map {
+ name = HARDWARE_ERROR2
+ value = 9
+ test = Set
+ }
+ map {
+ name = HARDWARE_ERROR1
+ value = 8
+ test = Set
+ }
+ map {
+ name = LOW_BATTERY
+ value = 7
+ test = Set
+ }
+ map {
+ name = SUPPLY_SENSOR_INTERRUPTED
+ value = 6
+ test = Set
+ }
+ map {
+ name = SHORT_CIRCUIT_SUPPLY_SENSOR
+ value = 5
+ test = Set
+ }
+ map {
+ name = RETURN_SENSOR_INTERRUPTED
+ value = 4
+ test = Set
+ }
+ map {
+ name = SHORT_CIRCUIT_RETURN_SENSOR
+ value = 3
+ test = Set
+ }
+ map {
+ name = TEMP_ABOVE_RANGE
+ value = 2
+ test = Set
+ }
+ map {
+ name = TEMP_BELOW_RANGE
+ value = 1
+ test = Set
+ }
+ }
+ }
+ // Previous month dates (T1A1 and T1A2, storage 32-45)
+ field {
+ name = 'prev_{storage_counter-31counter}_month'
+ quantity = PointInTime
+ info = 'Previous month date.'
+ display_unit = date
+ match {
+ measurement_type = Instantaneous
+ vif_range = Date
+ storage_nr = 32,45
+ }
+ }
+ // Previous month energy (T1A1, storage 32-45)
+ field {
+ name = 'prev_{storage_counter-31counter}_month'
+ quantity = Energy
+ info = 'Previous month energy.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyEnergyVIF
+ storage_nr = 32,45
+ }
+ }
+ // Previous month volume (T1A2, storage 32-45)
+ field {
+ name = 'prev_{storage_counter-31counter}_month'
+ quantity = Volume
+ info = 'Previous month volume.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = Volume
+ storage_nr = 32,45
+ }
+ }
+ // T1B billing fields
+ field {
+ name = due_energy_consumption
+ quantity = Energy
+ info = 'The total heat energy consumption at the due date.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyEnergyVIF
+ storage_nr = 8
+ }
+ }
+ field {
+ name = due
+ quantity = PointInTime
+ info = 'The due date.'
+ display_unit = date
+ match {
+ measurement_type = Instantaneous
+ vif_range = Date
+ storage_nr = 8
+ }
+ }
+ field {
+ name = volume_flow
+ quantity = Flow
+ info = 'The current heat media volume flow.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = VolumeFlow
+ }
+ }
+ field {
+ name = power
+ quantity = Power
+ info = 'The current power consumption.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = PowerW
+ }
+ }
+ field {
+ name = total_energy_consumption_last_month
+ quantity = Energy
+ info = 'The total heat energy consumption recorded at end of last month.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyEnergyVIF
+ storage_nr = 32
+ }
+ }
+ field {
+ name = last_month
+ quantity = PointInTime
+ info = 'Last month date and time.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = DateTime
+ }
+ }
+ field {
+ name = max_power_last_month
+ quantity = Power
+ info = 'Maximum power consumption last month.'
+ match {
+ measurement_type = Maximum
+ vif_range = PowerW
+ storage_nr = 32
+ add_combinable = PerMonth
+ }
+ }
+ field {
+ name = flow_temperature
+ quantity = Temperature
+ info = 'The current forward heat media temperature.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = FlowTemperature
+ }
+ }
+ field {
+ name = return_temperature
+ quantity = Temperature
+ info = 'The current return heat media temperature.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = ReturnTemperature
+ }
+ }
+ }
+ tests {
+ test {
+ args = 'Heat c5isf 55445555 NOKEY'
+ telegram = E544496A55554455880D7A320200002F2F_04060000000004130000000002FD17240084800106000000008280016C2124C480010600000080C280016CFFFF84810106000000808281016CFFFFC481010600000080C281016CFFFF84820106000000808282016CFFFFC482010600000080C282016CFFFF84830106000000808283016CFFFFC483010600000080C283016CFFFF84840106000000808284016CFFFFC484010600000080C284016CFFFF84850106000000808285016CFFFFC485010600000080C285016CFFFF84860106000000808286016CFFFFC486010600000080C286016CFFFF
+ json = '{"_":"telegram","media":"heat/cooling load","driver":"c5isf","name":"Heat","id":"55445555","prev_10_month_kwh":-2147483648,"prev_10_month_date":null,"prev_11_month_kwh":-2147483648,"prev_11_month_date":null,"prev_12_month_kwh":-2147483648,"prev_12_month_date":null,"prev_13_month_kwh":-2147483648,"prev_13_month_date":null,"prev_14_month_kwh":-2147483648,"prev_14_month_date":null,"prev_1_month_kwh":0,"prev_1_month_date":"2017-04-01","prev_2_month_kwh":-2147483648,"prev_2_month_date":null,"prev_3_month_kwh":-2147483648,"prev_3_month_date":null,"prev_4_month_kwh":-2147483648,"prev_4_month_date":null,"prev_5_month_kwh":-2147483648,"prev_5_month_date":null,"prev_6_month_kwh":-2147483648,"prev_6_month_date":null,"prev_7_month_kwh":-2147483648,"prev_7_month_date":null,"prev_8_month_kwh":-2147483648,"prev_8_month_date":null,"prev_9_month_kwh":-2147483648,"prev_9_month_date":null,"total_energy_consumption_kwh":0,"total_energy_consumption_last_month_kwh":0,"total_volume_m3":0,"status":"DRY ERROR RETURN_SENSOR_INTERRUPTED","timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'Heat;55445555;0;0;DRY ERROR RETURN_SENSOR_INTERRUPTED;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'Heat c5isf 32002044 NOKEY'
+ telegram = 5E44496A4420003288047AFC0050052F2F_0406D00E00000413B28A05008404060000000082046CC121043B00000000042D000000000259E719025D051402FD17000084800106C00C00008280016CC125948001AE25090000002F2F2F2F2F2F
+ json = '{"_":"telegram","media":"heat","driver":"c5isf","name":"Heat","id":"32002044","due_date":"2022-01-01","due_energy_consumption_kwh":0,"flow_temperature_c":66.31,"max_power_last_month_kw":9,"power_kw":0,"prev_1_month_kwh":3264,"prev_1_month_date":"2022-05-01","return_temperature_c":51.25,"total_energy_consumption_kwh":3792,"total_energy_consumption_last_month_kwh":3264,"total_volume_m3":363.186,"volume_flow_m3h":0,"status":"OK","timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'Heat;32002044;3792;363.186;OK;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'HeatCool c5isf 32022703 NOKEY'
+ telegram = 8E44496A03270232880D7AED0080052F2F0406050200008410069A1700000413BC6F2E00840406E701000082046C2131841406C20B000082146C2131043B00000000042D0000000002595D09025D240802FD17000084800106050200008280016C213C948001AE2501000000849001069A1700008290016C213C849001AE25000000002F2F2F2F2F2F2F2F2F2F2F2F
+ json = '{"_":"telegram","media":"heat/cooling load","driver":"c5isf","name":"HeatCool","id":"32022703","due_date":"2025-01-01","due_energy_consumption_kwh":487,"flow_temperature_c":23.97,"max_power_last_month_kw":1,"power_kw":0,"prev_1_month_kwh":517,"prev_1_month_date":"2025-12-01","return_temperature_c":20.84,"total_cooling_consumption_kwh":6042,"total_energy_consumption_kwh":517,"total_energy_consumption_last_month_kwh":517,"total_volume_m3":3043.26,"volume_flow_m3h":0,"status":"OK","timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'HeatCool;32022703;517;3043.26;OK;1111-11-11 11:11.11'
+ }
+ }
+}
diff --git a/components/wmbus_common/drivers/src/caltose.xmq b/components/wmbus_common/drivers/src/caltose.xmq
new file mode 100644
index 00000000..05d77955
--- /dev/null
+++ b/components/wmbus_common/drivers/src/caltose.xmq
@@ -0,0 +1,135 @@
+// Copyright (C) 2026 Hynek Moravec (gpl-3.0-or-later)
+//
+// Zenner Caltos E (D1605) heat cost allocator, manufacturer ZRI, version 0xFC,
+// device type 0x08 (heat cost allocation). The same meter is also marketed
+// generically as "Zenner HCA"; this driver covers both. It supersedes the
+// earlier duplicate "zennerhca" driver, which detected the identical
+// ZRI,FC,08 fingerprint and caused ambiguous detection. Driver named after the
+// device model (Caltos E), matching the project convention (c5isf, minomess, ...).
+//
+// Status comes from the manufacturer bits of the TPL status byte (see the
+// mfct_tpl_status_bits block) per the Zenner Caltos E / D1605-HCA spec:
+// 0x04 BATTERY_WARNING, 0x08 INVALID_METROLOGICAL_TABLE, 0x20 REMOVAL,
+// 0x40 MODE_2F (commissioning/learning mode), 0x80 PRODUCT_SCALE.
+driver {
+ name = caltose
+ aliases = zennerhca
+ meter_type = HeatCostAllocationMeter
+ default_fields = name,id,status,consumption_hca,target_hca,target_date,timestamp
+ detect {
+ mvt = ZRI,FC,08
+ }
+ mfct_tpl_status_bits {
+ mask_bits = 0xff
+ default_message = OK
+ map {
+ name = BATTERY_WARNING
+ value = 0x04
+ test = set
+ }
+ map {
+ name = INVALID_METROLOGICAL_TABLE
+ value = 0x08
+ test = set
+ }
+ map {
+ name = REMOVAL
+ value = 0x20
+ test = set
+ }
+ map {
+ name = MODE_2F
+ value = 0x40
+ test = set
+ }
+ map {
+ name = PRODUCT_SCALE
+ value = 0x80
+ test = set
+ }
+ }
+ library {
+ use = consumption_hca
+ use = target_hca
+ use = target_date
+ }
+ fields {
+ field {
+ name = status
+ quantity = Text
+ info = 'Status and error flags.'
+ attributes = STATUS,INCLUDE_TPL_STATUS
+ }
+ field {
+ name = base
+ quantity = PointInTime
+ info = 'Base date of compact profile (most recent monthly value).'
+ display_unit = date
+ match {
+ measurement_type = Instantaneous
+ vif_range = Date
+ storage_nr = 8
+ }
+ }
+ field {
+ name = base
+ quantity = HCA
+ info = 'Base HCA value of compact profile.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = HeatCostAllocation
+ storage_nr = 8
+ }
+ }
+ field {
+ name = 'consumption_{storage_counter-8counter}'
+ quantity = HCA
+ info = 'Compact-profile history values (most recent first after base month).'
+ match {
+ measurement_type = Instantaneous
+ vif_range = HeatCostAllocation
+ add_combinable = Synthetic
+ storage_nr = 9,40
+ }
+ }
+ field {
+ name = 'consumption_{storage_counter-8counter}'
+ quantity = Text
+ info = 'Billing period date for each compact-profile history slot.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = Date
+ add_combinable = Synthetic
+ storage_nr = 9,40
+ }
+ }
+ }
+ tests {
+ test {
+ args = 'Caltos caltose 80081991 6B6B5EB80884328A7B1E45043D39FAAD'
+ telegram = 5e44496a91190880fc087a744050052f05bf0c6baf613a078098e048ae47ffb93083b455065e03de9b7878c8b3bb063637ea5693160e831cc3d4e0470fabbcd4fedb09d289a0025ca7960d5c7b8064b1093037e7a0a57908edb554d17ad587
+ json = '{"_":"telegram","media":"heat cost allocation","driver":"caltose","name":"Caltos","id":"80081991","base_date":"2026-02-01","base_hca":203,"consumption_hca":225,"consumption_1_hca":99,"consumption_1":"2026-01-01","consumption_2_hca":52,"consumption_2":"2025-12-01","consumption_3_hca":10,"consumption_3":"2025-11-01","consumption_4_hca":0,"consumption_4":"2025-10-01","target_date":"2026-01-01","target_hca":99,"status":"MODE_2F","timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'Caltos;80081991;MODE_2F;225;99;2026-01-01;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'Caltos2 caltose 80081809 EB7CA876CAA641DAC39239326F6DD7EA'
+ telegram = 5e44496a09180880fc087a84405005760039129d1ce51ce54ef7b4aa5383f1fd525f87046d4f869a3189e5b0c400ecf9c41ae6ed6cf94810f09f4f113c04cc731e379e62ea0331ec7f8b09511357a7f7a484851c3de2ec414dcfdebd4af67d
+ json = '{"_":"telegram","media":"heat cost allocation","driver":"caltose","name":"Caltos2","id":"80081809","base_date":"2026-02-01","base_hca":0,"consumption_hca":0,"consumption_1_hca":0,"consumption_1":"2026-01-01","consumption_2_hca":0,"consumption_2":"2025-12-01","consumption_3_hca":0,"consumption_3":"2025-11-01","consumption_4_hca":0,"consumption_4":"2025-10-01","target_date":"2026-01-01","target_hca":0,"status":"MODE_2F","timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'Caltos2;80081809;MODE_2F;0;0;2026-01-01;1111-11-11 11:11.11'
+ }
+ test {
+ comment = 'Real meter previously covered by the removed zennerhca driver'
+ args = 'Caltos3 caltose 80081812 DC7C9EF16126348CDFD52CE6567A9FFD'
+ telegram = 5e44496a12180880fc087a7040500584a63344b5af071a23539c6020cbba81fa6adcfe738682a0924d4f6f7d89d165c9144e4918cdc2d5f86be7b5bd8143528273accc131a13d0b100f2540dc1f2c379dde4984236d3bca424113cf0ee1bbd
+ json = '{"_":"telegram","media":"heat cost allocation","driver":"caltose","name":"Caltos3","id":"80081812","base_date":"2026-02-01","base_hca":272,"consumption_hca":339,"consumption_1_hca":627,"consumption_1":"2026-01-01","consumption_2_hca":395,"consumption_2":"2025-12-01","consumption_3_hca":176,"consumption_3":"2025-11-01","consumption_4_hca":7,"consumption_4":"2025-10-01","target_date":"2026-01-01","target_hca":627,"status":"MODE_2F","timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'Caltos3;80081812;MODE_2F;339;627;2026-01-01;1111-11-11 11:11.11'
+ }
+ test {
+ comment = 'Real meter previously covered by the removed zennerhca driver'
+ args = 'Caltos4 caltose 80081907 750381240D0A7E371D4CB8D1869D8F9B'
+ telegram = 5e44496a07190880fc087a714050058922c9598bdddfbb5bbf44b9d54830830eb6be3ba8117dfc88ae1b251837dfecb04e071554125366ef9d72dce87c9a099ad0cd9bf70f4a2e9c4c58d780444219c3f546c887fbc2d93c272e314f925473
+ json = '{"_":"telegram","media":"heat cost allocation","driver":"caltose","name":"Caltos4","id":"80081907","base_date":"2026-02-01","base_hca":0,"consumption_hca":0,"consumption_1_hca":2,"consumption_1":"2026-01-01","consumption_2_hca":0,"consumption_2":"2025-12-01","consumption_3_hca":0,"consumption_3":"2025-11-01","consumption_4_hca":0,"consumption_4":"2025-10-01","target_date":"2026-01-01","target_hca":2,"status":"MODE_2F","timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'Caltos4;80081907;MODE_2F;0;2;2026-01-01;1111-11-11 11:11.11'
+ }
+ }
+}
diff --git a/components/wmbus_common/drivers/src/cma12w.xmq b/components/wmbus_common/drivers/src/cma12w.xmq
new file mode 100644
index 00000000..f49aa144
--- /dev/null
+++ b/components/wmbus_common/drivers/src/cma12w.xmq
@@ -0,0 +1,68 @@
+// Copyright (C) 2020-2022 Fredrik Öhrström (gpl-3.0-or-later)
+driver {
+ name = cma12w
+ meter_type = TempHygroMeter
+ default_fields = name,id,current_temperature_c,timestamp
+ detect {
+ mvt = ELV,20,1b
+ mvt = ELV,1f,1b
+ }
+ library {
+ use = software_version
+ }
+ fields {
+ field {
+ name = status
+ quantity = Text
+ info = 'Meter status from tpl status field.'
+ attributes = STATUS,INCLUDE_TPL_STATUS
+ }
+ field {
+ name = current_temperature
+ quantity = Temperature
+ info = 'The current temperature.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = ExternalTemperature
+ }
+ }
+ field {
+ name = average_temperature_1h
+ quantity = Temperature
+ info = 'The average temperature over the last hour.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = ExternalTemperature
+ storage_nr = 1
+ }
+ }
+ field {
+ name = battery
+ quantity = Text
+ info = 'Battery status.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = DigitalInput
+ }
+ lookup {
+ name = BATTERY
+ map_type = BitToString
+ mask_bits = 0xffff
+ }
+ }
+ }
+ tests {
+ test {
+ args = 'Tempo cma12w 66666666 NOKEY'
+ telegram = 2744961566666666201B7AF9000020_2F2F02651E094265180902FD1B30030DFD0F05302E302E340F
+ json = '{"_":"telegram","media":"room sensor","driver":"cma12w","name":"Tempo","id":"66666666","software_version":"4.0.0","status":"OK","current_temperature_c":23.34,"average_temperature_1h_c":23.28,"battery":"BATTERY_330","timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'Tempo;66666666;23.34;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'Tempo2 cma12w 68003902 NOKEY'
+ telegram = 27449615023900681F1B7AC5000020_2F2F02651E094265210902FD1B30420DFD0F05352E302E330F
+ json = '{"_": "telegram","average_temperature_1h_c": 23.37,"battery": "BATTERY_4230","current_temperature_c": 23.34,"driver": "cma12w","id": "68003902","media": "room sensor","name": "Tempo2","software_version": "3.0.5","status": "OK","timestamp": "1111-11-11T11:11:11Z"}'
+ fields = 'Tempo2;68003902;23.34;1111-11-11 11:11.11'
+ }
+ }
+}
diff --git a/components/wmbus_common/drivers/src/compact5.xmq b/components/wmbus_common/drivers/src/compact5.xmq
new file mode 100644
index 00000000..d36d6b07
--- /dev/null
+++ b/components/wmbus_common/drivers/src/compact5.xmq
@@ -0,0 +1,99 @@
+// Copyright (C) 2020-2022 Fredrik Öhrström (gpl-3.0-or-later)
+driver {
+ name = compact5
+ meter_type = HeatMeter
+ default_fields = name,id,total_kwh,current_kwh,previous_kwh,timestamp
+ detect {
+ mvt = TCH,45,04
+ mvt = TCH,45,C3
+ mvt = TCH,22,43
+ mvt = TCH,45,43
+ mvt = TCH,39,43
+ }
+ fields {
+ field {
+ name = payload_decoder
+ quantity = Text
+ info = 'Decode mfct specific payload (CI=0xA1/0xA2) into prev/curr billing values.'
+ attributes = HIDE
+ match_entire_payload = true
+ ixml = "decode = -hdr, prev_val, -one_byte, curr_val, -rest.
+ -hdr = hex, hex, hex, hex, hex, hex.
+ -one_byte = hex, hex.
+ -rest = hex*.
+ -hex = ['A'-'F'; '0'-'9'].
+ prev_val = hex, hex, hex, hex, hex, hex, @DV_PREV.
+ curr_val = hex, hex, hex, hex, hex, hex, @DV_CURR.
+ DV_PREV>dvk = +'037E'.
+ DV_CURR>dvk = +'037F'."
+ }
+ field {
+ name = prev_raw
+ quantity = Dimensionless
+ info = 'Raw previous billing period value in kWh.'
+ attributes = HIDE
+ display_unit = counter
+ vif_scaling = None
+ dif_signedness = Unsigned
+ match {
+ difvifkey = 037E
+ }
+ }
+ field {
+ name = curr_raw
+ quantity = Dimensionless
+ info = 'Raw current billing period value in kWh.'
+ attributes = HIDE
+ display_unit = counter
+ vif_scaling = None
+ dif_signedness = Unsigned
+ match {
+ difvifkey = 037F
+ }
+ }
+ field {
+ name = total
+ quantity = Energy
+ info = 'The total energy consumption recorded by this meter.'
+ calculate = '(prev_raw_counter + curr_raw_counter) / 1 counter * 1 kwh'
+ }
+ field {
+ name = current
+ quantity = Energy
+ info = 'Energy consumption so far in this billing period.'
+ calculate = 'curr_raw_counter / 1 counter * 1 kwh'
+ }
+ field {
+ name = previous
+ quantity = Energy
+ info = 'Energy consumption in previous billing period.'
+ calculate = 'prev_raw_counter / 1 counter * 1 kwh'
+ }
+ }
+ tests {
+ test {
+ args = 'Heating compact5 62626262 NOKEY'
+ telegram = 36446850626262624543A1_009F2777010060780000000A000000000000000000000000000000000000000000000000A0400000B4010000
+ json = '{"_":"telegram","current_kwh":120,"id":"62626262","media":"heat","driver":"compact5","name":"Heating","previous_kwh":375,"timestamp":"1111-11-11T11:11:11Z","total_kwh":495}'
+ fields = 'Heating;62626262;495;120;375;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'Heating2 compact5 66336633 NOKEY'
+ telegram = 37446850336633663943a2_10672c866100181c01000480794435d50000000000000000000000000000000000000000000000000000000000
+ json = '{"_":"telegram","current_kwh":284,"id":"66336633","media":"heat","driver":"compact5","name":"Heating2","previous_kwh":24966,"timestamp":"1111-11-11T11:11:11Z","total_kwh":25250}'
+ fields = 'Heating2;66336633;25250;284;24966;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'vario compact5 67552079 NOKEY'
+ telegram = 37446850792055673943A2_109F2F13C500608F1D00008066E8A69B26988D335F6411450C564C5145145CA0F1DA35B9DD37A1936BBF3D31D8
+ json = '{"_":"telegram","current_kwh":7567,"id":"67552079","media":"heat","driver":"compact5","name":"vario","previous_kwh":50451,"timestamp":"1111-11-11T11:11:11Z","total_kwh":58018}'
+ fields = 'vario;67552079;58018;7567;50451;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'tt compact5 41711422 NOKEY'
+ telegram = 3d446850221471414543a100ff32406002c8cd0000019f81071f68e001072c0c9106642075B5a6ac6ad229e993376aa7d10e1e5c60c1050000000056fed3
+ json = '{"_":"telegram","current_kwh":205,"id":"41711422","media":"heat","driver":"compact5","name":"tt","previous_kwh":155712,"timestamp":"1111-11-11T11:11:11Z","total_kwh":155917}'
+ fields = 'tt;41711422;155917;205;155712;1111-11-11 11:11.11'
+ }
+ }
+}
diff --git a/components/wmbus_common/drivers/src/dme173.xmq b/components/wmbus_common/drivers/src/dme173.xmq
new file mode 100644
index 00000000..f9fdb968
--- /dev/null
+++ b/components/wmbus_common/drivers/src/dme173.xmq
@@ -0,0 +1,87 @@
+// Copyright (C) 2026 Fredrik Öhrström (gpl-3.0-or-later)
+driver {
+ name = dme173
+ meter_type = WaterMeter
+ default_fields = name,id,status,total_m3,timestamp
+ detect {
+ mvt = DME,63,07
+ }
+ library {
+ use = total_m3
+ use = flow_temperature_c
+ use = total_backward_m3
+ }
+ fields {
+ field {
+ name = status
+ quantity = Text
+ info = 'Status and error flags.'
+ attributes = INCLUDE_TPL_STATUS
+ match {
+ measurement_type = Instantaneous
+ vif_range = ErrorFlags
+ }
+ lookup {
+ name = ERROR_FLAGS
+ map_type = BitToString
+ mask_bits = 0xffffffff
+ default_message = OK
+ }
+ }
+ field {
+ name = target
+ quantity = Volume
+ info = 'The total water consumption at the end of last billing period.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = Volume
+ storage_nr = 8
+ }
+ }
+ field {
+ name = target
+ quantity = PointInTime
+ info = 'The end of last billing period.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = DateTime
+ storage_nr = 8
+ }
+ }
+ field {
+ name = total_at_set_date
+ quantity = Volume
+ info = 'The total water consumption even earlier?'
+ match {
+ measurement_type = Instantaneous
+ vif_range = Volume
+ storage_nr = 1
+ }
+ }
+ field {
+ name = set
+ quantity = PointInTime
+ info = 'The even earlier date?'
+ display_unit = date
+ match {
+ measurement_type = Instantaneous
+ vif_range = Date
+ storage_nr = 1
+ }
+ }
+ }
+ tests {
+ test {
+ args = 'dwater dme173 22332233 NOKEY'
+ telegram = 5344A5113322332263078C0085900F002C256C1601004BB24778D6AC091E7ADF003107102F2F_0C138386000004FD17000000000A5A550002FD74EE114C1300020000426C3F3C84046D3B375F318C041374460000
+ json = '{"_": "telegram","flow_temperature_c": 5.5,"id": "22332233","media": "water","driver": "dme173","name": "dwater","set_date": "2025-12-31","status": "OK","target_datetime": "2026-01-31 23:59","target_m3": 4.674,"timestamp": "1111-11-11T11:11:11Z","total_at_set_date_m3": 0.2,"total_m3": 8.683}'
+ fields = 'dwater;22332233;OK;8.683;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'dwater2 dme173 44334433 NOKEY'
+ telegram = 5344A5113344334463078C002A900F002C25B7640000BAD0D4FB907B9BE17AFA003107102F2F_0C131717000004FD17000000000C933C000000004C1300020000426C3F3C84046D3B375F318C0413000200002F2F
+ json = '{"_": "telegram","id": "44334433","media": "water","driver": "dme173","name": "dwater2","set_date": "2025-12-31","status": "OK","target_datetime": "2026-01-31 23:59","target_m3": 0.2,"timestamp": "1111-11-11T11:11:11Z","total_at_set_date_m3": 0.2,"total_backward_m3": 0,"total_m3": 1.717}'
+ fields = 'dwater2;44334433;OK;1.717;1111-11-11 11:11.11'
+ }
+ }
+}
diff --git a/components/wmbus_common/drivers/src/dme_07.xmq b/components/wmbus_common/drivers/src/dme_07.xmq
new file mode 100644
index 00000000..d78a37b5
--- /dev/null
+++ b/components/wmbus_common/drivers/src/dme_07.xmq
@@ -0,0 +1,44 @@
+// Copyright (C) 2021-2022 Fredrik Öhrström (gpl-3.0-or-later)
+driver {
+ name = dme_07
+ meter_type = WaterMeter
+ default_fields = name,id,total_m3,status,timestamp
+ detect {
+ mvt = DME,7b,07
+ }
+ fields {
+ field {
+ name = status
+ quantity = Text
+ info = 'Status of meter.'
+ attributes = STATUS
+ match {
+ measurement_type = Instantaneous
+ vif_range = ErrorFlags
+ }
+ lookup {
+ name = ERROR_FLAGS
+ map_type = BitToString
+ mask_bits = 0xffff
+ default_message = OK
+ }
+ }
+ field {
+ name = total
+ quantity = Volume
+ info = 'The total water consumption recorded by this meter.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = Volume
+ }
+ }
+ }
+ tests {
+ test {
+ args = 'DigiWasser dme_07 93929190 NOKEY'
+ telegram = 1E44A511909192937B077A9F0010052F2F_04130347030002FD1700002F2F2F
+ json = '{"_":"telegram","media":"water","driver":"dme_07","name":"DigiWasser","id":"93929190","status":"OK","total_m3":214.787,"timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'DigiWasser;93929190;214.787;OK;1111-11-11 11:11.11'
+ }
+ }
+}
diff --git a/components/wmbus_common/drivers/src/ebeco500.xmq b/components/wmbus_common/drivers/src/ebeco500.xmq
new file mode 100644
index 00000000..36ab81b8
--- /dev/null
+++ b/components/wmbus_common/drivers/src/ebeco500.xmq
@@ -0,0 +1,122 @@
+// Copyright (C) 2026 Magnus Friskytt (gpl-3.0-or-later)
+driver {
+ name = ebeco500
+ meter_type = ElectricityMeter
+ meter_type_text = 'Underfloor Heating Thermostat'
+ default_fields = name,id,status,total_kwh,external_temperature_c,on_time_h,timestamp
+ manufacturer = Ebeco
+ detect {
+ mvt = EBE,01,02
+ }
+ library {
+ use = external_temperature_c
+ use = total_kwh
+ use = on_time_h
+ use = meter_datetime
+ }
+ fields {
+ field {
+ name = status
+ quantity = Text
+ info = 'Thermostat error and status flags.'
+ attributes = STATUS,INCLUDE_TPL_STATUS
+ match {
+ difvifkey = 02FB17
+ }
+ lookup {
+ name = ERROR_FLAGS
+ map_type = BitToString
+ mask_bits = 0xffff
+ default_message = OK
+ map {
+ bit = 0
+ name = TEMPORARY
+ test = Set
+ }
+ map {
+ bit = 1
+ name = MIN_LIMIT
+ test = Set
+ }
+ map {
+ bit = 2
+ name = MAX_LIMIT
+ test = Set
+ }
+ map {
+ bit = 3
+ name = OPEN_WINDOW
+ test = Set
+ }
+ map {
+ bit = 4
+ name = ROOM_OC
+ test = Set
+ }
+ map {
+ bit = 5
+ name = ROOM_SC
+ test = Set
+ }
+ map {
+ bit = 6
+ name = FLOOR_OC
+ test = Set
+ }
+ map {
+ bit = 7
+ name = FLOOR_SC
+ test = Set
+ }
+ map {
+ bit = 8
+ name = RELAY_OC
+ test = Set
+ }
+ map {
+ bit = 9
+ name = RELAY_SC
+ test = Set
+ }
+ map {
+ bit = 10
+ name = RELAY_OT
+ test = Set
+ }
+ map {
+ bit = 11
+ name = SYSTEM
+ test = Set
+ }
+ map {
+ bit = 12
+ name = STARTUP
+ test = Set
+ }
+ map {
+ bit = 13
+ name = POWER_CTRL
+ test = Set
+ }
+ map {
+ bit = 14
+ name = POWER_OFF
+ test = Set
+ }
+ map {
+ bit = 15
+ name = RELAY_TEST
+ test = Set
+ }
+ }
+ }
+ }
+ tests {
+ test {
+ args = 'Golvvarme ebeco500 000001e3 FCC9B808216D9D1F5FF6E1A3CF7D3084'
+ telegram = 31444514e301000001028c20807a2e002025e25dea4f2bc8ec94920260443a95c2bf3856e3e380e889cc8233ca5d9771f289
+ json = '{"_":"telegram","media":"electricity","driver":"ebeco500","name":"Golvvarme","id":"000001e3","on_time_h":105.633333,"external_temperature_c":22.8,"total_kwh":68.6,"meter_datetime":"2000-02-01 13:54:50","status":"OK","timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'Golvvarme;000001e3;OK;68.6;22.8;105.633333;1111-11-11 11:11.11'
+ }
+ }
+}
diff --git a/components/wmbus_common/drivers/src/ebzwmbe.xmq b/components/wmbus_common/drivers/src/ebzwmbe.xmq
new file mode 100644
index 00000000..3498ec9d
--- /dev/null
+++ b/components/wmbus_common/drivers/src/ebzwmbe.xmq
@@ -0,0 +1,82 @@
+// Copyright (C) 2020-2022 Fredrik Öhrström (gpl-3.0-or-later)
+driver {
+ name = ebzwmbe
+ meter_type = ElectricityMeter
+ default_fields = name,id,total_energy_consumption_kwh,current_power_consumption_kw,current_power_consumption_phase1_kw,current_power_consumption_phase2_kw,current_power_consumption_phase3_kw,timestamp
+ detect {
+ mvt = EBZ,01,02
+ }
+ fields {
+ field {
+ name = total_energy_consumption
+ quantity = Energy
+ info = 'The total energy consumption recorded by this meter.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyEnergyVIF
+ }
+ }
+ field {
+ name = current_power_consumption_phase1
+ quantity = Power
+ info = 'Current power consumption at phase 1.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ difvifkey = 04A9FF01
+ }
+ }
+ field {
+ name = current_power_consumption_phase2
+ quantity = Power
+ info = 'Current power consumption at phase 2.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ difvifkey = 04A9FF02
+ }
+ }
+ field {
+ name = current_power_consumption_phase3
+ quantity = Power
+ info = 'Current power consumption at phase 3.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ difvifkey = 04A9FF03
+ }
+ }
+ field {
+ name = customer
+ quantity = Text
+ info = 'Customer name.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = Customer
+ }
+ }
+ field {
+ name = current_power_consumption
+ quantity = Power
+ info = 'Calculated sum of power consumption of all phases.'
+ calculate = current_power_consumption_phase1_kw+current_power_consumption_phase2_kw+current_power_consumption_phase3_kw
+ }
+ }
+ tests {
+ test {
+ args = 'Elen1 ebzwmbe 22992299 NOKEY'
+ telegram = 5B445A149922992202378C20F6900F002C25BC9E0000BF48954821BC508D72992299225A140102F6003007102F2F040330F92A0004A9FF01FF24000004A9FF026A29000004A9FF03460600000DFD11063132333435362F2F2F2F2F2F
+ json = '{"_":"telegram","media":"electricity","driver":"ebzwmbe","name":"Elen1","id":"22992299","total_energy_consumption_kwh":2816.304,"current_power_consumption_phase1_kw":0.09471,"current_power_consumption_phase2_kw":0.10602,"current_power_consumption_phase3_kw":0.01606,"customer":"654321","current_power_consumption_kw":0.21679,"timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'Elen1;22992299;2816.304;0.21679;0.09471;0.10602;0.01606;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'MyEl ebzwmbe 01135263 NOKEY'
+ comment = 'This is eBZ DD3 meter with a wMB-E01 wmbus radio converter module.'
+ telegram = 4B445A146450000002378C20F3900F002C25F38B00006CBB07CEB215901172635213015A140102F3002007102F2F_04039D3E03000429E82201000DFD110E333632353331313031305A424531
+ json = '{"_":"telegram","media":"electricity","driver":"ebzwmbe","name":"MyEl","id":"01135263","total_energy_consumption_kwh":212.637,"customer":"1EBZ0101135263","current_power_consumption_kw":null,"timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'MyEl;01135263;212.637;null;null;null;null;1111-11-11 11:11.11'
+ }
+ }
+}
diff --git a/components/wmbus_common/drivers/src/ehzp.xmq b/components/wmbus_common/drivers/src/ehzp.xmq
new file mode 100644
index 00000000..f57d0673
--- /dev/null
+++ b/components/wmbus_common/drivers/src/ehzp.xmq
@@ -0,0 +1,159 @@
+// Copyright (C) 2020-2022 Fredrik Öhrström (gpl-3.0-or-later)
+driver {
+ name = ehzp
+ meter_type = ElectricityMeter
+ default_fields = name,id,total_energy_consumption_kwh,current_power_consumption_kw,total_energy_production_kwh,timestamp
+ detect {
+ mvt = EMH,02,02
+ mvt = EMH,00,02
+ }
+ library {
+ use = on_time_h
+ }
+ fields {
+ field {
+ name = status
+ quantity = Text
+ info = 'Meter status. Includes both meter error field and tpl status field.'
+ attributes = STATUS,INCLUDE_TPL_STATUS
+ match {
+ measurement_type = Instantaneous
+ difvifkey = 01FD971D
+ }
+ lookup {
+ name = ERROR_FLAGS
+ map_type = BitToString
+ mask_bits = 0xff
+ default_message = OK
+ }
+ }
+ field {
+ name = total_energy_consumption
+ quantity = Energy
+ info = 'The total energy consumption recorded by this meter.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyEnergyVIF
+ }
+ }
+ field {
+ name = current_power_consumption
+ quantity = Power
+ info = 'Current power consumption.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyPowerVIF
+ }
+ }
+ field {
+ name = total_energy_production
+ quantity = Energy
+ info = 'The total energy production recorded by this meter.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyEnergyVIF
+ add_combinable = BackwardFlow
+ }
+ }
+ field {
+ name = total_energy_consumption_at_1
+ quantity = Energy
+ info = 'The base total energy consumption recorded by this meter.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyEnergyVIF
+ storage_nr = 3
+ // This storage 3 is the base for the following compact values.
+ }
+ }
+ field {
+ name = 'total_energy_consumption_at_{storage_counter-2counter}'
+ quantity = Energy
+ info = 'The total energy consumption at given point in time.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyEnergyVIF
+ add_combinable = Synthetic
+ storage_nr = 4,10
+ /* This storage 4 follows 3 and is marked synthetic, wmbusmeters will look
+ for a compact profile to fill these values. */
+ }
+ }
+ field {
+ name = total_energy_production_at_1
+ quantity = Energy
+ info = 'The total energy production at previous point in time.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyEnergyVIF
+ add_combinable = BackwardFlow
+ storage_nr = 3
+ // This storage 3 is the base for the following compact values.
+ }
+ }
+ field {
+ name = 'total_energy_production_at_{storage_counter-2counter}'
+ quantity = Energy
+ info = 'The total energy production at previous point in time.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyEnergyVIF
+ add_combinable = Synthetic
+ add_combinable = BackwardFlow
+ storage_nr = 4,10
+ /* This storage 4 follows 3 and is marked synthetic, wmbusmeters will look
+ for a compact profile to fill these values, and match BackwardFlow. */
+ }
+ }
+ field {
+ name = actuality_duration_at_1
+ quantity = Time
+ info = 'The actuality duration at the previous point in time.'
+ display_unit = s
+ match {
+ measurement_type = Instantaneous
+ vif_range = ActualityDuration
+ storage_nr = 3
+ }
+ }
+ field {
+ name = 'actuality_duration_at_{storage_counter-2counter}'
+ quantity = Time
+ info = 'How long before the transmission this point of the compact profile was measured.'
+ display_unit = s
+ match {
+ measurement_type = Instantaneous
+ vif_range = ActualityDuration
+ add_combinable = Synthetic
+ storage_nr = 4,10
+ /* The telegram carries no base time. wmbusmeters derives the actuality duration of
+ each profile point from the one of storage 3 plus the spacing of the profile. */
+ }
+ }
+ field {
+ name = on_time_at_1
+ quantity = Time
+ info = 'The on time at the previous point in time.'
+ display_unit = h
+ match {
+ measurement_type = Instantaneous
+ vif_range = OnTime
+ storage_nr = 3
+ }
+ }
+ }
+ tests {
+ test {
+ args = 'Elen3 ehzp 55995599 NOKEY'
+ telegram = 5344A8159955995502028C201D900F002C250C390000ED176BBBB1591ADB7A1D003007102F2F_0700583B74020000000007803CBCD70200000000000728B070200000000000042092A406002F2F2F2F2F2F2F2F2F
+ json = '{"_":"telegram","media":"electricity","driver":"ehzp","name":"Elen3","id":"55995599","status":"OK","on_time_h":120.929444,"total_energy_consumption_kwh":41.1718,"current_power_consumption_kw":2.126,"total_energy_production_kwh":0.1863,"timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'Elen3;55995599;41.1718;2.126;0.1863;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'Elen4 ehzp 13711234 NOKEY'
+ telegram = AE44a8153412711300027a010000002F2F0700285EB5050000000007803C34080000000000000728905F01000000000004208F84160101FD971D00C1017402C401208D841601C701031C76010000000000CD01831324410800010000000001000000000100000001000000000100000000010000000001000000C701833C0200000000000000CD0183933C244108000000000000000000000000000000000000000000000000000000000000000000
+ json = '{"_": "telegram","actuality_duration_at_1_s": 2,"actuality_duration_at_2_s": 10,"actuality_duration_at_3_s": 18,"actuality_duration_at_4_s": 26,"actuality_duration_at_5_s": 34,"actuality_duration_at_6_s": 42,"actuality_duration_at_7_s": 50,"actuality_duration_at_8_s": 58,"current_power_consumption_kw": 0.09,"driver": "ehzp","id": "13711234","media": "electricity","name": "Elen4","on_time_at_1_h": 5070.261389,"on_time_h": 5070.261944,"status": "OK","timestamp": "1111-11-11T11:11:11Z","total_energy_consumption_at_1_kwh": 95.772,"total_energy_consumption_at_2_kwh": 95.772,"total_energy_consumption_at_3_kwh": 95.771,"total_energy_consumption_at_4_kwh": 95.771,"total_energy_consumption_at_5_kwh": 95.771,"total_energy_consumption_at_6_kwh": 95.771,"total_energy_consumption_at_7_kwh": 95.771,"total_energy_consumption_at_8_kwh": 95.77,"total_energy_consumption_kwh": 95.7722,"total_energy_production_at_1_kwh": 0.002,"total_energy_production_at_2_kwh": 0.002,"total_energy_production_at_3_kwh": 0.002,"total_energy_production_at_4_kwh": 0.002,"total_energy_production_at_5_kwh": 0.002,"total_energy_production_at_6_kwh": 0.002,"total_energy_production_at_7_kwh": 0.002,"total_energy_production_at_8_kwh": 0.002,"total_energy_production_kwh": 0.0021}'
+ fields = 'Elen4;13711234;95.7722;0.09;0.0021;1111-11-11 11:11.11'
+ }
+ }
+}
diff --git a/components/wmbus_common/drivers/src/ei6500.xmq b/components/wmbus_common/drivers/src/ei6500.xmq
new file mode 100644
index 00000000..fd55562c
--- /dev/null
+++ b/components/wmbus_common/drivers/src/ei6500.xmq
@@ -0,0 +1,552 @@
+// Copyright (C) 2020-2026 Fredrik Öhrström (gpl-3.0-or-later)
+driver {
+ name = ei6500
+ meter_type = SmokeDetector
+ default_fields = name,id,status,last_alarm_date,alarm_counter,timestamp
+ detect {
+ mvt = EIE,0c,1a
+ }
+ library {
+ use = software_version
+ }
+ mfct_tpl_status_bits {
+ mask_bits = 0xe0
+ default_message = OK
+ map {
+ name = RTC_INVALID
+ value = 0x40
+ test = set
+ }
+ }
+ fields {
+ field {
+ name = status
+ quantity = Text
+ info = 'Meter error flags. IMPORTANT! Smoke alarm is NOT reported here! You MUST check last alarm date and counter!'
+ attributes = STATUS,INCLUDE_TPL_STATUS
+ default_message = OK
+ match {
+ measurement_type = Instantaneous
+ vif_range = ErrorFlags
+ }
+ lookup {
+ name = ERROR_FLAGS
+ map_type = BitToString
+ mask_bits = 0xffff
+ default_message = OK
+ /* bits 0-7: not used (lower byte)
+ bit 8 (0x0100): not used
+ bit 9 (0x0200): application error, unknown field C
+ bit 10 (0x0400): application error, unknown field CI
+ bit 11 (0x0800): application error, unknown record
+ bit 12 (0x1000): application error, access right
+ bit 13 (0x2000): application error, record size
+ bit 14 (0x4000): application error, record value
+ bit 15 (0x8000): application error, bad password */
+ map {
+ name = UNKNOWN_FIELD_C
+ value = 0x0200
+ test = Set
+ }
+ map {
+ name = UNKNOWN_FIELD_CI
+ value = 0x0400
+ test = Set
+ }
+ map {
+ name = UNKNOWN_RECORD
+ value = 0x0800
+ test = Set
+ }
+ map {
+ name = ACCESS_RIGHT
+ value = 0x1000
+ test = Set
+ }
+ map {
+ name = RECORD_SIZE
+ value = 0x2000
+ test = Set
+ }
+ map {
+ name = RECORD_VALUE
+ value = 0x4000
+ test = Set
+ }
+ map {
+ name = BAD_PASSWORD
+ value = 0x8000
+ test = Set
+ }
+ }
+ }
+ field {
+ name = last_alarm
+ quantity = PointInTime
+ info = 'Date when the smoke alarm last triggered.'
+ display_unit = DateLT
+ match {
+ measurement_type = Instantaneous
+ vif_range = Date
+ tariff_nr = 1
+ subunit_nr = 1
+ }
+ }
+ field {
+ name = alarm
+ quantity = Dimensionless
+ info = 'Number of times the smoke alarm has triggered.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = CumulationCounter
+ tariff_nr = 1
+ subunit_nr = 1
+ }
+ }
+ field {
+ name = message
+ quantity = PointInTime
+ info = 'Device date time.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = DateTime
+ }
+ }
+ field {
+ name = duration_removed
+ quantity = Time
+ info = 'Time the smoke alarm has been removed.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = DurationOfTariff
+ tariff_nr = 2
+ subunit_nr = 1
+ }
+ }
+ field {
+ name = last_remove
+ quantity = PointInTime
+ info = 'Date when the smoke alarm was last removed.'
+ display_unit = DateLT
+ match {
+ measurement_type = Instantaneous
+ vif_range = Date
+ tariff_nr = 2
+ subunit_nr = 1
+ }
+ }
+ field {
+ name = removed
+ quantity = Dimensionless
+ info = 'Number of times the smoke alarm has been removed.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = CumulationCounter
+ subunit_nr = 1
+ tariff_nr = 2
+ }
+ }
+ field {
+ name = test_button_last
+ quantity = PointInTime
+ info = 'Date when test button was last pressed.'
+ display_unit = DateLT
+ match {
+ measurement_type = Instantaneous
+ vif_range = Date
+ subunit_nr = 1
+ tariff_nr = 3
+ }
+ }
+ field {
+ name = test_button
+ quantity = Dimensionless
+ info = 'Number of times the test button has been pressed.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = CumulationCounter
+ subunit_nr = 1
+ tariff_nr = 3
+ }
+ }
+ field {
+ name = installation
+ quantity = PointInTime
+ info = 'Date when the smoke alarm was installed.'
+ display_unit = DateLT
+ match {
+ measurement_type = Instantaneous
+ vif_range = Date
+ tariff_nr = 2
+ }
+ }
+ field {
+ name = last_sound_check
+ quantity = PointInTime
+ info = 'Date when the smoke alarm last checked the piezo speaker.'
+ display_unit = DateLT
+ match {
+ measurement_type = Instantaneous
+ vif_range = Date
+ storage_nr = 1
+ }
+ }
+ field {
+ name = ei_head_raw
+ quantity = Dimensionless
+ attributes = HIDE
+ vif_scaling = None
+ dif_signedness = Unsigned
+ match {
+ difvifkey = 8440FF2C
+ }
+ }
+ field {
+ name = dust_level
+ quantity = Text
+ deprecated_by = 'dust_level_counter, update before 2026-12-01'
+ info = 'Dust level 0 (best) to 31 (worst). Deprecated. Use dust_level_counter instead.'
+ match {
+ difvifkey = 8440FF2C
+ }
+ lookup {
+ name = DUST
+ map_type = IndexToString
+ mask_bits = 0x1f
+ // We use the default mapping of bits to DUST_1 DUST_2 DUST_3 etc, which happens since there are no map rules here.
+ }
+ }
+ field {
+ name = dust_level
+ quantity = Dimensionless
+ info = 'Dust level 0 (best) to 31 (worst). Recommend cleaning when level exceeds 28.'
+ calculate = 'ei_head_raw_counter & 31counter'
+ }
+ field {
+ name = dust_status
+ quantity = Text
+ info = 'Status of smoke detector sensors, merged into the status field.'
+ attributes = INJECT_INTO_STATUS,HIDE
+ match {
+ difvifkey = 8440FF2C
+ }
+ lookup {
+ name = DUST_STATUS
+ map_type = IndexToString
+ mask_bits = 0x0000001c
+ default_message = ''
+ map {
+ name = ''
+ value = 0x00000000
+ test = Set
+ }
+ map {
+ name = ''
+ value = 0x00000004
+ test = Set
+ }
+ map {
+ name = ''
+ value = 0x00000008
+ test = Set
+ }
+ map {
+ name = ''
+ value = 0x0000000c
+ test = Set
+ }
+ map {
+ name = ''
+ value = 0x00000010
+ test = Set
+ }
+ map {
+ name = ''
+ value = 0x00000014
+ test = Set
+ }
+ map {
+ name = ''
+ value = 0x00000018
+ test = Set
+ }
+ map {
+ name = CLEANING_RECOMMENDED
+ value = 0x0000001c
+ test = Set
+ }
+ }
+ }
+ field {
+ name = battery_level
+ quantity = Text
+ info = 'Battery voltage level.'
+ match {
+ difvifkey = 8440FF2C
+ }
+ lookup {
+ name = BATTERY_VOLTAGE
+ map_type = IndexToString
+ mask_bits = 0x0f00
+ default_message = 2.25V
+ map {
+ name = 2.25V
+ value = 0x0000
+ test = Set
+ }
+ map {
+ name = 2.30V
+ value = 0x0100
+ test = Set
+ }
+ map {
+ name = 2.35V
+ value = 0x0200
+ test = Set
+ }
+ map {
+ name = 2.40V
+ value = 0x0300
+ test = Set
+ }
+ map {
+ name = 2.45V
+ value = 0x0400
+ test = Set
+ }
+ map {
+ name = 2.50V
+ value = 0x0500
+ test = Set
+ }
+ map {
+ name = 2.55V
+ value = 0x0600
+ test = Set
+ }
+ map {
+ name = 2.60V
+ value = 0x0700
+ test = Set
+ }
+ map {
+ name = 2.65V
+ value = 0x0800
+ test = Set
+ }
+ map {
+ name = 2.70V
+ value = 0x0900
+ test = Set
+ }
+ map {
+ name = 2.75V
+ value = 0x0a00
+ test = Set
+ }
+ map {
+ name = 2.80V
+ value = 0x0b00
+ test = Set
+ }
+ map {
+ name = 2.85V
+ value = 0x0c00
+ test = Set
+ }
+ map {
+ name = 2.90V
+ value = 0x0d00
+ test = Set
+ }
+ map {
+ name = 2.95V
+ value = 0x0e00
+ test = Set
+ }
+ map {
+ name = 3.00V
+ value = 0x0f00
+ test = Set
+ }
+ }
+ }
+ field {
+ name = obstacle_distance
+ quantity = Text
+ info = 'The distance to a detected obstacle.'
+ match {
+ difvifkey = 8440FF2C
+ }
+ lookup {
+ name = OBSTACLE_DISTANCE
+ map_type = IndexToString
+ mask_bits = 0x700000
+ default_message = ''
+ map {
+ name = SEODS_NOT_COMPLETED
+ value = 0x000000
+ test = Set
+ }
+ map {
+ name = ''
+ value = 0x100000
+ test = Set
+ }
+ map {
+ name = 45_TO_60_CM
+ value = 0x200000
+ test = Set
+ }
+ map {
+ name = 38_TO_53_CM
+ value = 0x300000
+ test = Set
+ }
+ map {
+ name = 33_TO_48_CM
+ value = 0x400000
+ test = Set
+ }
+ map {
+ name = 28_TO_40_CM
+ value = 0x500000
+ test = Set
+ }
+ map {
+ name = 20_TO_33_CM
+ value = 0x600000
+ test = Set
+ }
+ map {
+ name = 0_TO_25_CM
+ value = 0x700000
+ test = Set
+ }
+ }
+ }
+ field {
+ name = head_status
+ quantity = Text
+ info = 'Status of smoke detector sensors, merged into the status field.'
+ attributes = INJECT_INTO_STATUS,HIDE
+ match {
+ difvifkey = 8440FF2C
+ }
+ lookup {
+ name = HEAD_STATUS
+ map_type = BitToString
+ mask_bits = 0xff8ff0e0
+ // 0x00000000-0x000001f dust level
+ map {
+ name = SOUNDER_FAULT
+ value = 0x00000020
+ test = Set
+ }
+ map {
+ name = TAMPER_WHILE_REMOVED
+ value = 0x00000040
+ test = Set
+ }
+ map {
+ name = EOL_REACHED
+ value = 0x00000080
+ test = Set
+ }
+ // 0x00000100-0x000f00 battery level
+ map {
+ name = LOW_BATTERY_FAULT
+ value = 0x00001000
+ test = Set
+ }
+ map {
+ name = ALARM_SENSOR_FAULT
+ value = 0x00002000
+ test = Set
+ }
+ map {
+ name = OBSTACLE_DETECTOR_FAULT
+ value = 0x00004000
+ test = Set
+ }
+ map {
+ name = EOL_WITHIN_12_MONTH
+ value = 0x00008000
+ test = Set
+ }
+ map {
+ name = SEODS_NOT_YET_COMPLETED
+ value = 0x00010000
+ test = NotSet
+ }
+ map {
+ name = ENV_CHANGED_SINCE_INSTALLATION
+ value = 0x00020000
+ test = Set
+ }
+ map {
+ name = COMM_TO_HEAD_FAULT
+ value = 0x00040000
+ test = Set
+ }
+ map {
+ name = INTERFERENCE_PREVENTING_OBSTACLE_DETECTION
+ value = 0x00080000
+ test = Set
+ }
+ // 0x00100000-0x0700000 distance
+ // 0x00800000 reserved
+ map {
+ name = OBSTACLE_DETECTED
+ value = 0x01000000
+ test = Set
+ }
+ map {
+ name = SMOKE_DETECTOR_FULLY_COVERED
+ value = 0x02000000
+ test = Set
+ }
+ }
+ }
+ }
+ tests {
+ test {
+ args = 'Smokey1 ei6500 01097274 NOKEY'
+ telegram = 58442515747209010C1A7A8B0000000BFD0F070101046D2A06D82502FD17000082206CD825426CD0238440FF2C000F11008250FD61000082506C01018260FD6100008360FD3100000082606C01018270FD61000082706C0101
+ json = '{"_":"telegram","media":"smoke detector","driver":"ei6500","name":"Smokey1","id":"01097274","status":"OK","last_alarm_date":"2000-01-01","alarm_counter":0,"software_version":"010107","message_datetime":"2022-05-24 06:42","duration_removed_h":0,"last_remove_date":"2000-01-01","removed_counter":0,"test_button_last_date":"2000-01-01","test_button_counter":0,"installation_date":"2022-05-24","last_sound_check_date":"2022-03-16","dust_level": "DUST_0","dust_level_deprecated_by": "dust_level_counter, update before 2026-12-01","dust_level_counter":0,"battery_level":"3.00V","obstacle_distance":"","timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'Smokey1;01097274;OK;2000-01-01;0;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'Smokey2 ei6500 01097274 NOKEY'
+ telegram = 58442515747209010C1A7A8D0000000BFD0F070101046D2E06D82502FD17000082206CD825426CD0238440FF2C000F11008250FD61000082506C01018260FD6100008360FD3100000082606C01018270FD61020082706CD825
+ json = '{"_":"telegram","media":"smoke detector","driver":"ei6500","name":"Smokey2","id":"01097274","status":"OK","last_alarm_date":"2000-01-01","alarm_counter":0,"software_version":"010107","message_datetime":"2022-05-24 06:46","duration_removed_h":0,"last_remove_date":"2000-01-01","removed_counter":0,"test_button_last_date":"2022-05-24","test_button_counter":2,"installation_date":"2022-05-24","last_sound_check_date":"2022-03-16","dust_level": "DUST_0","dust_level_deprecated_by": "dust_level_counter, update before 2026-12-01","dust_level_counter":0,"battery_level":"3.00V","obstacle_distance":"","timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'Smokey2;01097274;OK;2000-01-01;0;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'Smokey3 ei6500 01097274 NOKEY'
+ telegram = 58442515747209010C1A7A900000000BFD0F070101046D3406D82502FD17000082206CD825426CD0238440FF2C020F11008250FD61010082506CD8258260FD6100008360FD3100000082606C01018270FD61020082706CD825
+ json = '{"_":"telegram","media":"smoke detector","driver":"ei6500","name":"Smokey3","id":"01097274","status":"OK","last_alarm_date":"2022-05-24","alarm_counter":1,"software_version":"010107","message_datetime":"2022-05-24 06:52","duration_removed_h":0,"last_remove_date":"2000-01-01","removed_counter":0,"test_button_last_date":"2022-05-24","test_button_counter":2,"installation_date":"2022-05-24","last_sound_check_date":"2022-03-16","dust_level": "DUST_2","dust_level_deprecated_by": "dust_level_counter, update before 2026-12-01","dust_level_counter":2,"battery_level":"3.00V","obstacle_distance":"","timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'Smokey3;01097274;OK;2022-05-24;1;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'Smokey4 ei6500 01097274 NOKEY'
+ telegram = 58442515747209010C1A7A940000000BFD0F070101046D0007D82502FD17000082206CD825426CD0238440FF2C420F11008250FD61010082506CD8258260FD6101008360FD3101000082606CD8258270FD61020082706CD825
+ json = '{"_":"telegram","media":"smoke detector","driver":"ei6500","name":"Smokey4","id":"01097274","status":"TAMPER_WHILE_REMOVED","last_alarm_date":"2022-05-24","alarm_counter":1,"software_version":"010107","message_datetime":"2022-05-24 07:00","duration_removed_h":0.016667,"last_remove_date":"2022-05-24","removed_counter":1,"test_button_last_date":"2022-05-24","test_button_counter":2,"installation_date":"2022-05-24","last_sound_check_date":"2022-03-16","dust_level": "DUST_2","dust_level_deprecated_by": "dust_level_counter, update before 2026-12-01","dust_level_counter":2,"battery_level":"3.00V","obstacle_distance":"","timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'Smokey4;01097274;TAMPER_WHILE_REMOVED;2022-05-24;1;1111-11-11 11:11.11'
+ }
+ test {
+ comment = 'Synthetic telegram: dust level 27, no cleaning recommendation yet.'
+ args = 'Smokey5 ei6500 01097274 NOKEY'
+ telegram = 58442515747209010C1A7A8B0000000BFD0F070101046D2A06D82502FD17000082206CD825426CD0238440FF2C1b0F11008250FD61000082506C01018260FD6100008360FD3100000082606C01018270FD61000082706C0101
+ json = '{"_":"telegram","media":"smoke detector","driver":"ei6500","name":"Smokey5","id":"01097274","status":"OK","last_alarm_date":"2000-01-01","alarm_counter":0,"software_version":"010107","message_datetime":"2022-05-24 06:42","duration_removed_h":0,"last_remove_date":"2000-01-01","removed_counter":0,"test_button_last_date":"2000-01-01","test_button_counter":0,"installation_date":"2022-05-24","last_sound_check_date":"2022-03-16","dust_level": "DUST_1B","dust_level_deprecated_by": "dust_level_counter, update before 2026-12-01","dust_level_counter":27,"battery_level":"3.00V","obstacle_distance":"","timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'Smokey5;01097274;OK;2000-01-01;0;1111-11-11 11:11.11'
+ }
+ test {
+ comment = 'Synthetic telegram: dust level 28, cleaning is recommended.'
+ args = 'Smokey6 ei6500 01097274 NOKEY'
+ telegram = 58442515747209010C1A7A8B0000000BFD0F070101046D2A06D82502FD17000082206CD825426CD0238440FF2C1c0F11008250FD61000082506C01018260FD6100008360FD3100000082606C01018270FD61000082706C0101
+ json = '{"_":"telegram","media":"smoke detector","driver":"ei6500","name":"Smokey6","id":"01097274","status":"CLEANING_RECOMMENDED","last_alarm_date":"2000-01-01","alarm_counter":0,"software_version":"010107","message_datetime":"2022-05-24 06:42","duration_removed_h":0,"last_remove_date":"2000-01-01","removed_counter":0,"test_button_last_date":"2000-01-01","test_button_counter":0,"installation_date":"2022-05-24","last_sound_check_date":"2022-03-16","dust_level": "DUST_1C","dust_level_deprecated_by": "dust_level_counter, update before 2026-12-01","dust_level_counter":28,"battery_level":"3.00V","obstacle_distance":"","timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'Smokey6;01097274;CLEANING_RECOMMENDED;2000-01-01;0;1111-11-11 11:11.11'
+ }
+ }
+}
diff --git a/components/wmbus_common/drivers/src/elf.xmq b/components/wmbus_common/drivers/src/elf.xmq
new file mode 100644
index 00000000..c8fca099
--- /dev/null
+++ b/components/wmbus_common/drivers/src/elf.xmq
@@ -0,0 +1,120 @@
+// Copyright (C) 2021-2022 Fredrik Öhrström (gpl-3.0-or-later)
+driver {
+ name = elf
+ meter_type = HeatMeter
+ default_fields = name,id,total_energy_consumption_kwh,current_power_consumption_kw,total_volume_m3,flow_temperature_c,return_temperature_c,external_temperature_c,status,timestamp
+ detect {
+ mvt = APA,40,04
+ }
+ library {
+ use = operating_time_h
+ use = meter_date
+ }
+ fields {
+ field {
+ name = status
+ quantity = Text
+ info = 'Meter status from manufacturer status and tpl status field.'
+ attributes = STATUS,INCLUDE_TPL_STATUS
+ match {
+ difvifkey = 047F
+ }
+ lookup {
+ name = ERROR_FLAGS
+ map_type = BitToString
+ mask_bits = 0xffffffff
+ default_message = OK
+ }
+ }
+ field {
+ name = total_energy_consumption
+ quantity = Energy
+ info = 'The total energy consumption recorded by this meter.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyEnergyVIF
+ }
+ }
+ field {
+ name = current_power_consumption
+ quantity = Power
+ info = 'Current power consumption.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = PowerW
+ }
+ }
+ field {
+ name = total_volume
+ quantity = Volume
+ info = 'Total volume of heat media.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = Volume
+ }
+ }
+ field {
+ name = total_energy_consumption_at_date
+ quantity = Energy
+ info = 'The total energy consumption recorded at the target date.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyEnergyVIF
+ storage_nr = 1
+ }
+ }
+ field {
+ name = flow_temperature
+ quantity = Temperature
+ info = 'The flow temperature.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = FlowTemperature
+ }
+ }
+ field {
+ name = return_temperature
+ quantity = Temperature
+ info = 'The return temperature.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = ReturnTemperature
+ }
+ }
+ field {
+ name = external_temperature
+ quantity = Temperature
+ info = 'The external temperature.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = ExternalTemperature
+ }
+ }
+ field {
+ name = version
+ quantity = Text
+ info = Version.
+ match {
+ measurement_type = Instantaneous
+ vif_range = ModelVersion
+ }
+ }
+ field {
+ name = battery
+ quantity = Voltage
+ info = 'Battery voltage.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = Voltage
+ }
+ }
+ }
+ tests {
+ test {
+ args = 'Hetta elf 01885619 NOKEY'
+ telegram = 51440186010905001837721956880101064004DA000020026CA9220E017799241103000C13641320000A2D00000A5A90060A5E800544050E77000001FD0C010A6564370AFD4731030A274907047F00000002
+ json = '{"_":"telegram","media":"heat","driver":"elf","name":"Hetta","id":"01885619","status":"ERROR_FLAGS_2000000","meter_date":"2021-02-09","total_energy_consumption_kwh":3112.49977,"current_power_consumption_kw":0,"total_volume_m3":201.364,"total_energy_consumption_at_date_kwh":3047.8,"flow_temperature_c":69,"return_temperature_c":58,"external_temperature_c":37.64,"operating_time_h":17976,"version":"01","battery_v":3.31,"timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'Hetta;01885619;3112.49977;0;201.364;69;58;37.64;ERROR_FLAGS_2000000;1111-11-11 11:11.11'
+ }
+ }
+}
diff --git a/components/wmbus_common/drivers/src/elf2.xmq b/components/wmbus_common/drivers/src/elf2.xmq
index 1c099003..99c754c7 100644
--- a/components/wmbus_common/drivers/src/elf2.xmq
+++ b/components/wmbus_common/drivers/src/elf2.xmq
@@ -85,7 +85,7 @@ driver {
match {
measurement_type = Instantaneous
vif_range = AnyVolumeVIF
- subunit_nr = 1
+ subunit_nr = 1
tariff_nr = 0
storage_nr = 0
}
@@ -97,7 +97,7 @@ driver {
match {
measurement_type = Instantaneous
vif_range = AnyVolumeVIF
- subunit_nr = 0
+ subunit_nr = 0
tariff_nr = 1
storage_nr = 0
}
@@ -109,7 +109,7 @@ driver {
match {
measurement_type = Instantaneous
vif_range = AnyVolumeVIF
- subunit_nr = 0
+ subunit_nr = 0
tariff_nr = 2
storage_nr = 0
}
@@ -130,7 +130,7 @@ driver {
match {
measurement_type = Instantaneous
vif_range = AnyEnergyVIF
- subunit_nr = 1
+ subunit_nr = 1
tariff_nr = 0
storage_nr = 0
}
@@ -142,7 +142,7 @@ driver {
match {
measurement_type = Instantaneous
vif_range = AnyEnergyVIF
- storage_nr = 1
+ storage_nr = 1
}
}
field {
@@ -152,7 +152,7 @@ driver {
match {
measurement_type = Instantaneous
vif_range = AnyEnergyVIF
- subunit_nr = 1
+ subunit_nr = 1
tariff_nr = 0
storage_nr = 1
}
@@ -279,13 +279,13 @@ driver {
test {
args = 'Heato elf2 23170428 NOKEY'
telegram = 684E4E68080172280417230106420455000000_0C7828041723026C103902FD1700000E0A6342693401000C13634630000A3B00000A2D00000A5A49020A5E44020C22762101003C2215000000047E17090000_4116
- json = '{"_":"telegram","current_power_kw": 0,"current_volume_flow_m3h": 0,"fabrication_no": "23170428","id": "23170428","media": "heat","meter": "elf2","meter_date": "2024-09-16","name": "Heato","on_time_at_error_h": 15,"on_time_h": 12176,"status": "OK","t1_temperature_c": 24.9,"t2_temperature_c": 24.4,"timestamp": "1111-11-11T11:11:11Z","total_energy_kwh": 3741.507306,"total_volume_m3": 304.663}'
+ json = '{"_":"telegram","current_power_kw": 0,"current_volume_flow_m3h": 0,"fabrication_no": "23170428","id": "23170428","media": "heat","driver": "elf2","meter_date": "2024-09-16","name": "Heato","on_time_at_error_h": 15,"on_time_h": 12176,"status": "OK","t1_temperature_c": 24.9,"t2_temperature_c": 24.4,"timestamp": "1111-11-11T11:11:11Z","total_energy_kwh": 3741.507306,"total_volume_m3": 304.663}'
fields = 'Heato;23170428;OK;3741.507306;1111-11-11 11:11.11'
}
test {
args = 'Heato elf2 24271170 ACA5769E7902B8A770A7118C11D5F0F6'
telegram = 6E44010670112724420D7A35006025B2F96D0306197B34C7E4E6F45F434FCB78BCBBE8837A2C253BEC3E958B3BBBFBE6AE465214941AF544237DB8A69E6C4CE8DFB7A1F64860200BB2E995F1A4F501E7702E0A581576F60B58CBDA39E1F150F29E04D689C38FD6C430ABD05DD789E0
- json = '{"_":"telegram","media":"heat/cooling load","meter":"elf2","name":"Heato","id":"24271170","current_power_kw":0,"current_volume_flow_m3h":0,"input1_volume_m3":0.002,"input2_volume_m3":0.002,"meter_datetime":"2025-10-15 14:39","t1_temperature_c":22.5,"t2_temperature_c":22.6,"total_energy_kwh":144,"total_energy_cooling_kwh":1,"total_energy_cooling_period_kwh":1,"total_energy_period_kwh":72,"total_volume_m3":17.856,"total_volume_cooling_m3":1.576,"status":"OK","timestamp":"1111-11-11T11:11:11Z"}'
+ json = '{"_":"telegram","media":"heat/cooling load","driver":"elf2","name":"Heato","id":"24271170","current_power_kw":0,"current_volume_flow_m3h":0,"input1_volume_m3":0.002,"input2_volume_m3":0.002,"meter_datetime":"2025-10-15 14:39","t1_temperature_c":22.5,"t2_temperature_c":22.6,"total_energy_kwh":144,"total_energy_cooling_kwh":1,"total_energy_cooling_period_kwh":1,"total_energy_period_kwh":72,"total_volume_m3":17.856,"total_volume_cooling_m3":1.576,"status":"OK","timestamp":"1111-11-11T11:11:11Z"}'
fields = 'Heato;24271170;OK;144;1111-11-11 11:11.11'
}
}
diff --git a/components/wmbus_common/drivers/src/elster.xmq b/components/wmbus_common/drivers/src/elster.xmq
index 64f50eda..72308f3a 100644
--- a/components/wmbus_common/drivers/src/elster.xmq
+++ b/components/wmbus_common/drivers/src/elster.xmq
@@ -7,24 +7,14 @@ driver {
mvt = ELS,81,03
}
library {
+ use = total_m3
use = actuality_duration_s
}
- fields {
- field {
- name = total
- quantity = Volume
- info = 'The total water consumption.'
- match {
- measurement_type = Instantaneous
- vif_range = Volume
- }
- }
- }
tests {
test {
args = 'Gas elster 05105025 NOKEY'
telegram = 3644A511640010253837722550100593158103E70020052F2F_0374E602000C137034220302FD74EE0F2F2F2F2F2F2F2F2F2F2F2F2F2F2F
- json = '{"_":"telegram","media":"gas","meter":"elster","name":"Gas","id":"05105025","actuality_duration_s":742,"total_m3":3223.47,"timestamp":"1111-11-11T11:11:11Z"}'
+ json = '{"_":"telegram","media":"gas","driver":"elster","name":"Gas","id":"05105025","actuality_duration_s":742,"total_m3":3223.47,"timestamp":"1111-11-11T11:11:11Z"}'
fields = 'Gas;05105025;3223.47;1111-11-11 11:11.11'
}
}
diff --git a/components/wmbus_common/drivers/src/eltako.xmq b/components/wmbus_common/drivers/src/eltako.xmq
index add45d99..f0556063 100644
--- a/components/wmbus_common/drivers/src/eltako.xmq
+++ b/components/wmbus_common/drivers/src/eltako.xmq
@@ -273,7 +273,7 @@ driver {
test {
args = 'Eltako eltako 23451236 NOKEY'
telegram = 689292680800723612452394150102cd0000008c1004688902008c1104688902008c2004000000008c21040000000002fdc9ff01ee0002fddBff01100002acff0120008240acff010a0002fdc9ff02ef0002fddBff02110002acff0224008240acff02070002fdc9ff03ee0002fddBff030e0002acff031c008240acff03060002ff68010002acff0062008240acff00190001ff1300f416
- json = '{"_":"telegram","active_power_overall_kw": 0.98,"active_tariff_counter": 0,"ct_numerator_counter": 1,"current_l1_a": 1.6,"current_l2_a": 1.7,"current_l3_a": 1.4,"id": "23451236","media": "electricity","meter": "eltako","name": "Eltako","power_l1_kw": 0.32,"power_l2_kw": 0.36,"power_l3_kw": 0.28,"reactive_power_l1_kw": 0.1,"reactive_power_l2_kw": 0.07,"reactive_power_l3_kw": 0.06,"reactive_power_overall_kw": 0.25,"timestamp": "1111-11-11T11:11:11Z","total_kwh": 289.68,"total_reactive_tariff_1_kwh": 289.68,"total_reactive_tariff_2_kwh": 0,"total_tariff_1_kwh": 289.68,"total_tariff_2_kwh": 0,"voltage_l1_n_v": 238,"voltage_l2_n_v": 239,"voltage_l3_n_v": 238}'
+ json = '{"_":"telegram","active_power_overall_kw": 0.98,"active_tariff_counter": 0,"ct_numerator_counter": 1,"current_l1_a": 1.6,"current_l2_a": 1.7,"current_l3_a": 1.4,"id": "23451236","media": "electricity","driver": "eltako","name": "Eltako","power_l1_kw": 0.32,"power_l2_kw": 0.36,"power_l3_kw": 0.28,"reactive_power_l1_kw": 0.1,"reactive_power_l2_kw": 0.07,"reactive_power_l3_kw": 0.06,"reactive_power_overall_kw": 0.25,"timestamp": "1111-11-11T11:11:11Z","total_kwh": 289.68,"total_reactive_tariff_1_kwh": 289.68,"total_reactive_tariff_2_kwh": 0,"total_tariff_1_kwh": 289.68,"total_tariff_2_kwh": 0,"voltage_l1_n_v": 238,"voltage_l2_n_v": 239,"voltage_l3_n_v": 238}'
fields = 'Eltako;23451236;289.68;1111-11-11 11:11.11'
}
}
diff --git a/components/wmbus_common/drivers/src/elvsense.xmq b/components/wmbus_common/drivers/src/elvsense.xmq
index 9fb9f825..d925f15c 100644
--- a/components/wmbus_common/drivers/src/elvsense.xmq
+++ b/components/wmbus_common/drivers/src/elvsense.xmq
@@ -56,10 +56,10 @@ driver {
}
}
field {
- name = co2
- quantity = Dimensionless
- info = 'CO2 concentration ppm for 300W meters'
- vif_scaling = None
+ name = co2
+ quantity = Dimensionless
+ info = 'CO2 concentration ppm for 300W meters'
+ vif_scaling = None
display_unit = ppm
match {
difvifkey = 027C03324F43
@@ -76,20 +76,20 @@ driver {
test {
args = 'ES1 elvsense 80008957 NOKEY'
telegram = 1844961557890080501B7aea00012002656f0802fd46610e0f
- json = '{"_": "telegram","battery_v": 3.681,"id": "80008957","media": "room sensor","meter": "elvsense","name": "ES1","status": "OK","temperature_c": 21.59,"timestamp": "1111-11-11T11:11:11Z"}'
+ json = '{"_": "telegram","battery_v": 3.681,"id": "80008957","media": "room sensor","driver": "elvsense","name": "ES1","status": "OK","temperature_c": 21.59,"timestamp": "1111-11-11T11:11:11Z"}'
fields = 'ES1;80008957;21.59;null;null;3.681;OK;1111-11-11 11:11.11'
}
test {
args = 'ES2 elvsense 80008957 NOKEY'
telegram = 1e44961557890080501B7aff0010252f2f0265680802fd46630e0f2f2f2f2f
- json = '{"_": "telegram","battery_v": 3.683,"id": "80008957","media": "room sensor","meter": "elvsense","name": "ES2","status": "OK","temperature_c": 21.52,"timestamp": "1111-11-11T11:11:11Z"}'
+ json = '{"_": "telegram","battery_v": 3.683,"id": "80008957","media": "room sensor","driver": "elvsense","name": "ES2","status": "OK","temperature_c": 21.52,"timestamp": "1111-11-11T11:11:11Z"}'
fields = 'ES2;80008957;21.52;null;null;3.683;OK;1111-11-11 11:11.11'
}
test {
args = 'FAKE elvsense 80008958 NOKEY'
comment = 'This is a fake meter to test ppm and named vif CO2. But 300W probably looks similar.'
telegram = 2b44961558890080501B7aff0010252f2f_02656808_02fd46630e_02fb1a0700_027c03324f430001_0f2f2f2f2f
- json = '{"_": "telegram","battery_v": 3.683,"co2_ppm": 256,"humidity_rh": 0.7,"id": "80008958","media": "room sensor","meter": "elvsense","name": "FAKE","status": "OK","temperature_c": 21.52,"timestamp": "1111-11-11T11:11:11Z"}'
+ json = '{"_": "telegram","battery_v": 3.683,"co2_ppm": 256,"humidity_rh": 0.7,"id": "80008958","media": "room sensor","driver": "elvsense","name": "FAKE","status": "OK","temperature_c": 21.52,"timestamp": "1111-11-11T11:11:11Z"}'
fields = 'FAKE;80008958;21.52;0.7;256;3.683;OK;1111-11-11 11:11.11'
}
}
diff --git a/components/wmbus_common/drivers/src/em24.xmq b/components/wmbus_common/drivers/src/em24.xmq
new file mode 100644
index 00000000..1603d02f
--- /dev/null
+++ b/components/wmbus_common/drivers/src/em24.xmq
@@ -0,0 +1,280 @@
+// Copyright (C) 2017-2022 Fredrik Öhrström (gpl-3.0-or-later)
+driver {
+ name = em24
+ meter_type = ElectricityMeter
+ default_fields = name,id,total_energy_consumption_kwh,total_energy_production_kwh,total_reactive_energy_consumption_kvarh,total_reactive_energy_production_kvarh,total_apparent_energy_consumption_kvah,total_apparent_energy_production_kvah,timestamp
+ detect {
+ mvt = KAM,33,02
+ mvt = GAV,00,02
+ }
+ compact_frame_formats {
+ difvif = 040504FB827504853C04FB82F53C01FD17
+ }
+ fields {
+ field {
+ name = status
+ quantity = Text
+ info = 'Status of meter.'
+ attributes = STATUS
+ match {
+ measurement_type = Instantaneous
+ vif_range = ErrorFlags
+ }
+ lookup {
+ name = ERROR_FLAGS
+ map_type = BitToString
+ mask_bits = 0xff
+ default_message = OK
+ map {
+ name = V_1_OVERFLOW
+ value = 0x01
+ test = Set
+ }
+ map {
+ name = V_2_OVERFLOW
+ value = 0x02
+ test = Set
+ }
+ map {
+ name = V_2_OVERFLOW
+ value = 0x04
+ test = Set
+ }
+ map {
+ name = I_1_OVERFLOW
+ value = 0x08
+ test = Set
+ }
+ map {
+ name = I_2_OVERFLOW
+ value = 0x10
+ test = Set
+ }
+ map {
+ name = I_3_OVERFLOW
+ value = 0x20
+ test = Set
+ }
+ map {
+ name = FREQUENCY
+ value = 0x40
+ test = Set
+ }
+ }
+ }
+ field {
+ name = error
+ quantity = Text
+ info = 'Any errors currently being reported, this field is deprecated and replaced by the status field.'
+ deprecated_by = 'status, update before 2026-12-01'
+ match {
+ measurement_type = Instantaneous
+ vif_range = ErrorFlags
+ }
+ lookup {
+ name = ERROR_FLAGS
+ map_type = BitToString
+ mask_bits = 0xff
+ default_message = ''
+ map {
+ name = V_1_OVERFLOW
+ value = 0x01
+ test = Set
+ }
+ map {
+ name = V_2_OVERFLOW
+ value = 0x02
+ test = Set
+ }
+ map {
+ name = V_2_OVERFLOW
+ value = 0x04
+ test = Set
+ }
+ map {
+ name = I_1_OVERFLOW
+ value = 0x08
+ test = Set
+ }
+ map {
+ name = I_2_OVERFLOW
+ value = 0x10
+ test = Set
+ }
+ map {
+ name = I_3_OVERFLOW
+ value = 0x20
+ test = Set
+ }
+ map {
+ name = FREQUENCY
+ value = 0x40
+ test = Set
+ }
+ }
+ }
+ field {
+ name = total_energy_consumption
+ quantity = Energy
+ info = 'The total energy consumption recorded by this meter.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyEnergyVIF
+ }
+ }
+ field {
+ name = power
+ quantity = Power
+ info = 'Power measured by this meter at the moment.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyPowerVIF
+ }
+ }
+ field {
+ name = total_energy_production
+ quantity = Energy
+ info = 'The total energy backward (production) recorded by this meter.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyEnergyVIF
+ add_combinable = BackwardFlow
+ }
+ }
+ field {
+ name = total_reactive_energy_consumption
+ quantity = Reactive_Energy
+ info = 'The reactive total energy consumption recorded by this meter.'
+ vif_scaling = None
+ dif_signedness = Signed
+ match {
+ difvifkey = 04FB8275
+ }
+ }
+ field {
+ name = total_reactive_energy_production
+ quantity = Reactive_Energy
+ info = 'The total reactive energy backward (production) recorded by this meter.'
+ vif_scaling = None
+ dif_signedness = Signed
+ match {
+ difvifkey = 04FB82F53C
+ }
+ }
+ field {
+ name = total_apparent_energy_consumption
+ quantity = Apparent_Energy
+ info = 'Calculated: the total apparent energy consumption.'
+ calculate = 'sqrt( (total_energy_consumption_kwh * total_energy_consumption_kwh) + (total_reactive_energy_consumption_kvarh * total_reactive_energy_consumption_kvarh) )'
+ }
+ field {
+ name = total_apparent_energy_production
+ quantity = Apparent_Energy
+ info = 'Calculated: the total apparent energy production.'
+ calculate = 'sqrt( (total_energy_production_kwh * total_energy_production_kwh) + (total_reactive_energy_production_kvarh * total_reactive_energy_production_kvarh) )'
+ }
+ field {
+ name = amperage_at_phase_1
+ quantity = Amperage
+ info = 'Amperage at phase L1.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ difvifkey = 04FDD9FC01
+ }
+ }
+ field {
+ name = amperage_at_phase_2
+ quantity = Amperage
+ info = 'Amperage at phase L2.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ difvifkey = 04FDD9FC02
+ }
+ }
+ field {
+ name = amperage_at_phase_3
+ quantity = Amperage
+ info = 'Amperage at phase L3.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ difvifkey = 04FDD9FC03
+ }
+ }
+ field {
+ name = voltage_at_phase_1
+ quantity = Voltage
+ info = 'Voltage at phase L1.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ difvifkey = 04FDC8FC01
+ }
+ }
+ field {
+ name = voltage_at_phase_2
+ quantity = Voltage
+ info = 'Voltage at phase L2.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ difvifkey = 04FDC8FC02
+ }
+ }
+ field {
+ name = voltage_at_phase_3
+ quantity = Voltage
+ info = 'Voltage at phase L3.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ difvifkey = 04FDC8FC03
+ }
+ }
+ field {
+ name = raw_frequency
+ quantity = Frequency
+ info = 'Frequency in 0.1 Hz'
+ attributes = HIDE
+ vif_scaling = None
+ dif_signedness = Signed
+ match {
+ difvifkey = 02FB2E
+ }
+ }
+ field {
+ name = frequency
+ quantity = Frequency
+ info = 'Frequency of AC.'
+ calculate = 'raw_frequency_hz / 10 counter'
+ }
+ }
+ tests {
+ test {
+ args = 'Elen em24 66666666 NOKEY'
+ telegram = 35442D2C6666666633028D2070806A0520B4D378_0405F208000004FB82753F00000004853C0000000004FB82F53CCA01000001FD1722
+ json = '{"_":"telegram","media":"electricity","driver":"em24","name":"Elen","id":"66666666","status":"I_3_OVERFLOW V_2_OVERFLOW","error":"I_3_OVERFLOW V_2_OVERFLOW","error_deprecated_by": "status, update before 2026-12-01","total_energy_consumption_kwh":229,"total_energy_production_kwh":0,"total_reactive_energy_consumption_kvarh":63,"total_reactive_energy_production_kvarh":458,"total_apparent_energy_consumption_kvah":237.507895,"total_apparent_energy_production_kvah":458,"frequency_hz":null,"timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'Elen;66666666;229;0;63;458;237.507895;458;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'Elen2 em24 02020202 NOKEY'
+ telegram = 4144361C0202020200028C209A7A9A0030252F2F_04050100000004FB82750000000004FB82F53C00000000042A0000000001FD17002F2F2F2F2F2F2F2F2F2F2F2F2F
+ json = '{"_":"telegram","error":"","error_deprecated_by": "status, update before 2026-12-01","id":"02020202","media":"electricity","driver":"em24","name":"Elen2","power_kw":0,"status":"OK","timestamp":"1111-11-11T11:11:11Z","total_apparent_energy_consumption_kvah":0.1,"total_apparent_energy_production_kvah":null,"total_energy_consumption_kwh":0.1,"total_reactive_energy_consumption_kvarh":0,"total_reactive_energy_production_kvarh":0,"frequency_hz":null}'
+ fields = 'Elen2;02020202;0.1;null;0;0;0.1;null;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'Elen2 em24 02020202 NOKEY'
+ telegram = 8144361C0202020200028C20357A351070252F2F_04050100000004FB82750000000004FB82F53C00000000042A0000000004FB140000000004FB943C0000000004FDD9FC010000000004FDD9FC020000000004FDD9FC030000000004FDC8FC011709000004FDC8FC02EC04000004FDC8FC03EC04000002FB2EF40101FD17002F2F2F
+ json = '{"_":"telegram","amperage_at_phase_1_a":0,"amperage_at_phase_2_a":0,"amperage_at_phase_3_a":0,"error":"","error_deprecated_by": "status, update before 2026-12-01","frequency_hz":50,"id":"02020202","media":"electricity","driver":"em24","name":"Elen2","power_kw":0,"status":"OK","timestamp":"1111-11-11T11:11:11Z","total_apparent_energy_consumption_kvah":0.1,"total_apparent_energy_production_kvah":null,"total_energy_consumption_kwh":0.1,"total_reactive_energy_consumption_kvarh":0,"total_reactive_energy_production_kvarh":0,"voltage_at_phase_1_v":232.7,"voltage_at_phase_2_v":126,"voltage_at_phase_3_v":126}'
+ fields = 'Elen2;02020202;0.1;null;0;0;0.1;null;1111-11-11 11:11.11'
+ }
+ }
+}
diff --git a/components/wmbus_common/drivers/src/emerlin868.xmq b/components/wmbus_common/drivers/src/emerlin868.xmq
new file mode 100644
index 00000000..669e66f5
--- /dev/null
+++ b/components/wmbus_common/drivers/src/emerlin868.xmq
@@ -0,0 +1,60 @@
+// Copyright (C) 2017-2022 Fredrik Öhrström (gpl-3.0-or-later)
+// Copyright (C) 2018 David Mallon (gpl-3.0-or-later)
+driver {
+ name = emerlin868
+ meter_type = WaterMeter
+ default_fields = name,id,total_m3,target_m3,timestamp
+ detect {
+ mvt = ELR,11,37
+ mvt = ELR,12,37
+ }
+ fields {
+ field {
+ name = total
+ quantity = Volume
+ info = 'The total water consumption recorded by this meter.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ measurement_type = Instantaneous
+ vif_range = Volume
+ }
+ }
+ field {
+ name = target
+ quantity = Volume
+ info = 'The target water consumption recorded at previous period.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ measurement_type = Instantaneous
+ vif_range = Volume
+ storage_nr = 1
+ }
+ }
+ field {
+ name = target_date
+ quantity = Text
+ info = 'Date when previous period ended.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = Date
+ storage_nr = 1
+ }
+ }
+ }
+ tests {
+ test {
+ args = 'Vodda emerlin868 95949392 NOKEY'
+ telegram = 2E4492159293949511377ABE0020252F2F_04135515000004FD971D80800000441300000000426C000002FDFD02B300
+ json = '{"_":"telegram","media":"radio converter (meter side)","driver":"emerlin868","name":"Vodda","id":"95949392","total_m3":5.461,"target_m3":0,"target_date":"2000-00-00","timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'Vodda;95949392;5.461;0;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'Water emerlin868 00502420 NOKEY'
+ telegram = 434492152024500012378C60A8900F002C25A89D05001CFF60E32B67B46A7AA8002007102F2F_04132564000004FD971D80800000441300000080426C000002FDFD02B300
+ json = '{"_":"telegram","media":"radio converter (meter side)","driver":"emerlin868","name":"Water","id":"00502420","total_m3":25.637,"target_m3":-2147483.648,"target_date":"2000-00-00","timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'Water;00502420;25.637;-2147483.648;1111-11-11 11:11.11'
+ }
+ }
+}
diff --git a/components/wmbus_common/drivers/src/enercal.xmq b/components/wmbus_common/drivers/src/enercal.xmq
new file mode 100644
index 00000000..119abf0c
--- /dev/null
+++ b/components/wmbus_common/drivers/src/enercal.xmq
@@ -0,0 +1,152 @@
+// Copyright (C) 2022 Fredrik Öhrström (gpl-3.0-or-later)
+driver {
+ name = enercal
+ meter_type = HeatMeter
+ default_fields = name,id,status,total_kwh,target_kwh,total_m3,target_m3,timestamp
+ detect {
+ mvt = GWF,08,04
+ }
+ library {
+ use = total_kwh
+ use = target_kwh
+ use = total_m3
+ use = target_m3
+ use = target_date
+ use = operating_time_h
+ use = on_time_h
+ use = meter_datetime
+ }
+ fields {
+ field {
+ name = status
+ quantity = Text
+ info = 'Meter status.'
+ attributes = STATUS,INCLUDE_TPL_STATUS
+ }
+ field {
+ name = power
+ quantity = Power
+ info = 'The active power consumption.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyPowerVIF
+ }
+ }
+ field {
+ name = flow
+ quantity = Flow
+ info = 'The flow of water.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = VolumeFlow
+ }
+ }
+ field {
+ name = flow_max
+ quantity = Flow
+ info = 'The maximum forward flow of water since the last set date?'
+ match {
+ measurement_type = Maximum
+ vif_range = VolumeFlow
+ }
+ }
+ field {
+ name = forward
+ quantity = Temperature
+ info = 'The forward temperature of the water.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = FlowTemperature
+ }
+ }
+ field {
+ name = return
+ quantity = Temperature
+ info = 'The return temperature of the water.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = ReturnTemperature
+ }
+ }
+ field {
+ name = difference
+ quantity = Temperature
+ info = 'The temperature difference forward-return for the water.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = TemperatureDifference
+ }
+ }
+ field {
+ name = subunit1
+ quantity = Volume
+ info = 'The amount of water that has passed through subunit 1.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = Volume
+ subunit_nr = 1
+ }
+ }
+ field {
+ name = subunit1_target
+ quantity = Volume
+ info = 'The amount of water that had passed through the subunit 1 at the set date.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = Volume
+ storage_nr = 1
+ subunit_nr = 1
+ }
+ }
+ field {
+ name = subunit1
+ quantity = HCA
+ info = 'The current heat cost allocation for subunit 1.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = HeatCostAllocation
+ subunit_nr = 1
+ }
+ }
+ field {
+ name = subunit1_target
+ quantity = HCA
+ info = 'The current heat cost allocation for subunit 1 at the target date.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = HeatCostAllocation
+ subunit_nr = 1
+ storage_nr = 1
+ }
+ }
+ field {
+ name = subunit2
+ quantity = HCA
+ info = 'The current heat cost allocation for subunit 2.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = HeatCostAllocation
+ subunit_nr = 2
+ }
+ }
+ field {
+ name = subunit2_target
+ quantity = HCA
+ info = 'The current heat cost allocation for subunit 2 at the target date.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = HeatCostAllocation
+ subunit_nr = 2
+ storage_nr = 1
+ }
+ }
+ }
+ tests {
+ test {
+ args = 'Heat enercal 00309924 NOKEY'
+ telegram = 688e8e6808017224993000e61e080406000000040681460600041488f2350084401426e02600025B2600025f220002622a00042247880200042647880200043B00000000042c00000000046d2d0ede2784406e000000008480406e00000000c4800006B1450600c48000143Be43500c4c000142fd22600c4c0006e00000000c480406e00000000c280006cc1271f00000000f416
+ json = '{"_":"telegram","media":"heat","driver":"enercal","name":"Heat","id":"00309924","status":"OK","total_kwh":411265,"target_kwh":411057,"power_kw":0,"flow_m3h":0,"forward_c":38,"return_c":34,"difference_c":4.2,"total_m3":35354.96,"target_m3":35318.35,"subunit1_m3":25477.5,"subunit1_target_m3":25441.75,"subunit1_hca":0,"subunit1_target_hca":0,"subunit2_hca":0,"subunit2_target_hca":0,"target_date":"2022-07-01","operating_time_h":165959,"on_time_h":165959,"meter_datetime":"2022-07-30 14:45","timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'Heat;00309924;OK;411265;411057;35354.96;35318.35;1111-11-11 11:11.11'
+ }
+ }
+}
diff --git a/components/wmbus_common/drivers/src/engelmann_faw.xmq b/components/wmbus_common/drivers/src/engelmann_faw.xmq
new file mode 100644
index 00000000..df12ea5a
--- /dev/null
+++ b/components/wmbus_common/drivers/src/engelmann_faw.xmq
@@ -0,0 +1,136 @@
+// Copyright (C) 2020-2022 Fredrik Öhrström (gpl-3.0-or-later)
+driver {
+ name = engelmann_faw
+ aliases = engelmann-faw
+ meter_type = WaterMeter
+ default_fields = name,id,status,reporting_date,consumption_at_reporting_date_m3,timestamp
+ detect {
+ mvt = EFE,00,07
+ }
+ compact_frame_formats {
+ difvif = 426C441301FD17840113C40113840213C40213840313C40313840413C40413840513C40513840613C40613840713C40713840813046D0413
+ }
+ fields {
+ field {
+ name = total
+ quantity = Volume
+ info = 'The current total water consumption.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = Volume
+ storage_nr = 0
+ }
+ }
+ field {
+ name = meter
+ quantity = PointInTime
+ info = 'Date and time of the meter.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = DateTime
+ storage_nr = 0
+ }
+ }
+ field {
+ name = status
+ quantity = Text
+ info = 'Status and error flags.'
+ attributes = STATUS,INCLUDE_TPL_STATUS
+ match {
+ measurement_type = Instantaneous
+ vif_range = ErrorFlags
+ }
+ lookup {
+ name = ERROR_FLAGS
+ map_type = BitToString
+ mask_bits = 0xff
+ default_message = OK
+ map {
+ name = VOLUME_DETECTION_COILS_DEFECT
+ value = 0x01
+ test = Set
+ }
+ map {
+ name = RESET
+ value = 0x02
+ test = Set
+ }
+ map {
+ name = CRC_ERROR
+ value = 0x04
+ test = Set
+ }
+ map {
+ name = REMOVAL_DETECTED
+ value = 0x08
+ test = Set
+ }
+ map {
+ name = MAGNETIC_MANIPULATION
+ value = 0x10
+ test = Set
+ }
+ map {
+ name = LEAKAGE
+ value = 0x20
+ test = Set
+ }
+ map {
+ name = BLOCKED
+ value = 0x40
+ test = Set
+ }
+ map {
+ name = REVERSE_FLOW
+ value = 0x80
+ test = Set
+ }
+ }
+ }
+ field {
+ name = reporting
+ quantity = PointInTime
+ info = 'The reporting date of the last billing period.'
+ display_unit = date
+ match {
+ measurement_type = Instantaneous
+ vif_range = Date
+ storage_nr = 1
+ }
+ }
+ field {
+ name = consumption_at_reporting_date
+ quantity = Volume
+ info = 'The water consumption at the last billing period date.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = Volume
+ storage_nr = 1
+ }
+ }
+ field {
+ name = 'consumption_{storage_counter-1counter}_months_ago'
+ quantity = Volume
+ info = 'Water consumption N month(s) ago.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = Volume
+ storage_nr = 2,16
+ }
+ }
+ }
+ tests {
+ test {
+ args = 'Wasserzaehler engelmann_faw 20254060 NOKEY'
+ telegram = 8f44c5145502004301077260402520c51400076b0000002f2f426cbf2c441322e9000001fd17008401133c340100c40113ae2d010084021303290100c402137e21010084031313180100c403138a0e010084041337060100c40413b2fc00008405139af30000c4051322e90000840613c1df0000c40613cdd5000084071365ce0000c407136dc500008408138dbf0000
+ json = '{"_":"telegram","media":"water","driver":"engelmann_faw","name":"Wasserzaehler","id":"20254060","status":"OK","reporting_date":"2021-12-31","consumption_at_reporting_date_m3":59.682,"consumption_1_months_ago_m3":78.908,"consumption_2_months_ago_m3":77.23,"consumption_3_months_ago_m3":76.035,"consumption_4_months_ago_m3":74.11,"consumption_5_months_ago_m3":71.699,"consumption_6_months_ago_m3":69.258,"consumption_7_months_ago_m3":67.127,"consumption_8_months_ago_m3":64.69,"consumption_9_months_ago_m3":62.362,"consumption_10_months_ago_m3":59.682,"consumption_11_months_ago_m3":57.281,"consumption_12_months_ago_m3":54.733,"consumption_13_months_ago_m3":52.837,"consumption_14_months_ago_m3":50.541,"consumption_15_months_ago_m3":49.037,"timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'Wasserzaehler;20254060;OK;2021-12-31;59.682;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'Wasserzaehler engelmann_faw 22862420 5CDB04C8EDFDC09EC49B5E76A24D4822'
+ telegram = 7644c5145528502202077320248622c5140007700060255f2aa2967eb77d0bf310341e3636d9acc17cc5d69ce5632d7b494cc0718e2dbb916bbec0c3ff2b0d6087fd0abb3ba0779564cc089e4527987ef2221d26d7ea448ca59759acfbea4e5b0026ef463592b6668fbf680896e78b461dbb77c32903e0
+ json = '{"_":"telegram","media":"water","driver":"engelmann_faw","name":"Wasserzaehler","id":"22862420","status":"OK","reporting_date":"2025-12-31","consumption_at_reporting_date_m3":228.854,"consumption_1_months_ago_m3":252.159,"consumption_2_months_ago_m3":245.162,"consumption_3_months_ago_m3":239.697,"consumption_4_months_ago_m3":235.11,"consumption_5_months_ago_m3":228.854,"consumption_6_months_ago_m3":222.898,"consumption_7_months_ago_m3":218.216,"consumption_8_months_ago_m3":213.264,"consumption_9_months_ago_m3":207.555,"consumption_10_months_ago_m3":203.639,"consumption_11_months_ago_m3":195.67,"consumption_12_months_ago_m3":188.6,"consumption_13_months_ago_m3":184.596,"consumption_14_months_ago_m3":177.827,"consumption_15_months_ago_m3":172.538,"meter_datetime":"2026-05-04 11:58","total_m3":253.409,"timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'Wasserzaehler;22862420;OK;2025-12-31;228.854;1111-11-11 11:11.11'
+ }
+ }
+}
diff --git a/components/wmbus_common/drivers/src/esyswm.xmq b/components/wmbus_common/drivers/src/esyswm.xmq
new file mode 100644
index 00000000..03b04090
--- /dev/null
+++ b/components/wmbus_common/drivers/src/esyswm.xmq
@@ -0,0 +1,140 @@
+// Copyright (C) 2020-2022 Fredrik Öhrström (gpl-3.0-or-later)
+driver {
+ name = esyswm
+ meter_type = ElectricityMeter
+ default_fields = name,id,total_energy_consumption_kwh,current_power_consumption_kw,total_energy_production_kwh,total_energy_consumption_tariff1_kwh,total_energy_consumption_tariff2_kwh,current_power_consumption_phase1_kw,current_power_consumption_phase2_kw,current_power_consumption_phase3_kw,enhanced_id,timestamp
+ detect {
+ mvt = ESY,30,37
+ mvt = ESY,11,02
+ }
+ library {
+ use = fabrication_no
+ use = enhanced_id
+ use = location
+ }
+ fields {
+ field {
+ name = location_hex
+ quantity = Text
+ info = 'Meter installed at this customer location. Deprecate field.'
+ deprecated_by = 'location, update by 2026-12-01'
+ match {
+ measurement_type = Instantaneous
+ vif_range = Location
+ }
+ }
+ field {
+ name = total_energy_consumption
+ quantity = Energy
+ info = 'The total energy consumption recorded by this meter.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyEnergyVIF
+ }
+ }
+ field {
+ name = current_power_consumption
+ quantity = Power
+ info = 'Calculated sum of power consumption of all phases.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyPowerVIF
+ }
+ }
+ field {
+ name = total_energy_production
+ quantity = Energy
+ info = 'The total energy production recorded by this meter.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyEnergyVIF
+ add_combinable = BackwardFlow
+ }
+ }
+ field {
+ name = total_energy_consumption_tariff1
+ quantity = Energy
+ info = 'The total energy consumption recorded by this meter on tariff 1.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyEnergyVIF
+ tariff_nr = 1
+ }
+ }
+ field {
+ name = total_energy_consumption_tariff2
+ quantity = Energy
+ info = 'The total energy consumption recorded by this meter on tariff 2.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyEnergyVIF
+ tariff_nr = 2
+ }
+ }
+ field {
+ name = current_power_consumption_phase1
+ quantity = Power
+ info = 'Current power consumption phase 1.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ difvifkey = 04A9FF01
+ }
+ }
+ field {
+ name = current_power_consumption_phase2
+ quantity = Power
+ info = 'Current power consumption phase 2.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ difvifkey = 04A9FF02
+ }
+ }
+ field {
+ name = current_power_consumption_phase3
+ quantity = Power
+ info = 'Current power consumption phase 3.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ difvifkey = 04A9FF03
+ }
+ }
+ field {
+ name = version
+ quantity = Text
+ info = 'Static version information.'
+ readable_string = Normal
+ match {
+ difvifkey = 0DFD09
+ }
+ }
+ }
+ tests {
+ test {
+ args = 'Elen2 esyswm 77997799 NOKEY'
+ comment = 'Static telegram'
+ telegram = 7B4479169977997730378C208B900F002C25E4EF0A002EA98E7D58B3ADC57299779977991611028B005087102F2F#0DFD090F34302e3030562030303030303030300D790E31323334353637383839595345310DFD100AAAAAAAAAAAAAAAAAAAAA0D780E31323334353637383930594553312F2F2F2F2F2F2F2F2F2F2F
+ json = '{"_":"telegram","media":"electricity","driver":"esyswm","name":"Elen2","id":"77997799","fabrication_no":"1SEY0987654321","enhanced_id":"1ESY9887654321","location":"AAAAAAAAAAAAAAAAAAAA","location_hex":"AAAAAAAAAAAAAAAAAAAA","location_hex_deprecated_by": "location, update by 2026-12-01","version":"00000000 V00.04","timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'Elen2;77997799;null;null;null;null;null;null;null;null;1ESY9887654321;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'Elen2 esyswm 77997799 NOKEY'
+ comment = 'Dynamic telegram (combined with static to accumulate library fields)'
+ telegram = 7B4479169977997730378C208B900F002C25E4EF0A002EA98E7D58B3ADC57299779977991611028B005087102F2F#0DFD090F34302e3030562030303030303030300D790E31323334353637383839595345310DFD100AAAAAAAAAAAAAAAAAAAAA0D780E31323334353637383930594553312F2F2F2F2F2F2F2F2F2F2F,7B4479169977997730378C20F0900F002C2549EE0A0077C19D3D1A08ABCD729977997779161102F0005007102F2F#0702F5C3FA000000000007823C5407000000000000841004E081020084200415000000042938AB000004A9FF01FA0A000004A9FF02050A000004A9FF03389600002F2F2F2F2F2F2F2F2F2F2F2F2F
+ json = '{"_":"telegram","media":"electricity","driver":"esyswm","name":"Elen2","id":"77997799","fabrication_no":"1SEY0987654321","enhanced_id":"1ESY9887654321","location":"AAAAAAAAAAAAAAAAAAAA","location_hex":"AAAAAAAAAAAAAAAAAAAA","location_hex_deprecated_by": "location, update by 2026-12-01","total_energy_consumption_kwh":1643.4165,"current_power_consumption_kw":0.43832,"total_energy_production_kwh":0.1876,"total_energy_consumption_tariff1_kwh":1643.2,"total_energy_consumption_tariff2_kwh":0.21,"current_power_consumption_phase1_kw":0.0281,"current_power_consumption_phase2_kw":0.02565,"current_power_consumption_phase3_kw":0.38456,"version":"00000000 V00.04","timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'Elen2;77997799;1643.4165;0.43832;0.1876;1643.2;0.21;0.0281;0.02565;0.38456;1ESY9887654321;1111-11-11 11:11.11'
+ }
+ }
+}
diff --git a/components/wmbus_common/drivers/src/eurisii.xmq b/components/wmbus_common/drivers/src/eurisii.xmq
new file mode 100644
index 00000000..d296f96a
--- /dev/null
+++ b/components/wmbus_common/drivers/src/eurisii.xmq
@@ -0,0 +1,148 @@
+// Copyright (C) 2019-2023 Fredrik Öhrström (gpl-3.0-or-later)
+driver {
+ name = eurisii
+ meter_type = HeatCostAllocationMeter
+ default_fields = name,id,current_consumption_hca,status,timestamp
+ detect {
+ mvt = INE,55,08
+ mvt = RAM,55,08
+ }
+ fields {
+ field {
+ name = status
+ quantity = Text
+ info = 'Meter status from error flags and tpl status field.'
+ attributes = STATUS,INCLUDE_TPL_STATUS
+ match {
+ measurement_type = Instantaneous
+ vif_range = ErrorFlags
+ }
+ lookup {
+ name = ERROR_FLAGS
+ map_type = BitToString
+ mask_bits = 0xffff
+ default_message = OK
+ map {
+ name = MEASUREMENT
+ value = 0x0001
+ test = Set
+ }
+ map {
+ name = SABOTAGE
+ value = 0x0002
+ test = Set
+ }
+ map {
+ name = BATTERY
+ value = 0x0004
+ test = Set
+ }
+ map {
+ name = CS
+ value = 0x0008
+ test = Set
+ }
+ map {
+ name = HF
+ value = 0x0010
+ test = Set
+ }
+ map {
+ name = RESET
+ value = 0x0020
+ test = Set
+ }
+ }
+ }
+ field {
+ name = error_flags
+ quantity = Text
+ info = 'DEPRECATED! Use status instead.'
+ attributes = STATUS,INCLUDE_TPL_STATUS
+ deprecated_by = 'status, update before 2026-12-01'
+ match {
+ measurement_type = Instantaneous
+ vif_range = ErrorFlags
+ }
+ lookup {
+ name = ERROR_FLAGS
+ map_type = BitToString
+ mask_bits = 0xffff
+ default_message = OK
+ map {
+ name = MEASUREMENT
+ value = 0x0001
+ test = Set
+ }
+ map {
+ name = SABOTAGE
+ value = 0x0002
+ test = Set
+ }
+ map {
+ name = BATTERY
+ value = 0x0004
+ test = Set
+ }
+ map {
+ name = CS
+ value = 0x0008
+ test = Set
+ }
+ map {
+ name = HF
+ value = 0x0010
+ test = Set
+ }
+ map {
+ name = RESET
+ value = 0x0020
+ test = Set
+ }
+ }
+ }
+ field {
+ name = current_consumption
+ quantity = HCA
+ info = 'The current heat cost allocation.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = HeatCostAllocation
+ }
+ }
+ field {
+ name = 'consumption_at_set_date_{storage_counter}'
+ quantity = HCA
+ info = 'The heat cost allocation at set date #.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = HeatCostAllocation
+ storage_nr = 1,17
+ }
+ }
+ field {
+ name = consumption_at_set_date
+ quantity = HCA
+ info = 'Deprecated field.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = HeatCostAllocation
+ storage_nr = 1
+ }
+ }
+ }
+ tests {
+ test {
+ args = 'HeatMeter eurisii 88018801 NOKEY'
+ telegram = 7644C52501880188550872_01880188C5255508010000002F2F0B6E332211426E110182016E1102C2016E110382026E1104C2026E110582036E1106C2036E110782046E1108C2046E110982056E1110C2056E111182066E1112C2066E111382076E1114C2076E111582086E1116C2086E111702FD172100
+ json = '{"_":"telegram","media":"heat cost allocation","driver":"eurisii","name":"HeatMeter","id":"88018801","status":"MEASUREMENT RESET","current_consumption_hca":112233,"consumption_at_set_date_hca":273,"consumption_at_set_date_1_hca":273,"consumption_at_set_date_2_hca":529,"consumption_at_set_date_3_hca":785,"consumption_at_set_date_4_hca":1041,"consumption_at_set_date_5_hca":1297,"consumption_at_set_date_6_hca":1553,"consumption_at_set_date_7_hca":1809,"consumption_at_set_date_8_hca":2065,"consumption_at_set_date_9_hca":2321,"consumption_at_set_date_10_hca":4113,"consumption_at_set_date_11_hca":4369,"consumption_at_set_date_12_hca":4625,"consumption_at_set_date_13_hca":4881,"consumption_at_set_date_14_hca":5137,"consumption_at_set_date_15_hca":5393,"consumption_at_set_date_16_hca":5649,"consumption_at_set_date_17_hca":5905,"error_flags":"MEASUREMENT RESET","error_flags_deprecated_by": "status, update before 2026-12-01","timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'HeatMeter;88018801;112233;MEASUREMENT RESET;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'HeatMeter2 eurisii 60200770 NOKEY'
+ telegram = 76442D4870072060550872700720602D485508280060052F2F_0B6E320100426E550082016E3500C2016E1F0082026E1F00C2026E130082036E1300C2036E130082046E1300C2046E120082056E7D01C2056E440182066E0601C2066EB80082076E7F00C2076E320082086E1C00C2086E1C0002FD170000
+ json = '{"_":"telegram","media":"heat cost allocation","driver":"eurisii","name":"HeatMeter2","id":"60200770","status":"OK","error_flags":"OK","error_flags_deprecated_by": "status, update before 2026-12-01","current_consumption_hca":132,"consumption_at_set_date_4_hca":31,"consumption_at_set_date_5_hca":19,"consumption_at_set_date_6_hca":19,"consumption_at_set_date_2_hca":53,"consumption_at_set_date_14_hca":127,"consumption_at_set_date_8_hca":19,"consumption_at_set_date_1_hca":85,"consumption_at_set_date_10_hca":381,"consumption_at_set_date_17_hca":28,"consumption_at_set_date_13_hca":184,"consumption_at_set_date_11_hca":324,"consumption_at_set_date_3_hca":31,"consumption_at_set_date_9_hca":18,"consumption_at_set_date_15_hca":50,"consumption_at_set_date_16_hca":28,"consumption_at_set_date_7_hca":19,"consumption_at_set_date_12_hca":262,"consumption_at_set_date_hca":85,"timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'HeatMeter2;60200770;132;OK;1111-11-11 11:11.11'
+ }
+ }
+}
diff --git a/components/wmbus_common/drivers/src/ev200.xmq b/components/wmbus_common/drivers/src/ev200.xmq
new file mode 100644
index 00000000..77f9fdb7
--- /dev/null
+++ b/components/wmbus_common/drivers/src/ev200.xmq
@@ -0,0 +1,39 @@
+// Copyright (C) 2017-2022 Fredrik Öhrström (gpl-3.0-or-later)
+// Copyright (C) 2018 David Mallon (gpl-3.0-or-later)
+driver {
+ name = ev200
+ meter_type = WaterMeter
+ default_fields = name,id,total_m3,target_m3,timestamp
+ detect {
+ mvt = ELR,0d,07
+ }
+ fields {
+ field {
+ name = total
+ quantity = Volume
+ info = 'The total water consumption recorded by this meter.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = Volume
+ }
+ }
+ field {
+ name = target
+ quantity = Volume
+ info = 'The target water consumption recorded at previous period.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = Volume
+ storage_nr = 1
+ }
+ }
+ }
+ tests {
+ test {
+ args = 'Voda ev200 99993030 NOKEY'
+ telegram = 2E449215303099990D077AB50820452F2F_0C12495849004C12557545000FB10445007022C50BFFFFFFFF0000FFF000
+ json = '{"_":"telegram","media":"water","driver":"ev200","name":"Voda","id":"99993030","total_m3":49.5849,"target_m3":45.7555,"timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'Voda;99993030;49.5849;45.7555;1111-11-11 11:11.11'
+ }
+ }
+}
diff --git a/components/wmbus_common/drivers/src/evo868.xmq b/components/wmbus_common/drivers/src/evo868.xmq
new file mode 100644
index 00000000..9308c0f9
--- /dev/null
+++ b/components/wmbus_common/drivers/src/evo868.xmq
@@ -0,0 +1,161 @@
+// Copyright (C) 2017-2022 Fredrik Öhrström (gpl-3.0-or-later)
+driver {
+ name = evo868
+ meter_type = WaterMeter
+ default_fields = name,id,total_m3,current_status,consumption_at_set_date_m3,set_date,timestamp
+ detect {
+ mvt = MAD,50,06
+ mvt = MAD,50,07
+ mvt = MAD,50,16
+ }
+ library {
+ use = fabrication_no
+ use = total_m3
+ }
+ fields {
+ field {
+ name = current_status
+ quantity = Text
+ info = 'Status of meter.'
+ attributes = STATUS,INCLUDE_TPL_STATUS
+ match {
+ measurement_type = Instantaneous
+ vif_range = ErrorFlags
+ }
+ lookup {
+ name = ERROR_FLAGS
+ map_type = BitToString
+ mask_bits = 0xffff
+ default_message = OK
+ }
+ }
+ field {
+ name = consumption_at_set_date
+ quantity = Volume
+ info = 'The total water consumption at the most recent billing period date.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ measurement_type = Instantaneous
+ vif_range = Volume
+ storage_nr = 1
+ }
+ }
+ field {
+ name = set_date
+ quantity = Text
+ info = 'The most recent billing period date.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = Date
+ storage_nr = 1
+ }
+ }
+ field {
+ name = consumption_at_set_date_2
+ quantity = Volume
+ info = 'The total water consumption at the second most recent billing period date.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ measurement_type = Instantaneous
+ vif_range = Volume
+ storage_nr = 2
+ }
+ }
+ field {
+ name = set_date_2
+ quantity = Text
+ info = 'The second most recent billing period date.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = Date
+ storage_nr = 2
+ }
+ }
+ field {
+ name = max_flow_since_datetime
+ quantity = Flow
+ info = 'Maximum water flow since date time.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ measurement_type = Maximum
+ vif_range = VolumeFlow
+ storage_nr = 3
+ }
+ }
+ field {
+ name = max_flow
+ quantity = PointInTime
+ info = 'The datetime to which maximum flow is measured.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = DateTime
+ storage_nr = 3
+ }
+ }
+ field {
+ name = 'consumption_at_history_{storage_counter-7counter}'
+ quantity = Volume
+ info = 'The total water consumption at the historic date.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ measurement_type = Instantaneous
+ vif_range = Volume
+ storage_nr = 8,19
+ }
+ }
+ field {
+ name = history_reference
+ quantity = PointInTime
+ info = 'Reference date for history.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ display_unit = date
+ match {
+ measurement_type = Instantaneous
+ vif_range = Date
+ storage_nr = 8
+ }
+ }
+ field {
+ name = 'history_{storage_counter-7counter}'
+ quantity = PointInTime
+ info = 'The historic date #.'
+ display_unit = date
+ calculate = 'history_reference_date - ((storage_counter-8counter) * 1 month)'
+ match {
+ measurement_type = Instantaneous
+ vif_range = Volume
+ storage_nr = 8,19
+ }
+ }
+ field {
+ name = device
+ quantity = PointInTime
+ info = 'Date and time when the meter sent the telegram.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = DateTime
+ }
+ }
+ }
+ tests {
+ test {
+ args = 'Votchka evo868 79787776 NOKEY'
+ comment = 'Test Maddalena EVO 868 wmbus module on water meter'
+ telegram = ae4424347677787950077ac10000202f2f041306070000046d1e31b12104fd17000000000e787880048120004413c9040000426c9f2c840113c904000082016c9f2cd3013b9a0200c4016d0534a7218104fd280182046c9f2c840413c9040000c404131b00000084051300000000c405130000000084061300000000c406130000000084071300000000c407130000000084081300000000c408130000000084091300000000c4091300000000ffff
+ json = '{"_":"telegram","media":"water","driver":"evo868","name":"Votchka","id":"79787776","total_m3":1.798,"device_datetime":"2021-01-17 17:30","current_status":"OK","fabrication_no":"002081048078","consumption_at_set_date_m3":1.225,"set_date":"2020-12-31","consumption_at_set_date_2_m3":1.225,"set_date_2":"2020-12-31","max_flow_since_datetime_m3h":0.666,"max_flow_datetime":"2021-01-07 20:05","history_reference_date":"2020-12-31","consumption_at_history_1_m3":1.225,"history_1_date":"2020-12-31","consumption_at_history_2_m3":0.027,"history_2_date":"2020-11-30","consumption_at_history_3_m3":0,"history_3_date":"2020-10-31","consumption_at_history_4_m3":0,"history_4_date":"2020-09-30","consumption_at_history_5_m3":0,"history_5_date":"2020-08-31","consumption_at_history_6_m3":0,"history_6_date":"2020-07-31","consumption_at_history_7_m3":0,"history_7_date":"2020-06-30","consumption_at_history_8_m3":0,"history_8_date":"2020-05-31","consumption_at_history_9_m3":0,"history_9_date":"2020-04-30","consumption_at_history_10_m3":0,"history_10_date":"2020-03-31","consumption_at_history_11_m3":0,"history_11_date":"2020-02-29","consumption_at_history_12_m3":0,"history_12_date":"2020-01-31","timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'Votchka;79787776;1.798;OK;1.225;2020-12-31;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'Wasser evo868 21031894 NOKEY'
+ comment = 'Test Maddalena EVO 868 wmbus module on water meter'
+ telegram = AC4424349418032150167A39000020_2F2F041356850000046D0A340B3C04FD17000000000E786858000000004413D0620000426CFF2C8401137A83000082016C1E3BD3013BCF0200C4016D09210E338104FD280182046C1E3B8404137A830000C40413BE7E00008405131A7B0000C405136B7600008406132A700000C40613ED6A00008407130F690000C40713536800008408136E670000C40813EE65000084091330640000C40913D0620000
+ json = '{"_":"telegram","media":"cold water","driver":"evo868","name":"Wasser","id":"21031894","consumption_at_history_1_m3":33.658,"consumption_at_history_10_m3":26.094,"consumption_at_history_11_m3":25.648,"consumption_at_history_12_m3":25.296,"consumption_at_history_2_m3":32.446,"consumption_at_history_3_m3":31.514,"consumption_at_history_4_m3":30.315,"consumption_at_history_5_m3":28.714,"consumption_at_history_6_m3":27.373,"consumption_at_history_7_m3":26.895,"consumption_at_history_8_m3":26.707,"consumption_at_history_9_m3":26.478,"consumption_at_set_date_m3":25.296,"consumption_at_set_date_2_m3":33.658,"history_1_date":"2024-11-30","history_10_date":"2024-02-29","history_11_date":"2024-01-31","history_12_date":"2023-12-31","history_2_date":"2024-10-31","history_3_date":"2024-09-30","history_4_date":"2024-08-31","history_5_date":"2024-07-31","history_6_date":"2024-06-30","history_7_date":"2024-05-31","history_8_date":"2024-04-30","history_9_date":"2024-03-31","history_reference_date":"2024-11-30","max_flow_since_datetime_m3h":0.719,"total_m3":34.134,"current_status":"OK","device_datetime":"2024-12-11 20:10","fabrication_no":"000000005868","max_flow_datetime":"2024-03-14 01:09","set_date":"2023-12-31","set_date_2":"2024-11-30","timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'Wasser;21031894;34.134;OK;25.296;2023-12-31;1111-11-11 11:11.11'
+ }
+ }
+}
diff --git a/components/wmbus_common/drivers/src/fhkvdataiii.xmq b/components/wmbus_common/drivers/src/fhkvdataiii.xmq
new file mode 100644
index 00000000..6b7cc42b
--- /dev/null
+++ b/components/wmbus_common/drivers/src/fhkvdataiii.xmq
@@ -0,0 +1,170 @@
+// Copyright (C) 2019-2022 Fredrik Öhrström (gpl-3.0-or-later)
+driver {
+ name = fhkvdataiii
+ meter_type = HeatCostAllocationMeter
+ default_fields = name,id,current_hca,current_date,previous_hca,previous_date,temp_room_c,temp_radiator_c,timestamp
+ detect {
+ mvt = TCH,69,80
+ mvt = TCH,94,80
+ }
+ fields {
+ field {
+ name = mfct_specific
+ quantity = Text
+ attributes = HIDE
+ match_entire_payload = true
+ ixml = "decode = version_01 | version_69 | version_94.
+ version_01 = '01', PrevDate, PrevHca, CurrDate, CurrHca, TempRoom, TempRad, byte*.
+ version_69 = '11', PrevDate, PrevHca, CurrDate, CurrHca, TempRoom, TempRad, byte*.
+ version_94 = '0F', PrevDate, PrevHca, CurrDate, CurrHca, Extra, TempRoom, TempRad, byte*.
+
+ PrevDate = word, @DV_prev_date_raw.
+ PrevHca = word, @DV_prev_hca.
+ CurrDate = word, @DV_curr_date_raw.
+ CurrHca = word, @DV_curr_hca.
+ -Extra = byte.
+ TempRoom = word, @DV_temp_room.
+ TempRad = word, @DV_temp_rad.
+
+ -hex = ['A'-'F';'0'-'9'].
+ -byte = hex, hex.
+ -word = byte, byte.
+
+ DV_prev_date_raw>dvk = +'02FD3A'.
+ DV_prev_hca>dvk = +'426E'.
+ DV_curr_date_raw>dvk = +'42FD3A'.
+ DV_curr_hca>dvk = +'026E'.
+ DV_temp_room>dvk = +'0265'.
+ DV_temp_rad>dvk = +'025D'."
+ }
+ field {
+ name = current
+ quantity = HCA
+ info = 'Energy consumption so far in this billing period.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = HeatCostAllocation
+ }
+ }
+ field {
+ name = previous
+ quantity = HCA
+ info = 'Energy consumption in previous billing period.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = HeatCostAllocation
+ storage_nr = 1
+ }
+ }
+ field {
+ name = previous_date_raw
+ quantity = Dimensionless
+ attributes = HIDE
+ match {
+ difvifkey = 02FD3A
+ }
+ }
+ field {
+ name = current_date_raw
+ quantity = Dimensionless
+ attributes = HIDE
+ match {
+ difvifkey = 42FD3A
+ }
+ }
+ field {
+ name = previous_day
+ quantity = Dimensionless
+ attributes = HIDE
+ calculate = 'previous_date_raw_counter % 32counter'
+ }
+ field {
+ name = previous_month
+ quantity = Dimensionless
+ attributes = HIDE
+ calculate = '(previous_date_raw_counter >> 5counter) % 16counter'
+ }
+ field {
+ name = previous_year
+ quantity = Dimensionless
+ attributes = HIDE
+ calculate = 'previous_date_raw_counter >> 9counter'
+ }
+ field {
+ name = current_day
+ quantity = Dimensionless
+ attributes = HIDE
+ calculate = '((current_date_raw_counter >> 4counter) % 32counter) + ((((current_date_raw_counter >> 4counter) % 32counter) <= 0counter) * 1counter)'
+ }
+ field {
+ name = current_month
+ quantity = Dimensionless
+ attributes = HIDE
+ calculate = '((current_date_raw_counter >> 9counter) % 16counter) + ((((current_date_raw_counter >> 9counter) % 16counter) <= 0counter) * 12counter)'
+ }
+ field {
+ name = current_year
+ quantity = Dimensionless
+ attributes = HIDE
+ info = 'Current billing year is previous year unless month/day rolled over.'
+ calculate = 'previous_year_counter + ((current_month_counter < previous_month_counter) || ((current_month_counter == previous_month_counter) && (current_day_counter <= previous_day_counter)))'
+ }
+ field {
+ name = current
+ quantity = PointInTime
+ display_unit = date
+ info = 'Date of current billing period.'
+ calculate = "'2000-01-01' +
+ (((current_year_counter * 12counter) + current_month_counter - 1counter) * 1month) +
+ ((current_day_counter - 1counter) * 24h)"
+ }
+ field {
+ name = previous
+ quantity = PointInTime
+ display_unit = date
+ info = 'Date of last billing period.'
+ calculate = "'2000-01-01' +
+ (((previous_year_counter * 12counter) + previous_month_counter - 1counter) * 1month) +
+ ((previous_day_counter - 1counter) * 24h)"
+ }
+ field {
+ name = temp_room
+ quantity = Temperature
+ info = 'Current room temperature.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = ExternalTemperature
+ }
+ }
+ field {
+ name = temp_radiator
+ quantity = Temperature
+ info = 'Current radiator temperature.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = ReturnTemperature
+ }
+ }
+ }
+ tests {
+ test {
+ args = 'Room fhkvdataiii 11776622 NOKEY'
+ telegram = 34446850226677116980A0119F27020480048300C408F709143C003D341A2B0B2A0707000000000000062D114457563D71A1850000
+ json = '{"_":"telegram","media":"heat cost allocator","driver":"fhkvdataiii","name":"Room","id":"11776622","current_hca":131,"current_date":"2020-02-08","previous_hca":1026,"previous_date":"2019-12-31","temp_room_c":22.44,"temp_radiator_c":25.51,"timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'Room;11776622;131;2020-02-08;1026;2019-12-31;22.44;25.51;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'Rooom fhkvdataiii 11111234 NOKEY'
+ telegram = 33446850341211119480A2_0F9F292D005024040011BD08380904000000070000000000000000000000000001000000000003140E
+ json = '{"_":"telegram","media":"heat cost allocator","driver":"fhkvdataiii","name":"Rooom","id":"11111234","current_hca":4,"current_date":"2021-02-05","previous_hca":45,"previous_date":"2020-12-31","temp_room_c":22.37,"temp_radiator_c":23.6,"timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'Rooom;11111234;4;2021-02-05;45;2020-12-31;22.37;23.6;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'Version01 fhkvdataiii 41059605 NOKEY'
+ comment = 'Test another, perhaps older, telegram which starts with 01 instead of 11.'
+ telegram = 32446850059605416980A0_019F334805400CFE029808B1080000000000060F4F3467AEA9A88D7B8D503E000000000000000000
+ json = '{"_": "telegram","current_date": "2026-06-04","current_hca": 766,"id": "41059605","media": "heat cost allocator","driver": "fhkvdataiii","name": "Version01","previous_date": "2025-12-31","previous_hca": 1352,"temp_radiator_c": 22.25,"temp_room_c": 22,"timestamp": "1111-11-11T11:11:11Z"}'
+ fields = 'Version01;41059605;766;2026-06-04;1352;2025-12-31;22;22.25;1111-11-11 11:11.11'
+ }
+ }
+}
diff --git a/components/wmbus_common/drivers/src/fhkvdataiv.xmq b/components/wmbus_common/drivers/src/fhkvdataiv.xmq
new file mode 100644
index 00000000..64c4a79a
--- /dev/null
+++ b/components/wmbus_common/drivers/src/fhkvdataiv.xmq
@@ -0,0 +1,93 @@
+// Copyright (C) 2019-2022 Fredrik Öhrström (gpl-3.0-or-later)
+driver {
+ name = fhkvdataiv
+ meter_type = HeatCostAllocationMeter
+ default_fields = name,id,current_consumption_hca,set_date,consumption_at_set_date_hca,timestamp
+ detect {
+ mvt = TCH,69,08
+ mvt = TCH,94,08
+ }
+ fields {
+ field {
+ name = status
+ quantity = Text
+ info = 'Meter status from tpl status field.'
+ attributes = STATUS,INCLUDE_TPL_STATUS
+ }
+ field {
+ name = current_consumption
+ quantity = HCA
+ info = 'The current heat cost allocation.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ measurement_type = Instantaneous
+ vif_range = HeatCostAllocation
+ }
+ }
+ field {
+ name = set_date
+ quantity = Text
+ info = 'The most recent billing period date.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = Date
+ storage_nr = 1
+ }
+ }
+ field {
+ name = consumption_at_set_date
+ quantity = HCA
+ info = 'Heat cost allocation at the most recent billing period date.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ measurement_type = Instantaneous
+ vif_range = HeatCostAllocation
+ storage_nr = 1
+ }
+ }
+ field {
+ name = set_date_8
+ quantity = Text
+ info = 'Base date for compact-profile history.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = Date
+ storage_nr = 8
+ }
+ }
+ field {
+ name = 'consumption_at_set_date_{storage_counter-8counter}'
+ quantity = HCA
+ info = 'Heat cost allocation history from compact-profile billing periods.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ measurement_type = Instantaneous
+ vif_range = HeatCostAllocation
+ add_combinable = Synthetic
+ storage_nr = 9,22
+ }
+ }
+ field {
+ name = 'consumption_at_set_date_{storage_counter-8counter}'
+ quantity = Text
+ info = 'Billing period date for each compact-profile history slot.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = Date
+ add_combinable = Synthetic
+ storage_nr = 9,22
+ }
+ }
+ }
+ tests {
+ test {
+ args = 'Rooom fhkvdataiv 14542076 FCF41938F63432975B52505F547FCEDF'
+ telegram = 4E4468507620541494087AAD004005089D86B62A329B3439873999738F82461ABDE3C7AC78692B363F3B41EB68607F9C9160F550769B065B6EA00A2E44346E29FF5DC5CB86283C69324AD33D137F6F
+ json = '{"_":"telegram","media":"heat cost allocation","driver":"fhkvdataiv","name":"Rooom","id":"14542076","status":"OK","current_consumption_hca":2,"set_date":"2020-12-31","consumption_at_set_date_hca":25,"set_date_8":"2019-10-31","consumption_at_set_date_1_hca":22,"consumption_at_set_date_1":"2019-11-30","consumption_at_set_date_2_hca":3,"consumption_at_set_date_2":"2019-12-31","consumption_at_set_date_3_hca":0,"consumption_at_set_date_3":"2020-01-31","consumption_at_set_date_4_hca":0,"consumption_at_set_date_4":"2020-02-29","consumption_at_set_date_5_hca":0,"consumption_at_set_date_5":"2020-03-31","consumption_at_set_date_6_hca":0,"consumption_at_set_date_6":"2020-04-30","consumption_at_set_date_7_hca":0,"consumption_at_set_date_7":"2020-05-31","consumption_at_set_date_8_hca":0,"consumption_at_set_date_8":"2020-06-30","consumption_at_set_date_9_hca":0,"consumption_at_set_date_9":"2020-07-31","consumption_at_set_date_10_hca":0,"consumption_at_set_date_10":"2020-08-31","consumption_at_set_date_11_hca":0,"consumption_at_set_date_11":"2020-09-30","consumption_at_set_date_12_hca":0,"consumption_at_set_date_12":"2020-10-31","consumption_at_set_date_13_hca":0,"consumption_at_set_date_13":"2020-11-30","consumption_at_set_date_14_hca":0,"consumption_at_set_date_14":"2020-12-31","timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'Rooom;14542076;2;2020-12-31;25;1111-11-11 11:11.11'
+ }
+ }
+}
diff --git a/components/wmbus_common/drivers/src/fiowater.xmq b/components/wmbus_common/drivers/src/fiowater.xmq
index 611e53db..316462f4 100644
--- a/components/wmbus_common/drivers/src/fiowater.xmq
+++ b/components/wmbus_common/drivers/src/fiowater.xmq
@@ -15,7 +15,7 @@ driver {
test {
args = 'fio fiowater 25016914 NOKEY'
telegram = 43442F191469012501078C2075900F002C257C690300FDD9AE90F019F1C07A47102007102F2F_0C1322921500046D3A2A3A390F60011907052F2F2F2F2F2F2F2F2F2F2F2F
- json = '{"_":"telegram","media":"water","status":"TEMPORARY_ERROR","meter":"fiowater","name":"fio","id":"25016914","meter_datetime":"2025-09-26 10:58","total_m3":159.222,"timestamp":"1111-11-11T11:11:11Z"}'
+ json = '{"_":"telegram","media":"water","status":"TEMPORARY_ERROR","driver":"fiowater","name":"fio","id":"25016914","meter_datetime":"2025-09-26 10:58","total_m3":159.222,"timestamp":"1111-11-11T11:11:11Z"}'
fields = 'fio;25016914;TEMPORARY_ERROR;159.222;1111-11-11 11:11.11'
}
}
diff --git a/components/wmbus_common/drivers/src/gransystems.xmq b/components/wmbus_common/drivers/src/gransystems.xmq
new file mode 100644
index 00000000..bf4196f7
--- /dev/null
+++ b/components/wmbus_common/drivers/src/gransystems.xmq
@@ -0,0 +1,272 @@
+// Copyright (C) 2018-2022 Fredrik Öhrström (gpl-3.0-or-later)
+// Implements GSS CC101 (single-phase) and CC301 (three-phase) electricity meters.
+driver {
+ name = gransystems
+ meter_type = ElectricityMeter
+ default_fields = name,id,total_energy_consumption_kwh,timestamp
+ detect {
+ mvt = GSS,01,02
+ }
+ fields {
+ field {
+ name = status
+ quantity = Text
+ info = 'Status of meter.'
+ attributes = STATUS,INCLUDE_TPL_STATUS
+ match {
+ measurement_type = Instantaneous
+ vif_range = ErrorFlags
+ }
+ lookup {
+ name = ERROR_FLAGS
+ map_type = BitToString
+ mask_bits = 0xffff
+ default_message = OK
+ map {
+ name = METER_HARDWARE_ERROR
+ value = 0x0001
+ test = Set
+ }
+ map {
+ name = RTC_ERROR
+ value = 0x0002
+ test = Set
+ }
+ map {
+ name = DSP_COMMUNICATION_ERROR
+ value = 0x0100
+ test = Set
+ }
+ map {
+ name = DSP_HARDWARE_ERROR
+ value = 0x0200
+ test = Set
+ }
+ map {
+ name = RAM_ERROR
+ value = 0x4000
+ test = Set
+ }
+ map {
+ name = ROM_ERROR
+ value = 0x8000
+ test = Set
+ }
+ map {
+ name = DEVICE_NOT_CONFIGURED
+ value = 0x0008
+ test = Set
+ }
+ map {
+ name = INTERNAL_ERROR
+ value = 0x0010
+ test = Set
+ }
+ map {
+ name = BATTERY_LOW
+ value = 0x0020
+ test = Set
+ }
+ map {
+ name = MAGNETIC_FRAUD_PRESENT
+ value = 0x0040
+ test = Set
+ }
+ map {
+ name = MAGNETIC_FRAUD_PAST
+ value = 0x0080
+ test = Set
+ }
+ map {
+ name = CALIBRATION_EEPROM_ERROR
+ value = 0x0800
+ test = Set
+ }
+ map {
+ name = EEPROM1_ERROR
+ value = 0x1000
+ test = Set
+ }
+ }
+ }
+ field {
+ name = info
+ quantity = Text
+ info = 'Is it a three phase or single phase meter.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = ErrorFlags
+ }
+ lookup {
+ name = INFO_FLAGS
+ map_type = IndexToString
+ mask_bits = 0x01030000
+ default_message = SINGLE_PHASE_METER
+ map {
+ name = SINGLE_PHASE_METER
+ value = 0x01020000
+ test = Set
+ }
+ map {
+ name = THREE_PHASE_METER
+ value = 0x01010000
+ test = Set
+ }
+ }
+ }
+ field {
+ name = total_energy_consumption
+ quantity = Energy
+ info = 'The total energy consumption recorded by this meter.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyEnergyVIF
+ }
+ }
+ field {
+ name = 'total_energy_consumption_tariff_{tariff_counter}'
+ quantity = Energy
+ info = 'The total energy consumption recorded by this meter.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyEnergyVIF
+ tariff_nr = 1,4
+ }
+ }
+ field {
+ name = target
+ quantity = PointInTime
+ info = 'Last day?'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ display_unit = datetime
+ match {
+ measurement_type = Instantaneous
+ vif_range = DateTime
+ storage_nr = 2
+ }
+ }
+ field {
+ name = target_energy_consumption
+ quantity = Energy
+ info = 'Last day energy consumption?'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyEnergyVIF
+ storage_nr = 2
+ }
+ }
+ field {
+ name = 'target_energy_consumption_tariff_{tariff_counter}'
+ quantity = Energy
+ info = 'Last day energy consumption for tariff?'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyEnergyVIF
+ tariff_nr = 1,4
+ storage_nr = 2
+ }
+ }
+ field {
+ name = device
+ quantity = PointInTime
+ info = 'Device date time when telegram was sent.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ display_unit = datetime
+ match {
+ measurement_type = Instantaneous
+ vif_range = DateTime
+ }
+ }
+ field {
+ name = voltage_at_phase_1
+ quantity = Voltage
+ info = 'Voltage for single phase meter.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ measurement_type = Instantaneous
+ vif_range = Voltage
+ subunit_nr = 0
+ }
+ }
+ field {
+ name = 'voltage_at_phase_{subunit_counter}'
+ quantity = Voltage
+ info = 'Voltage at phase L#.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ measurement_type = Instantaneous
+ vif_range = Voltage
+ subunit_nr = 1,3
+ }
+ }
+ field {
+ name = current_at_phase_1
+ quantity = Amperage
+ info = 'Amperage for single phase meter.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ measurement_type = Instantaneous
+ vif_range = Amperage
+ subunit_nr = 0
+ }
+ }
+ field {
+ name = 'current_at_phase_{subunit_counter}'
+ quantity = Amperage
+ info = 'Amperage at phase L#.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ measurement_type = Instantaneous
+ vif_range = Amperage
+ subunit_nr = 1,3
+ }
+ }
+ field {
+ name = raw_frequency
+ quantity = Frequency
+ info = 'Raw input to frequency.'
+ vif_scaling = None
+ dif_signedness = Signed
+ attributes = HIDE
+ match {
+ difvifkey = 02FB2D
+ }
+ }
+ field {
+ name = frequency
+ quantity = Frequency
+ info = 'Frequency of AC.'
+ calculate = 'raw_frequency_hz / 100 counter'
+ }
+ }
+ tests {
+ test {
+ args = 'Gran101 gransystems 18046178 NOKEY'
+ comment = 'Gran-System-S electricity meter 101'
+ telegram = 7844731e78610418010278046d0f13bc21040394030000841003690300008420032b00000084300300000000848010030000000084016d0000bc2184010394030000841103690300008421032b00000084310300000000848110030000000004fd482e09000004fd5b0000000002fb2d861304fd1700000201
+ json = '{"_":"telegram","media":"electricity","driver":"gransystems","name":"Gran101","id":"18046178","status":"OK","info":"SINGLE_PHASE_METER","total_energy_consumption_kwh":0.916,"total_energy_consumption_tariff_4_kwh":0,"total_energy_consumption_tariff_2_kwh":0.043,"total_energy_consumption_tariff_3_kwh":0,"total_energy_consumption_tariff_1_kwh":0.873,"target_datetime":"2021-01-28 00:00","target_energy_consumption_kwh":0.916,"target_energy_consumption_tariff_2_kwh":0.043,"target_energy_consumption_tariff_4_kwh":0,"target_energy_consumption_tariff_3_kwh":0,"target_energy_consumption_tariff_1_kwh":0.873,"device_datetime":"2021-01-28 19:15","voltage_at_phase_1_v":235,"current_at_phase_1_a":0,"frequency_hz":49.98,"timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'Gran101;18046178;0.916;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'Gran301 gransystems 20100117 NOKEY'
+ comment = 'Gran-System-S electricity meter 301'
+ telegram = 9e44731e17011020010278046d0813bc21040300000000841003000000008420030000000084300300000000848010030000000084016d0000bc218401030000000084110300000000842103000000008431030000000084811003000000008440fd4825090000848040fd480000000084c040fd48000000008440fd5b00000000848040fd5b0000000084c040fd5b0000000002fb2d881304fd1702000101
+ json = '{"_":"telegram","media":"electricity","driver":"gransystems","name":"Gran301","id":"20100117","status":"RTC_ERROR","info":"THREE_PHASE_METER","total_energy_consumption_kwh":0,"total_energy_consumption_tariff_2_kwh":0,"total_energy_consumption_tariff_3_kwh":0,"total_energy_consumption_tariff_4_kwh":0,"total_energy_consumption_tariff_1_kwh":0,"target_datetime":"2021-01-28 00:00","target_energy_consumption_kwh":0,"target_energy_consumption_tariff_1_kwh":0,"target_energy_consumption_tariff_2_kwh":0,"target_energy_consumption_tariff_4_kwh":0,"target_energy_consumption_tariff_3_kwh":0,"device_datetime":"2021-01-28 19:08","voltage_at_phase_2_v":0,"voltage_at_phase_3_v":0,"voltage_at_phase_1_v":234.1,"current_at_phase_1_a":0,"current_at_phase_2_a":0,"current_at_phase_3_a":0,"frequency_hz":50,"timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'Gran301;20100117;0;1111-11-11 11:11.11'
+ }
+ }
+}
diff --git a/components/wmbus_common/drivers/src/gwfgas.xmq b/components/wmbus_common/drivers/src/gwfgas.xmq
new file mode 100644
index 00000000..61eae850
--- /dev/null
+++ b/components/wmbus_common/drivers/src/gwfgas.xmq
@@ -0,0 +1,30 @@
+// Copyright (C) 2026 Fredrik Öhrström (gpl-3.0-or-later)
+driver {
+ name = gwfgas
+ meter_type = GasMeter
+ default_fields = name,id,total_m3,target_m3,timestamp
+ detect {
+ mvt = GWF,3c,03
+ }
+ library {
+ use = total_m3
+ use = target_m3
+ use = target_date
+ use = actuality_duration_s
+ }
+ fields {
+ field {
+ name = status
+ quantity = Text
+ attributes = STATUS,INCLUDE_TPL_STATUS
+ }
+ }
+ tests {
+ test {
+ args = 'mygas1 gwfgas 23115250 NOKEY'
+ telegram = 3644E61E32616600020E7250521123E61E3C031B0020652F2F_03745E00000413FE8D0100441300000000426C000002FD74B10F0F0100A8
+ json = '{"_": "telegram","status":"OK","actuality_duration_s": 94,"id": "23115250","media": "gas","driver": "gwfgas","name": "mygas1","target_date": "1999-11-30","target_m3": 0,"timestamp": "1111-11-11T11:11:11Z","total_m3": 101.886}'
+ fields = 'mygas1;23115250;101.886;0;1111-11-11 11:11.11'
+ }
+ }
+}
diff --git a/components/wmbus_common/drivers/src/gwfwater.xmq b/components/wmbus_common/drivers/src/gwfwater.xmq
new file mode 100644
index 00000000..943f84f9
--- /dev/null
+++ b/components/wmbus_common/drivers/src/gwfwater.xmq
@@ -0,0 +1,162 @@
+/* Copyright (C) 2023-2026 Fredrik Öhrström (gpl-3.0-or-later)
+
+ There is yet another field that is not understood.
+ 049 : 02 dif (16 Bit Integer/Binary Instantaneous value)
+ 050 : FD vif (Second extension FD of VIF-codes)
+ 051 : 74 vife (Reserved)
+ 052 C?: B514
+
+ And the mfct specific data can start with something other than 01
+ which means that we do not know how to decode it. */
+driver {
+ name = gwfwater
+ meter_type = WaterMeter
+ default_fields = name,id,total_m3,timestamp
+ detect {
+ mvt = GWF,01,0E
+ mvt = GWF,3C,07
+ }
+ library {
+ use = actuality_duration_s
+ use = total_m3
+ use = target_m3
+ use = target_date
+ }
+ fields {
+ field {
+ name = mfct_decoder
+ quantity = Text
+ attributes = HIDE
+ info = 'Decode mfct specific data after DIF 0x0F (type = 01).'
+ ixml = "decode = good | bad.
+ -good = -'01', byte_a, byte_b, -rest.
+ -hex = ['A'-'F';'0'-'9'].
+ -byte = hex, hex.
+ byte_a = byte, @DV_01FD17.
+ byte_b = byte, @DV_0102.
+ -rest = byte*.
+ { 01FD17 should really be matched agains ErrorFlags vif range
+ but it doesnt right now. Bug? }
+ DV_01FD17>dvk = +'01FD17'.
+ DV_0102>dvk = +'0102'.
+ bad = not_01_byte, -rest.
+ not_01_byte = '0', not_1, @DV_ERRFLAG | not_0, hex, @DV_ERRFLAG.
+ -not_0 = ['1'-'9';'A'-'F'].
+ -not_1 = ['0';'2'-'9';'A'-'F'].
+ { 41FD17 should really be matched agains ErrorFlags vif range
+ and storage nr 1, but it doesnt right now. Bug? }
+ DV_ERRFLAG>dvk = +'41FD17'."
+ match {
+ difvifkey = 0F
+ }
+ }
+ field {
+ name = status
+ quantity = Text
+ info = 'Status and error flags.'
+ attributes = STATUS,INCLUDE_TPL_STATUS
+ match {
+ difvifkey = 01FD17
+ }
+ lookup {
+ name = MFCT_FLAGS
+ map_type = BitToString
+ mask_bits = 0xFF
+ default_message = OK
+ map {
+ name = CONTINUOUS_FLOW
+ value = 0x02
+ test = Set
+ }
+ map {
+ name = BROKEN_PIPE
+ value = 0x08
+ test = Set
+ }
+ map {
+ name = BATTERY_LOW
+ value = 0x20
+ test = Set
+ }
+ map {
+ name = BACKFLOW
+ value = 0x40
+ test = Set
+ }
+ }
+ }
+ field {
+ name = mfct_type_error
+ quantity = Text
+ info = 'Set when manufacturer specific block has unknown type byte.'
+ attributes = INJECT_INTO_STATUS,HIDE
+ match {
+ difvifkey = 41FD17
+ }
+ lookup {
+ name = UNKNOWN_MFCT_STATUS
+ map_type = BitToString
+ mask_bits = 0xFF
+ default_message = OK
+ }
+ }
+ field {
+ name = b_raw
+ quantity = Dimensionless
+ info = 'Raw byte b for battery and power mode extraction.'
+ attributes = HIDE
+ display_unit = counter
+ vif_scaling = None
+ dif_signedness = Unsigned
+ match {
+ difvifkey = 0102
+ }
+ }
+ field {
+ name = power_mode
+ quantity = Text
+ info = 'Normal or saving.'
+ match {
+ difvifkey = 0102
+ }
+ lookup {
+ name = POWER_MODE
+ map_type = BitToString
+ mask_bits = 0x01
+ default_message = NORMAL
+ map {
+ name = SAVING
+ value = 0x01
+ test = Set
+ }
+ }
+ }
+ field {
+ name = battery
+ quantity = Time
+ info = 'Remaining battery life in years.'
+ display_unit = y
+ calculate = 'floor(b_raw_counter / 8 counter) * 0.5 y'
+ }
+ }
+ tests {
+ test {
+ args = 'Wateroo gwfwater 20221031 NOKEY'
+ telegram = 3144E61E31102220010E8C04F47ABE0420452F2F_037410000004133E0000004413FFFFFFFF426CFFFF0F0120012F2F2F2F2F
+ json = '{"_":"telegram","actuality_duration_s": 16,"battery_y": 0,"id": "20221031","media": "bus/system component","driver": "gwfwater","name": "Wateroo","power_mode": "SAVING","status": "BATTERY_LOW POWER_LOW","target_date":null,"target_m3": -0.001,"timestamp": "1111-11-11T11:11:11Z","total_m3": 0.062}'
+ fields = 'Wateroo;20221031;0.062;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'Watertwo gwfwater 19680750 NOKEY'
+ telegram = 3944E61E46441510020E8C04427250076819E61E3C07B10020452F2F_03748B05000413DC690D004413A4630D00426C213C02FD74B5140F0100E3
+ json = '{"_": "telegram","actuality_duration_s": 1419,"battery_y": 14,"id": "19680750","media": "water","driver": "gwfwater","name": "Watertwo","power_mode": "SAVING","status": "OK","target_date": "2025-12-01","target_m3": 877.476,"timestamp": "1111-11-11T11:11:11Z","total_m3": 879.068}'
+ fields = 'Watertwo;19680750;879.068;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'Watererr gwfwater 19680750 NOKEY'
+ telegram = 3944E61E46441510020E8C04427250076819E61E3C07B10020452F2F_03748B05000413DC690D004413A4630D00426C213C02FD74B5140F0200E3
+ json = '{"_":"telegram","media":"water","driver":"gwfwater","name":"Watererr","id":"19680750","battery_y": null,"actuality_duration_s":1419,"target_m3":877.476,"target_date":"2025-12-01","total_m3":879.068,"status":"UNKNOWN_MFCT_STATUS_2","timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'Watererr;19680750;879.068;1111-11-11 11:11.11'
+ }
+ }
+}
diff --git a/components/wmbus_common/drivers/src/hcae2.xmq b/components/wmbus_common/drivers/src/hcae2.xmq
new file mode 100644
index 00000000..b8850ea1
--- /dev/null
+++ b/components/wmbus_common/drivers/src/hcae2.xmq
@@ -0,0 +1,285 @@
+// Copyright (C) 2019-2023 Fredrik Öhrström (gpl-3.0-or-later)
+// Copyright (C) 2026 Felix Hauptmann (gpl-3.0-or-later)
+driver {
+ name = hcae2
+ meter_type = HeatCostAllocationMeter
+ default_fields = name,id,consumption_hca,status,timestamp
+ detect {
+ mvt = EFE,31,08
+ }
+ library {
+ use = status-tpl-only
+ use = consumption_hca
+ use = target_hca
+ use = target_date
+ use = meter_datetime
+ use = meter_datetime_at_error
+ }
+ fields {
+ field {
+ name = status_mfct
+ quantity = Text
+ info = 'Manufacturer error flags. Same encoding the LCD displays as decimal value (documented by manufacturer).'
+ attributes = INJECT_INTO_STATUS,HIDE
+ match {
+ difvifkey = 317F
+ }
+ lookup {
+ name = ERROR_FLAGS
+ map_type = BitToString
+ mask_bits = 0xff
+ default_message = OK
+ map {
+ name = MEMORY_ERROR
+ info = 'Error during access to the data storage. Action: replace device.'
+ value = 0x01
+ test = Set
+ }
+ map {
+ name = UNEXPECTED_RST
+ info = 'Unexpected reset (POR, RAM parity, or access violation). Action: replacement recommended.'
+ value = 0x02
+ test = Set
+ }
+ map {
+ name = ANNO_10_11
+ info = 'Lifetime annunciation: meter is in its 10th or 11th year of operation. Info only.'
+ value = 0x04
+ test = Set
+ }
+ map {
+ name = OPEN_C
+ info = 'Sabotage detection triggered: removal of the HCA from the heat conductor was detected. Info only.'
+ value = 0x08
+ test = Set
+ }
+ map {
+ name = SENSOR_BREAK
+ info = 'Break of one of the measurement sensors. Action: replace device.'
+ value = 0x10
+ test = Set
+ }
+ map {
+ name = SENSOR_SHORT
+ info = 'Short circuit of one of the measurement sensors. Action: replace device.'
+ value = 0x20
+ test = Set
+ }
+ map {
+ name = WDT
+ info = 'Reset through watchdog timer. Info only.'
+ value = 0x40
+ test = Set
+ }
+ map {
+ name = REMOTE_SENSOR
+ info = 'Remote sensor is connected. Info only.'
+ value = 0x80
+ test = Set
+ }
+ }
+ }
+ field {
+ name = current_consumption
+ quantity = HCA
+ info = 'DEPRECATED Cumulative heat cost allocation for current billing-period.'
+ deprecated_by = 'consumption_hca, update by 2026-12-01'
+ match {
+ measurement_type = Instantaneous
+ vif_range = HeatCostAllocation
+ }
+ }
+ field {
+ name = consumption_at_set_date_1
+ quantity = HCA
+ info = 'Replaced by target_hca. Heat cost allocation for the most recent billing-period.'
+ deprecated_by = 'target_hca update before 2026-12-01'
+ match {
+ measurement_type = Instantaneous
+ vif_range = HeatCostAllocation
+ storage_nr = 1
+ }
+ }
+ /* ----------- Half-monthly delta history -----------
+
+ # Telegram Data Layout
+
+ Storages 2..31 hold 30 differences between consecutive half-monthly snapshots (not absolute readings).
+
+ # Field Interpretation
+
+ NOTE: the snapshot ranges in the table below rest on multiple assumptions.
+ See the Assumptions and Verification section at the bottom of this comment.
+
+ Sum from the oldest forward for the cumulative reading at any half-month.
+
+ Across target_date the stored delta is negative because the counter resets
+ to 0 at the billing-period boundary. Summing through it still yields the
+ correct post-reset cumulative.
+
+ # Example (ActiveLong telegram)
+
+ Snapshot ranges below are LEFT..RIGHT, both edges inclusive.
+
+ Anchors from the ActiveLong test telegram:
+
+ half_month_history_start_date = 2025-02-15
+ target_date = 2025-12-31
+ target_hca = 373 (reading at target_date)
+ consumption_hca = 242 (reading at telegram time)
+ meter_datetime = 2026-05-06 12:04
+
+ Anchored cells are tagged with the source field. Other dates follow from the assumed cadence.
+
+ storage field delta cum. sum snapshot range
+ ------- ---------------------------- ----- -------- ----------------------------------------------------
+ 2 delta_30_half_months_ago_hca 173 173 2025-01-01..2025-02-15 half_month_history_start_date
+ 3 delta_29_half_months_ago_hca 34 207 2025-02-16..2025-02-28
+ 4 delta_28_half_months_ago_hca 12 219 2025-03-01..2025-03-15
+ 5 delta_27_half_months_ago_hca 26 245 2025-03-16..2025-03-31
+ 6 delta_26_half_months_ago_hca 5 250 2025-04-01..2025-04-15
+ 7 delta_25_half_months_ago_hca 0 250 2025-04-16..2025-04-30
+ 8..17 ............................ 0 250 2025-05-01..2025-09-30
+ 18 delta_14_half_months_ago_hca 0 250 2025-10-01..2025-10-15
+ 19 delta_13_half_months_ago_hca 12 262 2025-10-16..2025-10-31
+ 20 delta_12_half_months_ago_hca 13 275 2025-11-01..2025-11-15
+ 21 delta_11_half_months_ago_hca 34 309 2025-11-16..2025-11-30
+ 22 delta_10_half_months_ago_hca 41 350 2025-12-01..2025-12-15
+ 23 delta_9_half_months_ago_hca 23 373 target_hca 2025-12-16..2025-12-31 target_date
+
+ --------------------------------------------------RESET: End of Billing Period--------------------------------------------------
+
+ 24 delta_8_half_months_ago_hca -335 38 2026-01-01..2026-01-15
+ 25 delta_7_half_months_ago_hca 67 105 2026-01-16..2026-01-31
+ 26 delta_6_half_months_ago_hca 30 135 2026-02-01..2026-02-15
+ 27 delta_5_half_months_ago_hca 40 175 2026-02-16..2026-02-28
+ 28 delta_4_half_months_ago_hca 2 177 2026-03-01..2026-03-15
+ 29 delta_3_half_months_ago_hca 33 210 2026-03-16..2026-03-31
+ 30 delta_2_half_months_ago_hca 25 235 2026-04-01..2026-04-15
+ 31 delta_1_half_months_ago_hca 7 242 2026-04-16..2026-04-30
+ [live in-progress; not transmitted] x=0 242+x=242 consumption_hca 2026-05-01..2026-05-06 meter_datetime
+
+ # Assumptions and Verification
+
+ ## Verified
+
+ - sum(delta_30_half_months_ago_hca..delta_9_half_months_ago_hca) == target_hca (373)
+ - sum(delta_{1..30}_half_months_ago_hca) == consumption_hca (242)
+ - delta_8_half_months_ago_hca is large-negative (-335, i.e. ~ -target_hca):
+ meter resets to 0 at target_date and stores raw cumulative differences
+ across the reset. Rules out the alternative where the meter zeros its
+ offset and stores post-reset accumulation directly (which would give a
+ small non-negative value for delta_8_half_months_ago_hca).
+
+ ## Assumptions
+
+ - cadence is 15th / end-of-month: the only cadence where target_date
+ coincides naturally with a snapshot date, no special boundary handling.
+ Disambiguator: a captured half_month_history_start_date that doesn't
+ fall on the 15th or last day of a month rules out this cadence.
+ half_month_history_start_date is the only cadence-determined right
+ edge observable in the telegram. target_date is constrained by
+ billing period configuration, not by the half-monthly cadence.
+ - delta_1_half_months_ago_hca's right edge is the most recent
+ completed snapshot before meter_datetime, with the trailing partial period
+ as a separate live-bucket row. Alternative: delta_1_half_months_ago_hca
+ extends to meter_datetime, no live row.
+ Disambiguator: observing x = consumption_hca - sum(delta_{1..30}_half_months_ago_hca) > 0
+ in any capture. The assumed model permits x >= 0, the alternative
+ requires x == 0 always. Any x > 0 rules out the alternative.
+
+ See the TBD comment at the top of the tests block below for the captures
+ needed to settle these.
+
+ Note on meter configurability: hcae2 can be configured for monthly or annual
+ billing periods, and a separate setting toggles between continued metering
+ across billing periods and reset at end of billing period. The captures we
+ have are all from a meter configured for annual billing with end-of-period
+ reset (verified by the negative delta crossing target_date: see Verified
+ above). Whether (and how) either setting affects the wmbus telegram contents on
+ differently-configured meters has not been observed.
+ -------------------------------------------------- */
+ field {
+ name = half_month_history_start
+ quantity = PointInTime
+ info = 'EXPERIMENTAL: Date of the oldest half-monthly delta in the history series, anchors delta_30_half_months_ago_hca.'
+ // don't rely on this (yet), drop DEPRECATED once hcae2 is fully understood
+ display_unit = date
+ match {
+ measurement_type = Instantaneous
+ vif_range = Date
+ storage_nr = 2
+ }
+ }
+ field {
+ name = 'delta_{32counter-storage_counter}_half_months_ago'
+ quantity = HCA
+ // don't rely on this (yet), drop DEPRECATED once hcae2 is fully understood
+ info = 'EXPERIMENTAL: Difference between two consecutive half-monthly snapshots (not an absolute reading).
+ -- The fields delta_{1..30}_half_months_ago_hca together span >= 15 months.
+ -- delta_30_half_months_ago_hca may cover more than a half-month: its left edge anchors to the start of the billing cycle containing the oldest snapshot.
+ -- Sum from delta_30_half_months_ago_hca forward for the cumulative reading at any half-month.
+ -- The delta crossing target_date is large-negative due to the billing-period reset (sum still yields the correct post-reset cumulative).
+ -- See the hcae2 driver source for further information including meter-configuration caveats.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = HeatCostAllocation
+ storage_nr = 2,31
+ }
+ }
+ }
+ tests {
+ /* TBD: captures still needed to fully verify the half-monthly delta model
+ (see Assumptions and Verification in the block comment above).
+
+ 1. A corresponding capture for ActiveLong taken ~1 month later from the same
+ meter (annual billing with end-of-period reset, same config).
+ Required to disambiguate cadence (15th/EOM vs 1st/15th) and
+ delta_1_half_months_ago_hca's right-edge model: both shift between the two captures under exactly
+ one of each pair of alternatives, so the diff settles them. Replacing
+ ActiveLong with such a pair is also acceptable.
+
+ 2. Capture pairs from meters with the other configuration combinations:
+ - annual billing, no reset (continuous metering across periods)
+ - monthly billing with reset
+ - monthly billing, no reset
+ Required to confirm whether the post-target_date negative-delta and
+ cum-sum invariants generalise across the meter-configurability matrix
+ or are specific to the annual+reset case captured so far. */
+ test {
+ args = 'T1Short hcae2 54423117 NOKEY'
+ comment = 'Captured HCAe2 telegram, T1 mode, short format'
+ telegram = 2D46C5141731425431087A0A0B00002F2F046D24344535036E000000426C5E34436E000000317F1A346D00335B34
+ json = '{"_":"telegram","media":"heat cost allocation","driver":"hcae2","name":"T1Short","id":"54423117","consumption_hca":0,"consumption_at_set_date_1_hca":0,"consumption_at_set_date_1_hca_deprecated_by": "target_hca update before 2026-12-01","current_consumption_hca":0,"current_consumption_hca_deprecated_by": "consumption_hca, update by 2026-12-01","target_date":"2026-04-30","target_hca":0,"meter_datetime":"2026-05-05 20:36","meter_datetime_at_error":"2026-04-27 19:00","status":"ALARM OPEN_C PERMANENT_ERROR SENSOR_BREAK UNEXPECTED_RST","timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'T1Short;54423117;0;ALARM OPEN_C PERMANENT_ERROR SENSOR_BREAK UNEXPECTED_RST;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'T1Long hcae2 54423117 NOKEY'
+ comment = 'Captured EFE telegram, T1 mode, long format, full half-monthly delta history (all zero on this meter)'
+ telegram = C946C5141731425431087AD00B00002F2F046D14374535036E000000426C5E34436E00000082016C2F3283016E000000C2016E000082026E0000C2026E000082036E0000C2036E000082046E0000C2046E000082056E0000C2056E000082066E0000C2066E000082076E0000C2076E000082086E0000C2086E000082096E0000C2096E0000820A6E0000C20A6E0000820B6E0000C20B6E0000820C6E0000C20C6E0000820D6E0000C20D6E0000820E6E0000C20E6E0000820F6E0000C20F6E0000317F1A346D00335B34
+ json = '{"_":"telegram","media":"heat cost allocation","driver":"hcae2","name":"T1Long","id":"54423117","consumption_hca":0,"consumption_at_set_date_1_hca":0,"consumption_at_set_date_1_hca_deprecated_by": "target_hca update before 2026-12-01","delta_10_half_months_ago_hca":0,"delta_11_half_months_ago_hca":0,"delta_12_half_months_ago_hca":0,"delta_13_half_months_ago_hca":0,"delta_14_half_months_ago_hca":0,"delta_15_half_months_ago_hca":0,"delta_16_half_months_ago_hca":0,"delta_17_half_months_ago_hca":0,"delta_18_half_months_ago_hca":0,"delta_19_half_months_ago_hca":0,"delta_1_half_months_ago_hca":0,"delta_20_half_months_ago_hca":0,"delta_21_half_months_ago_hca":0,"delta_22_half_months_ago_hca":0,"delta_23_half_months_ago_hca":0,"delta_24_half_months_ago_hca":0,"delta_25_half_months_ago_hca":0,"delta_26_half_months_ago_hca":0,"delta_27_half_months_ago_hca":0,"delta_28_half_months_ago_hca":0,"delta_29_half_months_ago_hca":0,"delta_2_half_months_ago_hca":0,"delta_30_half_months_ago_hca":0,"delta_3_half_months_ago_hca":0,"delta_4_half_months_ago_hca":0,"delta_5_half_months_ago_hca":0,"delta_6_half_months_ago_hca":0,"delta_7_half_months_ago_hca":0,"delta_8_half_months_ago_hca":0,"delta_9_half_months_ago_hca":0,"current_consumption_hca":0,"current_consumption_hca_deprecated_by": "consumption_hca, update by 2026-12-01","half_month_history_start_date":"2025-02-15","target_date":"2026-04-30","target_hca":0,"meter_datetime":"2026-05-05 23:20","meter_datetime_at_error":"2026-04-27 19:00","status":"ALARM OPEN_C PERMANENT_ERROR SENSOR_BREAK UNEXPECTED_RST","timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'T1Long;54423117;0;ALARM OPEN_C PERMANENT_ERROR SENSOR_BREAK UNEXPECTED_RST;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'C1Short hcae2 54423117 NOKEY'
+ comment = 'Captured HCAe2 telegram, C1 mode, short format'
+ telegram = 2F46C5141731425431087A0A0B00002F2F046D28344535036E000000426C5E34436E000000317F1A346D00335B340186
+ json = '{"_":"telegram","media":"heat cost allocation","driver":"hcae2","name":"C1Short","id":"54423117","consumption_hca":0,"consumption_at_set_date_1_hca":0,"consumption_at_set_date_1_hca_deprecated_by": "target_hca update before 2026-12-01","current_consumption_hca":0,"current_consumption_hca_deprecated_by": "consumption_hca, update by 2026-12-01","target_date":"2026-04-30","target_hca":0,"meter_datetime":"2026-05-05 20:40","meter_datetime_at_error":"2026-04-27 19:00","status":"ALARM OPEN_C PERMANENT_ERROR SENSOR_BREAK UNEXPECTED_RST","timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'C1Short;54423117;0;ALARM OPEN_C PERMANENT_ERROR SENSOR_BREAK UNEXPECTED_RST;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'C1Long hcae2 54423117 NOKEY'
+ comment = 'Captured HCAe2 telegram, C1 mode, long format, full half-monthly delta history (all zero on this meter)'
+ telegram = CD46C5141731425431087A0A0B00002F2F046D2A344535036E000000426C5E34436E00000082016C2F3283016E000000C2016E000082026E0000C2026E000082036E0000C2036E000082046E0000C2046E000082056E0000C2056E000082066E0000C2066E000082076E0000C2076E000082086E0000C2086E000082096E15420000C2096E0000820A6E0000C20A6E0000820B6E0000C20B6E0000820C6E0000C20C6E0000820D6E0000C20D6E0000820E6E0000C20E6E0000820F6E0000C20F6E0000317F1A346D00335B34BB6D
+ json = '{"_":"telegram","media":"heat cost allocation","driver":"hcae2","name":"C1Long","id":"54423117","consumption_hca":0,"consumption_at_set_date_1_hca":0,"consumption_at_set_date_1_hca_deprecated_by": "target_hca update before 2026-12-01","delta_10_half_months_ago_hca":0,"delta_11_half_months_ago_hca":0,"delta_12_half_months_ago_hca":0,"delta_13_half_months_ago_hca":0,"delta_14_half_months_ago_hca":0,"delta_15_half_months_ago_hca":0,"delta_16_half_months_ago_hca":0,"delta_17_half_months_ago_hca":0,"delta_18_half_months_ago_hca":0,"delta_19_half_months_ago_hca":0,"delta_1_half_months_ago_hca":0,"delta_20_half_months_ago_hca":0,"delta_21_half_months_ago_hca":0,"delta_22_half_months_ago_hca":0,"delta_23_half_months_ago_hca":0,"delta_24_half_months_ago_hca":0,"delta_25_half_months_ago_hca":0,"delta_26_half_months_ago_hca":0,"delta_27_half_months_ago_hca":0,"delta_28_half_months_ago_hca":0,"delta_29_half_months_ago_hca":0,"delta_2_half_months_ago_hca":0,"delta_30_half_months_ago_hca":0,"delta_3_half_months_ago_hca":0,"delta_4_half_months_ago_hca":0,"delta_5_half_months_ago_hca":0,"delta_6_half_months_ago_hca":0,"delta_7_half_months_ago_hca":0,"delta_8_half_months_ago_hca":0,"delta_9_half_months_ago_hca":0,"current_consumption_hca":0,"current_consumption_hca_deprecated_by": "consumption_hca, update by 2026-12-01","half_month_history_start_date":"2025-02-15","target_date":"2026-04-30","target_hca":0,"meter_datetime":"2026-05-05 20:42","meter_datetime_at_error":"2026-04-27 19:00","status":"ALARM OPEN_C PERMANENT_ERROR SENSOR_BREAK UNEXPECTED_RST","timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'C1Long;54423117;0;ALARM OPEN_C PERMANENT_ERROR SENSOR_BREAK UNEXPECTED_RST;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'ActiveLong hcae2 23412960 NOKEY'
+ comment = 'Captured HCAe2 telegram, C1 mode, long format, full half-monthly delta history (real data, thanks Christoph!)'
+ telegram = CE44C5146029412331087A6E00C0252F2F046D042C4635036EF20000426C3F3C436E75010082016C2F3283016EAD0000C2016E220082026E0C00C2026E1A0082036E0500C2036E000082046E0000C2046E000082056E0000C2056E000082066E0000C2066E000082076E0000C2076E000082086E0000C2086E000082096E0000C2096E0C00820A6E0D00C20A6E2200820B6E2900C20B6E1700820C6EB1FEC20C6E4300820D6E1E00C20D6E2800820E6E0200C20E6E2100820F6E1900C20F6E0700317F00346D002000002F2F2F2F2F
+ json = '{"_":"telegram","media":"heat cost allocation","driver":"hcae2","name":"ActiveLong","id":"23412960","consumption_hca":242,"consumption_at_set_date_1_hca":373,"consumption_at_set_date_1_hca_deprecated_by": "target_hca update before 2026-12-01","delta_10_half_months_ago_hca":41,"delta_11_half_months_ago_hca":34,"delta_12_half_months_ago_hca":13,"delta_13_half_months_ago_hca":12,"delta_14_half_months_ago_hca":0,"delta_15_half_months_ago_hca":0,"delta_16_half_months_ago_hca":0,"delta_17_half_months_ago_hca":0,"delta_18_half_months_ago_hca":0,"delta_19_half_months_ago_hca":0,"delta_1_half_months_ago_hca":7,"delta_20_half_months_ago_hca":0,"delta_21_half_months_ago_hca":0,"delta_22_half_months_ago_hca":0,"delta_23_half_months_ago_hca":0,"delta_24_half_months_ago_hca":0,"delta_25_half_months_ago_hca":0,"delta_26_half_months_ago_hca":5,"delta_27_half_months_ago_hca":26,"delta_28_half_months_ago_hca":12,"delta_29_half_months_ago_hca":34,"delta_2_half_months_ago_hca":25,"delta_30_half_months_ago_hca":173,"delta_3_half_months_ago_hca":33,"delta_4_half_months_ago_hca":2,"delta_5_half_months_ago_hca":40,"delta_6_half_months_ago_hca":30,"delta_7_half_months_ago_hca":67,"delta_8_half_months_ago_hca":-335,"delta_9_half_months_ago_hca":23,"current_consumption_hca":242,"current_consumption_hca_deprecated_by": "consumption_hca, update by 2026-12-01","half_month_history_start_date":"2025-02-15","target_date":"2025-12-31","target_hca":373,"meter_datetime":"2026-05-06 12:04","meter_datetime_at_error":"2000-00-00 00:00","status":"OK","timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'ActiveLong;23412960;242;OK;1111-11-11 11:11.11'
+ }
+ }
+}
diff --git a/components/wmbus_common/drivers/src/hydrocalm3.xmq b/components/wmbus_common/drivers/src/hydrocalm3.xmq
new file mode 100644
index 00000000..8f83a98a
--- /dev/null
+++ b/components/wmbus_common/drivers/src/hydrocalm3.xmq
@@ -0,0 +1,123 @@
+// Copyright (C) 2018-2022 Fredrik Öhrström (gpl-3.0-or-later)
+driver {
+ name = hydrocalm3
+ meter_type = HeatMeter
+ default_fields = name,id,total_heating_kwh,total_cooling_kwh,timestamp
+ detect {
+ mvt = BMT,0b,0d
+ }
+ mfct_tpl_status_bits {
+ mask_bits = 0xe0
+ default_message = OK
+ map {
+ name = SABOTAGE_ENCLOSURE
+ value = 0x80
+ test = Set
+ }
+ }
+ fields {
+ field {
+ name = status
+ quantity = Text
+ info = 'Meter status from tpl status field.'
+ attributes = STATUS,INCLUDE_TPL_STATUS
+ }
+ field {
+ name = total_heating
+ quantity = Energy
+ info = 'The total heating energy consumption recorded by this meter.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyEnergyVIF
+ index_nr = 1
+ }
+ }
+ field {
+ name = device
+ quantity = PointInTime
+ info = 'The date time when the recording was made.'
+ display_unit = datetime
+ match {
+ measurement_type = Instantaneous
+ vif_range = DateTime
+ }
+ }
+ field {
+ name = total_cooling
+ quantity = Energy
+ info = 'The total cooling energy consumption recorded by this meter.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyEnergyVIF
+ index_nr = 2
+ }
+ }
+ field {
+ name = total_heating
+ quantity = Volume
+ info = 'Total heating volume of media.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = Volume
+ index_nr = 1
+ }
+ }
+ field {
+ name = total_cooling
+ quantity = Volume
+ info = 'Total cooling volume of media.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = Volume
+ index_nr = 2
+ }
+ }
+ field {
+ name = c1_volume
+ quantity = Volume
+ info = 'Supply c1 volume.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = Volume
+ index_nr = 3
+ }
+ }
+ field {
+ name = c2_volume
+ quantity = Volume
+ info = 'Return c2 volume.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = Volume
+ index_nr = 4
+ }
+ }
+ field {
+ name = supply_temperature
+ quantity = Temperature
+ info = 'The supply t1 pipe temperature.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = FlowTemperature
+ index_nr = 1
+ }
+ }
+ field {
+ name = return_temperature
+ quantity = Temperature
+ info = 'The return t2 pipe temperature.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = ReturnTemperature
+ }
+ }
+ }
+ tests {
+ test {
+ args = 'HeatCool hydrocalm3 71727374 NOKEY'
+ telegram = 8E44B409747372710B0D7A798080052F2F_0C0E59600100046D1D36B9290C13679947000C0E000000000C13590000000C13000000000C13000000000A5A18020A5E11020F823D06003D06003D06003D0600140600620500480400E402001601000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002F2F
+ json = '{"_":"telegram","media":"heat/cooling load","driver":"hydrocalm3","name":"HeatCool","id":"71727374","status":"SABOTAGE_ENCLOSURE","total_heating_kwh":4460.833333,"total_cooling_kwh":0,"device_datetime":"2021-09-25 22:29","total_heating_m3":479.967,"total_cooling_m3":0.059,"c1_volume_m3":0,"c2_volume_m3":0,"supply_temperature_c":21.8,"return_temperature_c":21.1,"timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'HeatCool;71727374;4460.833333;0;1111-11-11 11:11.11'
+ }
+ }
+}
diff --git a/components/wmbus_common/drivers/src/hydrocalm4.xmq b/components/wmbus_common/drivers/src/hydrocalm4.xmq
index a866ecfc..cc56d4ee 100644
--- a/components/wmbus_common/drivers/src/hydrocalm4.xmq
+++ b/components/wmbus_common/drivers/src/hydrocalm4.xmq
@@ -8,9 +8,9 @@ driver {
}
fields {
field {
- name = status
- quantity = Text
- info = status_and_error_flags
+ name = status
+ quantity = Text
+ info = status_and_error_flags
attributes = INCLUDE_TPL_STATUS
match {
measurement_type = Instantaneous
@@ -23,7 +23,7 @@ driver {
default_message = OK
map {
name = SABOTAGE
- info = 'SABOTAGE_ENCLOSURE'
+ info = SABOTAGE_ENCLOSURE
value = 0x80
test = Set
}
@@ -55,60 +55,60 @@ driver {
match {
measurement_type = Instantaneous
vif_range = AnyEnergyVIF
- subunit_nr = 0
+ subunit_nr = 0
tariff_nr = 1
storage_nr = 0
}
}
field {
- name = total_heating
- info = 'Total heating volume of media.'
- quantity = Volume
+ name = total_heating
+ info = 'Total heating volume of media.'
+ quantity = Volume
match {
measurement_type = Instantaneous
vif_range = Volume
}
}
field {
- name = total_cooling
- info = 'Total cooling volume of media.'
- quantity = Volume
+ name = total_cooling
+ info = 'Total cooling volume of media.'
+ quantity = Volume
match {
measurement_type = Instantaneous
vif_range = Volume
- subunit_nr = 0
+ subunit_nr = 0
tariff_nr = 1
storage_nr = 0
}
}
field {
- name = c1_volume
- info = 'Supply c1 volume.'
- quantity = Volume
+ name = c1_volume
+ info = 'Supply c1 volume.'
+ quantity = Volume
match {
measurement_type = Instantaneous
vif_range = Volume
- subunit_nr = 1
+ subunit_nr = 1
tariff_nr = 0
storage_nr = 0
}
}
field {
- name = c2_volume
- info = 'Supply c2 volume.'
- quantity = Volume
+ name = c2_volume
+ info = 'Supply c2 volume.'
+ quantity = Volume
match {
measurement_type = Instantaneous
vif_range = Volume
- subunit_nr = 2
+ subunit_nr = 2
tariff_nr = 0
storage_nr = 0
}
}
field {
- name = supply_temperature
- info = 'The supply t1 pipe temperature.'
- quantity = Temperature
+ name = supply_temperature
+ info = 'The supply t1 pipe temperature.'
+ quantity = Temperature
match {
measurement_type = Instantaneous
vif_range = FlowTemperature
@@ -116,9 +116,9 @@ driver {
}
}
field {
- name = return_temperature
- info = 'The supply t2 pipe temperature.'
- quantity = Temperature
+ name = return_temperature
+ info = 'The supply t2 pipe temperature.'
+ quantity = Temperature
match {
measurement_type = Instantaneous
vif_range = ReturnTemperature
@@ -146,39 +146,38 @@ driver {
tests {
test {
args = 'testm4 hydrocalm4 05171338 NOKEY'
- comment = 'Standard-heat'
+ comment = Standard-heat
telegram = 2C44B409381317051A0D8C00497A76000000_046D25AA153A0C03000000000C13000000000F6400000000000000
- json = '{"_":"telegram","media":"heat/cooling load","meter":"hydrocalm4","status":"OK","name":"testm4","id":"05171338","device_datetime":"2024-10-21 10:37","total_heating_kwh":0,"total_heating_m3":0,"timestamp":"1111-11-11T11:11:11Z"}'
+ json = '{"_":"telegram","media":"heat/cooling load","driver":"hydrocalm4","status":"OK","name":"testm4","id":"05171338","device_datetime":"2024-10-21 10:37","total_heating_kwh":0,"total_heating_m3":0,"timestamp":"1111-11-11T11:11:11Z"}'
fields = 'testm4;05171338;OK;0;null;1111-11-11 11:11.11'
}
test {
args = 'testm4 hydrocalm4 05171338 NOKEY'
- comment = 'Combined-heat_cool'
+ comment = Combined-heat_cool
telegram = 3A44B409381317051A0D8C00497A7A000000_046D29AA153A0C03000000000C13000000008C1003050000008C1013040000000F6401000000000000
- json = '{"_":"telegram","media":"heat/cooling load","meter":"hydrocalm4","name":"testm4","id":"05171338","device_datetime":"2024-10-21 10:41","total_cooling_kwh":0.005,"total_cooling_m3":0.004,"total_heating_kwh":0,"total_heating_m3":0,"status":"OK","timestamp":"1111-11-11T11:11:11Z"}'
+ json = '{"_":"telegram","media":"heat/cooling load","driver":"hydrocalm4","name":"testm4","id":"05171338","device_datetime":"2024-10-21 10:41","total_cooling_kwh":0.005,"total_cooling_m3":0.004,"total_heating_kwh":0,"total_heating_m3":0,"status":"OK","timestamp":"1111-11-11T11:11:11Z"}'
fields = 'testm4;05171338;OK;0;0.005;1111-11-11 11:11.11'
}
test {
args = 'testm4 hydrocalm4 05171338 NOKEY'
- comment = 'Instantaneous-heat_temperature'
+ comment = Instantaneous-heat_temperature
telegram = 3E44B409381317051A0D8C00497A7C000000_046D2BAA153A0C03000000000C13000000000B3B0000000B280000000A5930230A5D08250F6402000000000000
- json = '{"_":"telegram","media":"heat/cooling load","meter":"hydrocalm4","name":"testm4","id":"05171338","device_datetime":"2024-10-21 10:43","power_kw": 0, "return_temperature_c":25.08,"supply_temperature_c":23.3,"total_heating_kwh":0,"total_heating_m3":0,"volume_flow_m3h":0,"status":"OK","timestamp":"1111-11-11T11:11:11Z"}'
+ json = '{"_":"telegram","media":"heat/cooling load","driver":"hydrocalm4","name":"testm4","id":"05171338","device_datetime":"2024-10-21 10:43","power_kw": 0, "return_temperature_c":25.08,"supply_temperature_c":23.3,"total_heating_kwh":0,"total_heating_m3":0,"volume_flow_m3h":0,"status":"OK","timestamp":"1111-11-11T11:11:11Z"}'
fields = 'testm4;05171338;OK;0;null;1111-11-11 11:11.11'
}
test {
args = 'testm4 hydrocalm4 05171338 NOKEY'
- comment = 'Instantaneous-heat_pulses'
+ comment = Instantaneous-heat_pulses
telegram = 3B44B409381317051A0D8C00497A7E000000_046D2DAA153A0C03000000000C13000000008C4013999900008C804013888800000F6403000000000000
- json = '{"_":"telegram","c1_volume_m3": 9.999,"c2_volume_m3": 8.888,"device_datetime": "2024-10-21 10:45","id": "05171338","media": "heat/cooling load","meter": "hydrocalm4","name": "testm4","status": "OK","timestamp": "1111-11-11T11:11:11Z","total_heating_kwh": 0,"total_heating_m3": 0}'
+ json = '{"_":"telegram","c1_volume_m3": 9.999,"c2_volume_m3": 8.888,"device_datetime": "2024-10-21 10:45","id": "05171338","media": "heat/cooling load","driver": "hydrocalm4","name": "testm4","status": "OK","timestamp": "1111-11-11T11:11:11Z","total_heating_kwh": 0,"total_heating_m3": 0}'
fields = 'testm4;05171338;OK;0;null;1111-11-11 11:11.11'
}
test {
args = 'testm5 hydrocalm4 05128041 NOKEY'
comment = 'Power unit J/h'
telegram = 3e44B409418012051a0d8c20f17a9d000020046d0d3126310c0a481878330c13098405000B3B0000000B301000000a5929360a5d94230f6402000000000000
- json = '{"_": "telegram","device_datetime": "2025-01-06 17:13","id": "05128041","media": "heat/cooling load","meter": "hydrocalm4","name": "testm5","power_kw": 3e-06,"return_temperature_c": 23.94,"status": "OK","supply_temperature_c": 36.29,"timestamp": "1111-11-11T11:11:11Z","total_heating_kwh": 938.384667,"total_heating_m3": 58.409,"volume_flow_m3h": 0}'
+ json = '{"_": "telegram","device_datetime": "2025-01-06 17:13","id": "05128041","media": "heat/cooling load","driver": "hydrocalm4","name": "testm5","power_kw": 3e-06,"return_temperature_c": 23.94,"status": "OK","supply_temperature_c": 36.29,"timestamp": "1111-11-11T11:11:11Z","total_heating_kwh": 938.384667,"total_heating_m3": 58.409,"volume_flow_m3h": 0}'
fields = 'testm5;05128041;OK;938.384667;null;1111-11-11 11:11.11'
}
-
}
}
diff --git a/components/wmbus_common/drivers/src/hydroclimav2.xmq b/components/wmbus_common/drivers/src/hydroclimav2.xmq
new file mode 100644
index 00000000..db57cee0
--- /dev/null
+++ b/components/wmbus_common/drivers/src/hydroclimav2.xmq
@@ -0,0 +1,331 @@
+// Copyright (C) 2026 Fredrik Öhrström (gpl-3.0-or-later)
+driver {
+ name = hydroclimav2
+ meter_type = HeatCostAllocationMeter
+ default_fields = name,id,status,consumption_hca,average_ambient_temperature_c,timestamp
+ manufacturer = BMeters
+ detect {
+ mvt = BMP,53,08
+ mvt = BMP,85,08
+ }
+ // The manufacturer bits of the TPL status byte mirror the event groups of
+ // the STS status word (the user manual labels events A0, A1, B8, C9-C15
+ // by group): 0x40 = group B (tamper), 0x80 = group C (device errors),
+ // 0x20 = group A (configuration, presumed by the same scheme). Verified
+ // against decrypted telegrams: TPL 0x40 <-> word bit 8, TPL 0x80 <-> word
+ // bit 9, TPL 0xC0 <-> bits 8+9. The detailed event is in the STS word
+ // below; the group flags still matter because the TPL byte travels
+ // unencrypted.
+ mfct_tpl_status_bits {
+ mask_bits = 0xe0
+ default_message = OK
+ map {
+ name = EVENT_GROUP_A
+ value = 0x20
+ test = set
+ }
+ map {
+ name = EVENT_GROUP_B
+ value = 0x40
+ test = set
+ }
+ map {
+ name = EVENT_GROUP_C
+ value = 0x80
+ test = set
+ }
+ }
+ library {
+ use = consumption_hca
+ use = target_hca
+ use = target_date
+ }
+ fields {
+ field {
+ name = status
+ quantity = Text
+ info = 'Status and error flags'
+ attributes = STATUS,INCLUDE_TPL_STATUS
+ match {
+ difvifkey = 02FD17
+ }
+ lookup {
+ name = ERROR_FLAGS
+ map_type = BitToString
+ mask_bits = 0xffff
+ default_message = OK
+ // STS status word bits per the Hydroclima 2 user manual v1.1
+ // (Alarms table): events A0, A1, B8, C9, C10, C11, C13 and
+ // C15 are bit positions in this word. Bits 2-7, 12 and 14 are
+ // not documented and stay unnamed. TAMPERING latches until a
+ // seal is applied after configuration; NOT_CONFIGURED clears
+ // after configuration; OSCILLATOR_ERROR clears after a
+ // date/time update.
+ map {
+ name = NOT_CONFIGURED
+ value = 0x0001
+ test = Set
+ }
+ map {
+ name = VERIFICATION_PENDING
+ value = 0x0002
+ test = Set
+ }
+ map {
+ name = TAMPERING
+ value = 0x0100
+ test = Set
+ }
+ map {
+ name = HEATER_TEMPERATURE_OUT_OF_RANGE
+ value = 0x0200
+ test = Set
+ }
+ map {
+ name = HEATER_TEMPERATURE_LOW
+ value = 0x0400
+ test = Set
+ }
+ map {
+ name = CONSUMPTION_OVERFLOW
+ value = 0x0800
+ test = Set
+ }
+ map {
+ name = OSCILLATOR_ERROR
+ value = 0x2000
+ test = Set
+ }
+ map {
+ name = POWER_FAILURE
+ value = 0x8000
+ test = Set
+ }
+ }
+ }
+ field {
+ name = average_ambient_temperature
+ quantity = Temperature
+ match {
+ measurement_type = Instantaneous
+ vif_range = ExternalTemperature
+ }
+ }
+ field {
+ name = max_ambient_temperature
+ quantity = Temperature
+ match {
+ measurement_type = Maximum
+ vif_range = ExternalTemperature
+ }
+ }
+ field {
+ name = average_heater_temperature
+ quantity = Temperature
+ match {
+ measurement_type = Instantaneous
+ vif_range = FlowTemperature
+ }
+ }
+ field {
+ name = average_ambient_temperature_last_month
+ quantity = Temperature
+ match {
+ measurement_type = Instantaneous
+ vif_range = ExternalTemperature
+ storage_nr = 1
+ }
+ }
+ field {
+ name = average_heater_temperature_last_month
+ quantity = Temperature
+ match {
+ measurement_type = Instantaneous
+ vif_range = FlowTemperature
+ storage_nr = 1
+ }
+ }
+ field {
+ name = consumption_at_set_date_1
+ quantity = HCA
+ match {
+ measurement_type = Instantaneous
+ vif_range = HeatCostAllocation
+ storage_nr = 2
+ }
+ }
+ field {
+ name = consumption_at_set_date_2
+ quantity = HCA
+ match {
+ measurement_type = Instantaneous
+ vif_range = HeatCostAllocation
+ storage_nr = 3
+ }
+ }
+ field {
+ name = consumption_at_set_date_3
+ quantity = HCA
+ match {
+ measurement_type = Instantaneous
+ vif_range = HeatCostAllocation
+ storage_nr = 4
+ }
+ }
+ field {
+ name = consumption_at_set_date_4
+ quantity = HCA
+ match {
+ measurement_type = Instantaneous
+ vif_range = HeatCostAllocation
+ storage_nr = 5
+ }
+ }
+ field {
+ name = ambient_temperature_at_set_date_1
+ quantity = Temperature
+ null_value = -327.68
+ match {
+ measurement_type = Instantaneous
+ vif_range = ExternalTemperature
+ storage_nr = 2
+ }
+ }
+ field {
+ name = ambient_temperature_at_set_date_2
+ quantity = Temperature
+ null_value = -327.68
+ match {
+ measurement_type = Instantaneous
+ vif_range = ExternalTemperature
+ storage_nr = 3
+ }
+ }
+ field {
+ name = ambient_temperature_at_set_date_3
+ quantity = Temperature
+ null_value = -327.68
+ match {
+ measurement_type = Instantaneous
+ vif_range = ExternalTemperature
+ storage_nr = 4
+ }
+ }
+ field {
+ name = total_consumption
+ quantity = HCA
+ match {
+ measurement_type = Instantaneous
+ vif_range = HeatCostAllocation
+ storage_nr = 10
+ }
+ }
+ field {
+ name = mfct_specific_data
+ quantity = Text
+ attributes = HIDE
+ match {
+ difvifkey = 0F
+ }
+ // An IXML grammar for decoding hydroclima telegrams.
+ ixml = "decode = frame_v10 | frame_v11.
+ frame_v10 = '10',
+ StatusWord,
+ Time,
+ Date,
+ AverageAmbient,
+ MaxAmbient,
+ MaxAmbientDate,
+ NumMeasurements,
+ AverageAmbientPrev,
+ AverageHeaterPrev,
+ IU,
+ TotalIU.
+ frame_v11 = '11',
+ StatusWord, { STS Status word }
+ Time, { TIM Current time }
+ Date, { DAT Current date }
+ OpenDate, { DOP Housing open event date }
+ AverageHeater, { TKA Average heater temperature (current period) }
+ AverageAmbient, { TOA Average ambient temperature (current period) }
+ MaxAmbient, { TMH1 Max temperature (previous period) }
+ MaxAmbientDate, { TMHD1 Date of max temperature }
+ AverageHeaterPrev, { TKA1 Average heater temperature (previous period) }
+ AverageAmbientPrev, { TOA1 Average ambient temperature (previous period) }
+ CNI1, { CNI1 Consumption history LIFO 0 }
+ CNI2, { CNI2 Consumption history LIFO 1 }
+ CNI3, { CNI3 Consumption history LIFO 2 }
+ CNI4, { CNI4 Consumption history LIFO 3 }
+ TONI1, { TONI1 Ambient temp history LIFO 0 }
+ TONI2, { TONI2 Ambient temp history LIFO 1 }
+ TONI3, { TONI3 Ambient temp history LIFO 2 }
+ TempCounters, { TK Temperature bin counters }
+ TotalU. { U Total consumption all periods }
+ StatusWord = word, @DV_status.
+ Time = word.
+ Date = word.
+ OpenDate = word.
+ AverageHeater = word, @DV_flow_temp.
+ AverageAmbient = word, @DV_ext_temp.
+ MaxAmbient = word, @DV_max_ext_temp.
+ MaxAmbientDate = word.
+ AverageHeaterPrev = word, @DV_flow_temp_s1.
+ AverageAmbientPrev = word, @DV_ext_temp_s1.
+ CNI1 = word, @DV_hca_s2.
+ CNI2 = word, @DV_hca_s3.
+ CNI3 = word, @DV_hca_s4.
+ CNI4 = word, @DV_hca_s5.
+ TONI1 = word, @DV_ext_temp_s2.
+ TONI2 = word, @DV_ext_temp_s3.
+ TONI3 = word, @DV_ext_temp_s4.
+ TempCounters = triplet, triplet, triplet.
+ TotalU = triplet, @DV_hca_total.
+ IU = word.
+ TotalIU = triplet, @DV_hca_total.
+ NumMeasurements = word.
+ -hex = ['A'-'F';'0'-'9'].
+ -byte = hex, hex.
+ -word = byte, byte.
+ -triplet = byte, byte, byte.
+ -quad = byte, byte, byte, byte.
+
+ DV_status>dvk = +'02FD17'. { int16 STS status word, surfaced as raw error flags }
+ DV_ext_temp>dvk = +'0265'. { int16 External temperature 10⁻² °C }
+ DV_flow_temp>dvk = +'0259'. { int16 Flow temperature 10⁻² °C }
+ DV_max_ext_temp>dvk = +'1265'. { max int16 External temperature 10⁻² °C }
+ DV_ext_temp_s1>dvk = +'4265'. { storage 1 int16 External temperature 10⁻² °C }
+ DV_flow_temp_s1>dvk = +'4259'. { storage 1 int16 Flow temperature 10⁻² °C }
+ DV_hca_s2>dvk = +'82016E'. { storage 2 int16 HCA }
+ DV_hca_s3>dvk = +'C2016E'. { storage 3 int16 HCA }
+ DV_hca_s4>dvk = +'82026E'. { storage 4 int16 HCA }
+ DV_hca_s5>dvk = +'C2026E'. { storage 5 int16 HCA }
+ DV_ext_temp_s2>dvk = +'820165'. { storage 2 int16 External temperature 10⁻² °C }
+ DV_ext_temp_s3>dvk = +'C20165'. { storage 3 int16 External temperature 10⁻² °C }
+ DV_ext_temp_s4>dvk = +'820265'. { storage 4 int16 External temperature 10⁻² °C }
+ DV_hca_total>dvk = +'83056E'. { storage 10 int24 HCA total }"
+ }
+ }
+ tests {
+ test {
+ comment = 'Version 0x53 frame_v10 NOKEY'
+ args = 'HCA hydroclimav2 68036198 NOKEY'
+ telegram = 2e44b0099861036853087a000020002F2F036E0000000F100043106A7D2C4A078F12202CB1242A06D3062100210000
+ json = '{"_":"telegram","media":"heat cost allocation","driver":"hydroclimav2","name":"HCA","id":"68036198","average_ambient_temperature_c":18.66,"average_ambient_temperature_last_month_c":15.78,"average_heater_temperature_last_month_c":17.47,"consumption_hca":0,"max_ambient_temperature_c":47.51,"total_consumption_hca":33,"status":"ERROR_FLAGS_4000 HEATER_TEMPERATURE_OUT_OF_RANGE TAMPERING","timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'HCA;68036198;ERROR_FLAGS_4000 HEATER_TEMPERATURE_OUT_OF_RANGE TAMPERING;0;18.66;1111-11-11 11:11.11'
+ }
+ test {
+ comment = 'Version 0x85 frame_v11 encrypted'
+ args = 'HC1 hydroclimav2 93000952 NOKEY'
+ telegram = 5144B0095209009385088C20807A800040252F2F_036E540200436ED90800426C3F3C0F1100009924293400003713DF08BC163633960E8208D401D90879076006DA080809DA08E537000A3500DF1201830900
+ json = '{"_":"telegram","media":"heat cost allocation","driver":"hydroclimav2","name":"HC1","id":"93000952","ambient_temperature_at_set_date_1_c":22.66,"ambient_temperature_at_set_date_2_c":23.12,"ambient_temperature_at_set_date_3_c":22.66,"average_ambient_temperature_c":22.71,"average_ambient_temperature_last_month_c":21.78,"average_heater_temperature_c":49.19,"average_heater_temperature_last_month_c":37.34,"consumption_at_set_date_1_hca":468,"consumption_at_set_date_2_hca":2265,"consumption_at_set_date_3_hca":1913,"consumption_at_set_date_4_hca":1632,"consumption_hca":596,"max_ambient_temperature_c":58.2,"target_date":"2025-12-31","target_hca":2265,"total_consumption_hca":2435,"status":"OK","timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'HC1;93000952;OK;596;22.71;1111-11-11 11:11.11'
+ }
+ test {
+ comment = 'Version 0x85 frame_v11 with TONI null sentinel 0x8000'
+ args = 'HC3 hydroclimav2 93001021 NOKEY'
+ telegram = 5144b0092110009385088c20fb7afb4040252F2F_036E000000436E000000426C3F350F110001B9604734CE2E2708FD071B088832940766070000000000000000C407260800802DD400000000000000220000
+ json = '{"_":"telegram","media":"heat cost allocation","driver":"hydroclimav2","name":"HC3","id":"93001021","ambient_temperature_at_set_date_1_c":19.88,"ambient_temperature_at_set_date_2_c":20.86,"ambient_temperature_at_set_date_3_c":null,"average_ambient_temperature_c":20.45,"average_ambient_temperature_last_month_c":18.94,"average_heater_temperature_c":20.87,"average_heater_temperature_last_month_c":19.4,"consumption_at_set_date_1_hca":0,"consumption_at_set_date_2_hca":0,"consumption_at_set_date_3_hca":0,"consumption_at_set_date_4_hca":0,"consumption_hca":0,"max_ambient_temperature_c":20.75,"target_date":"2025-05-31","target_hca":0,"total_consumption_hca":34,"status":"EVENT_GROUP_B TAMPERING","timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'HC3;93001021;EVENT_GROUP_B TAMPERING;0;20.45;1111-11-11 11:11.11'
+ }
+ }
+}
diff --git a/components/wmbus_common/drivers/src/hydrodigit.xmq b/components/wmbus_common/drivers/src/hydrodigit.xmq
new file mode 100644
index 00000000..c8def9ae
--- /dev/null
+++ b/components/wmbus_common/drivers/src/hydrodigit.xmq
@@ -0,0 +1,624 @@
+/* Copyright (C) 2019-2026 Fredrik Öhrström (gpl-3.0-or-later)
+ Copyright (C) 2025 Karel Blavka (gpl-3.0-or-later) */
+driver {
+ name = hydrodigit
+ meter_type = WaterMeter
+ default_fields = name,id,total_m3,meter_datetime,timestamp
+ detect {
+ mvt = BMT,13,06
+ mvt = BMT,17,06
+ mvt = BMT,13,07
+ mvt = BMT,15,07
+ mvt = BMT,17,07
+ }
+ fields {
+ field {
+ name = total
+ quantity = Volume
+ info = 'The total water consumption recorded by this meter.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = Volume
+ }
+ }
+ field {
+ name = meter
+ quantity = PointInTime
+ display_unit = datetime
+ info = 'Meter timestamp for measurement.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = DateTime
+ }
+ }
+ field {
+ name = contents
+ quantity = Text
+ info = 'Contents of this telegram'
+ match {
+ difvifkey = 01FB05
+ }
+ lookup {
+ name = CONTENTS
+ map_type = BitToString
+ mask_bits = 0x97
+ default_message = ''
+ map {
+ name = BATTERY_VOLTAGE
+ value = 0x01
+ test = Set
+ }
+ map {
+ name = FRAUD_DATE
+ value = 0x02
+ test = Set
+ }
+ map {
+ name = LEAK_DATE
+ value = 0x80
+ test = Set
+ }
+ map {
+ name = BACKFLOW
+ value = 0x04
+ test = Set
+ }
+ map {
+ name = MONTHLY_DATA
+ value = 0x10
+ test = Set
+ }
+ }
+ }
+ field {
+ name = fraud_type
+ quantity = Text
+ info = 'Type of fraud detected by the meter'
+ match {
+ difvifkey = 01FE0F
+ }
+ lookup {
+ name = FRAUD_TYPE
+ map_type = BitToString
+ mask_bits = 0xE0
+ default_message = NO_TYPE_INFO
+ map {
+ name = MAGNETIC_FRAUD_ATTEMPT
+ value = 0x80
+ test = Set
+ }
+ map {
+ name = SENSOR_FRAUD_ATTEMPT
+ value = 0x40
+ test = Set
+ }
+ map {
+ name = MODULE_REMOVED
+ value = 0x20
+ test = Set
+ }
+ }
+ }
+ field {
+ name = battery_raw
+ quantity = Dimensionless
+ attributes = HIDE
+ vif_scaling = None
+ dif_signedness = Unsigned
+ match {
+ difvifkey = 01FB01
+ }
+ }
+ field {
+ name = battery_nibble
+ quantity = Dimensionless
+ attributes = HIDE
+ calculate = 'battery_raw_counter % 16counter'
+ }
+ field {
+ name = voltage
+ quantity = Voltage
+ info = 'Voltage of the battery inside the meter'
+ calculate = '(
+ ((battery_nibble_counter == 1counter) * 1.9counter) +
+ ((battery_nibble_counter == 2counter) * 2.1counter) +
+ ((battery_nibble_counter == 3counter) * 2.2counter) +
+ ((battery_nibble_counter == 4counter) * 2.3counter) +
+ ((battery_nibble_counter == 5counter) * 2.4counter) +
+ ((battery_nibble_counter == 6counter) * 2.5counter) +
+ ((battery_nibble_counter == 7counter) * 2.65counter) +
+ ((battery_nibble_counter == 8counter) * 2.8counter) +
+ ((battery_nibble_counter == 9counter) * 2.9counter) +
+ ((battery_nibble_counter == 10counter) * 3.05counter) +
+ ((battery_nibble_counter == 11counter) * 3.2counter) +
+ ((battery_nibble_counter == 12counter) * 3.35counter) +
+ ((battery_nibble_counter == 13counter) * 3.5counter) +
+ (((battery_nibble_counter == 0counter) || (battery_nibble_counter == 14counter) || (battery_nibble_counter == 15counter)) * 3.7counter)
+ ) * 1v'
+ }
+ field {
+ name = fraud_date_raw
+ quantity = Dimensionless
+ attributes = HIDE
+ vif_scaling = None
+ dif_signedness = Unsigned
+ match {
+ difvifkey = 03FB02
+ }
+ }
+ field {
+ name = fraud
+ quantity = PointInTime
+ display_unit = date
+ info = 'Date of fraud detected by the meter'
+ match {
+ difvifkey = 03FB02
+ }
+ calculate = 'mkdate(
+ 2000counter + (((fraud_date_raw_counter & 255counter) >> 4counter) * 10counter) + (fraud_date_raw_counter & 15counter),
+ (fraud_date_raw_counter >> 8counter) & 15counter,
+ (((fraud_date_raw_counter >> 16counter) & 255counter) >> 4counter) * 10counter + ((fraud_date_raw_counter >> 16counter) & 15counter)
+ )'
+ }
+ field {
+ name = leak_date_raw
+ quantity = Dimensionless
+ attributes = HIDE
+ vif_scaling = None
+ dif_signedness = Unsigned
+ match {
+ difvifkey = 03FB03
+ }
+ }
+ field {
+ name = leak
+ quantity = PointInTime
+ display_unit = date
+ info = 'Date of leakage detected by meter'
+ match {
+ difvifkey = 03FB03
+ }
+ calculate = 'mkdate(
+ 2000counter + (((leak_date_raw_counter & 255counter) >> 4counter) * 10counter) + (leak_date_raw_counter & 15counter),
+ (((leak_date_raw_counter >> 8counter) & 255counter) >> 4counter) * 10counter + ((leak_date_raw_counter >> 8counter) & 15counter),
+ (((leak_date_raw_counter >> 16counter) & 255counter) >> 4counter) * 10counter + ((leak_date_raw_counter >> 16counter) & 15counter)
+ )'
+ }
+ field {
+ name = backflow_raw
+ quantity = Dimensionless
+ attributes = HIDE
+ vif_scaling = None
+ dif_signedness = Unsigned
+ match {
+ difvifkey = 04FB04
+ }
+ }
+ field {
+ name = backflow
+ quantity = Volume
+ info = 'Backflow detected by the meter'
+ match {
+ difvifkey = 04FB04
+ }
+ calculate = 'backflow_raw_counter / 1000counter * 1m3'
+ }
+ field {
+ name = january_total_raw
+ quantity = Dimensionless
+ attributes = HIDE
+ vif_scaling = None
+ dif_signedness = Unsigned
+ match {
+ difvifkey = 03FB10
+ }
+ }
+ field {
+ name = January_total
+ quantity = Volume
+ info = 'Total value at the end of January'
+ match {
+ difvifkey = 03FB10
+ }
+ calculate = '((january_total_raw_counter < 10000000counter) * (january_total_raw_counter / 100counter)) * 1m3'
+ }
+ field {
+ name = february_total_raw
+ quantity = Dimensionless
+ attributes = HIDE
+ vif_scaling = None
+ dif_signedness = Unsigned
+ match {
+ difvifkey = 03FB11
+ }
+ }
+ field {
+ name = February_total
+ quantity = Volume
+ info = 'Total value at the end of February'
+ match {
+ difvifkey = 03FB11
+ }
+ calculate = '((february_total_raw_counter < 10000000counter) * (february_total_raw_counter / 100counter)) * 1m3'
+ }
+ field {
+ name = march_total_raw
+ quantity = Dimensionless
+ attributes = HIDE
+ vif_scaling = None
+ dif_signedness = Unsigned
+ match {
+ difvifkey = 03FB12
+ }
+ }
+ field {
+ name = March_total
+ quantity = Volume
+ info = 'Total value at the end of March'
+ match {
+ difvifkey = 03FB12
+ }
+ calculate = '((march_total_raw_counter < 10000000counter) * (march_total_raw_counter / 100counter)) * 1m3'
+ }
+ field {
+ name = april_total_raw
+ quantity = Dimensionless
+ attributes = HIDE
+ vif_scaling = None
+ dif_signedness = Unsigned
+ match {
+ difvifkey = 03FB13
+ }
+ }
+ field {
+ name = April_total
+ quantity = Volume
+ info = 'Total value at the end of April'
+ match {
+ difvifkey = 03FB13
+ }
+ calculate = '((april_total_raw_counter < 10000000counter) * (april_total_raw_counter / 100counter)) * 1m3'
+ }
+ field {
+ name = may_total_raw
+ quantity = Dimensionless
+ attributes = HIDE
+ vif_scaling = None
+ dif_signedness = Unsigned
+ match {
+ difvifkey = 03FB14
+ }
+ }
+ field {
+ name = May_total
+ quantity = Volume
+ info = 'Total value at the end of May'
+ match {
+ difvifkey = 03FB14
+ }
+ calculate = '((may_total_raw_counter < 10000000counter) * (may_total_raw_counter / 100counter)) * 1m3'
+ }
+ field {
+ name = june_total_raw
+ quantity = Dimensionless
+ attributes = HIDE
+ vif_scaling = None
+ dif_signedness = Unsigned
+ match {
+ difvifkey = 03FB15
+ }
+ }
+ field {
+ name = June_total
+ quantity = Volume
+ info = 'Total value at the end of June'
+ match {
+ difvifkey = 03FB15
+ }
+ calculate = '((june_total_raw_counter < 10000000counter) * (june_total_raw_counter / 100counter)) * 1m3'
+ }
+ field {
+ name = july_total_raw
+ quantity = Dimensionless
+ attributes = HIDE
+ vif_scaling = None
+ dif_signedness = Unsigned
+ match {
+ difvifkey = 03FB16
+ }
+ }
+ field {
+ name = July_total
+ quantity = Volume
+ info = 'Total value at the end of July'
+ match {
+ difvifkey = 03FB16
+ }
+ calculate = '((july_total_raw_counter < 10000000counter) * (july_total_raw_counter / 100counter)) * 1m3'
+ }
+ field {
+ name = august_total_raw
+ quantity = Dimensionless
+ attributes = HIDE
+ vif_scaling = None
+ dif_signedness = Unsigned
+ match {
+ difvifkey = 03FB17
+ }
+ }
+ field {
+ name = August_total
+ quantity = Volume
+ info = 'Total value at the end of August'
+ match {
+ difvifkey = 03FB17
+ }
+ calculate = '((august_total_raw_counter < 10000000counter) * (august_total_raw_counter / 100counter)) * 1m3'
+ }
+ field {
+ name = september_total_raw
+ quantity = Dimensionless
+ attributes = HIDE
+ vif_scaling = None
+ dif_signedness = Unsigned
+ match {
+ difvifkey = 03FB18
+ }
+ }
+ field {
+ name = September_total
+ quantity = Volume
+ info = 'Total value at the end of September'
+ match {
+ difvifkey = 03FB18
+ }
+ calculate = '((september_total_raw_counter < 10000000counter) * (september_total_raw_counter / 100counter)) * 1m3'
+ }
+ field {
+ name = october_total_raw
+ quantity = Dimensionless
+ attributes = HIDE
+ vif_scaling = None
+ dif_signedness = Unsigned
+ match {
+ difvifkey = 03FB19
+ }
+ }
+ field {
+ name = October_total
+ quantity = Volume
+ info = 'Total value at the end of October'
+ match {
+ difvifkey = 03FB19
+ }
+ calculate = '((october_total_raw_counter < 10000000counter) * (october_total_raw_counter / 100counter)) * 1m3'
+ }
+ field {
+ name = november_total_raw
+ quantity = Dimensionless
+ attributes = HIDE
+ vif_scaling = None
+ dif_signedness = Unsigned
+ match {
+ difvifkey = 03FB1A
+ }
+ }
+ field {
+ name = November_total
+ quantity = Volume
+ info = 'Total value at the end of November'
+ match {
+ difvifkey = 03FB1A
+ }
+ calculate = '((november_total_raw_counter < 10000000counter) * (november_total_raw_counter / 100counter)) * 1m3'
+ }
+ field {
+ name = december_total_raw
+ quantity = Dimensionless
+ attributes = HIDE
+ vif_scaling = None
+ dif_signedness = Unsigned
+ match {
+ difvifkey = 03FB1B
+ }
+ }
+ field {
+ name = December_total
+ quantity = Volume
+ info = 'Total value at the end of December'
+ match {
+ difvifkey = 03FB1B
+ }
+ calculate = '((december_total_raw_counter < 10000000counter) * (december_total_raw_counter / 100counter)) * 1m3'
+ }
+ field {
+ name = mfct_specific_data
+ quantity = Text
+ attributes = HIDE
+ match {
+ difvifkey = 0F
+ }
+ ixml = "decode =
+ frame_00 |
+ frame_01 |
+ frame_02 |
+ frame_03 |
+ frame_04 |
+ frame_05 |
+ frame_06 |
+ frame_07 |
+ frame_10 |
+ frame_11 |
+ frame_12 |
+ frame_13 |
+ frame_14 |
+ frame_15 |
+ frame_16 |
+ frame_17 |
+ frame_80 |
+ frame_81 |
+ frame_82 |
+ frame_83 |
+ frame_84 |
+ frame_85 |
+ frame_86 |
+ frame_87 |
+ frame_90 |
+ frame_91 |
+ frame_92 |
+ frame_93 |
+ frame_94 |
+ frame_95 |
+ frame_96 |
+ frame_97.
+ frame_00 = contents_00, byte*.
+ frame_01 = contents_01, Battery, byte*.
+ frame_02 = contents_02, Fraud, byte*.
+ frame_03 = contents_03, Battery, Fraud, byte*.
+ frame_04 = contents_04, Backflow, byte*.
+ frame_05 = contents_05, Battery, Backflow, byte*.
+ frame_06 = contents_06, Fraud, Backflow, byte*.
+ frame_07 = contents_07, Battery, Fraud, Backflow, byte*.
+ frame_10 = contents_10, Monthly, byte*.
+ frame_11 = contents_11, Battery, Monthly, byte*.
+ frame_12 = contents_12, Fraud, Monthly, byte*.
+ frame_13 = contents_13, Battery, Fraud, Monthly, byte*.
+ frame_14 = contents_14, Backflow, Monthly, byte*.
+ frame_15 = contents_15, Battery, Backflow, Monthly, byte*.
+ frame_16 = contents_16, Fraud, Backflow, Monthly, byte*.
+ frame_17 = contents_17, Battery, Fraud, Backflow, Monthly, byte*.
+ frame_80 = contents_80, Leak, byte*.
+ frame_81 = contents_81, Battery, Leak, byte*.
+ frame_82 = contents_82, Fraud, Leak, byte*.
+ frame_83 = contents_83, Battery, Fraud, Leak, byte*.
+ frame_84 = contents_84, Leak, Backflow, byte*.
+ frame_85 = contents_85, Battery, Leak, Backflow, byte*.
+ frame_86 = contents_86, Fraud, Leak, Backflow, byte*.
+ frame_87 = contents_87, Battery, Fraud, Leak, Backflow, byte*.
+ frame_90 = contents_90, Leak, Monthly, byte*.
+ frame_91 = contents_91, Battery, Leak, Monthly, byte*.
+ frame_92 = contents_92, Fraud, Leak, Monthly, byte*.
+ frame_93 = contents_93, Battery, Fraud, Leak, Monthly, byte*.
+ frame_94 = contents_94, Leak, Backflow, Monthly, byte*.
+ frame_95 = contents_95, Battery, Leak, Backflow, Monthly, byte*.
+ frame_96 = contents_96, Fraud, Leak, Backflow, Monthly, byte*.
+ frame_97 = contents_97, Battery, Fraud, Leak, Backflow, Monthly, byte*.
+ contents_00 = ('00' | '08' | '20' | '28' | '40' | '48' | '60' | '68'), @DV_content_indicator.
+ contents_01 = ('01' | '09' | '21' | '29' | '41' | '49' | '61' | '69'), @DV_content_indicator.
+ contents_02 = ('02' | '0A' | '22' | '2A' | '42' | '4A' | '62' | '6A'), @DV_content_indicator.
+ contents_03 = ('03' | '0B' | '23' | '2B' | '43' | '4B' | '63' | '6B'), @DV_content_indicator.
+ contents_04 = ('04' | '0C' | '24' | '2C' | '44' | '4C' | '64' | '6C'), @DV_content_indicator.
+ contents_05 = ('05' | '0D' | '25' | '2D' | '45' | '4D' | '65' | '6D'), @DV_content_indicator.
+ contents_06 = ('06' | '0E' | '26' | '2E' | '46' | '4E' | '66' | '6E'), @DV_content_indicator.
+ contents_07 = ('07' | '0F' | '27' | '2F' | '47' | '4F' | '67' | '6F'), @DV_content_indicator.
+ contents_10 = ('10' | '18' | '30' | '38' | '50' | '58' | '70' | '78'), @DV_content_indicator.
+ contents_11 = ('11' | '19' | '31' | '39' | '51' | '59' | '71' | '79'), @DV_content_indicator.
+ contents_12 = ('12' | '1A' | '32' | '3A' | '52' | '5A' | '72' | '7A'), @DV_content_indicator.
+ contents_13 = ('13' | '1B' | '33' | '3B' | '53' | '5B' | '73' | '7B'), @DV_content_indicator.
+ contents_14 = ('14' | '1C' | '34' | '3C' | '54' | '5C' | '74' | '7C'), @DV_content_indicator.
+ contents_15 = ('15' | '1D' | '35' | '3D' | '55' | '5D' | '75' | '7D'), @DV_content_indicator.
+ contents_16 = ('16' | '1E' | '36' | '3E' | '56' | '5E' | '76' | '7E'), @DV_content_indicator.
+ contents_17 = ('17' | '1F' | '37' | '3F' | '57' | '5F' | '77' | '7F'), @DV_content_indicator.
+ contents_80 = ('80' | '88' | 'A0' | 'A8' | 'C0' | 'C8' | 'E0' | 'E8'), @DV_content_indicator.
+ contents_81 = ('81' | '89' | 'A1' | 'A9' | 'C1' | 'C9' | 'E1' | 'E9'), @DV_content_indicator.
+ contents_82 = ('82' | '8A' | 'A2' | 'AA' | 'C2' | 'CA' | 'E2' | 'EA'), @DV_content_indicator.
+ contents_83 = ('83' | '8B' | 'A3' | 'AB' | 'C3' | 'CB' | 'E3' | 'EB'), @DV_content_indicator.
+ contents_84 = ('84' | '8C' | 'A4' | 'AC' | 'C4' | 'CC' | 'E4' | 'EC'), @DV_content_indicator.
+ contents_85 = ('85' | '8D' | 'A5' | 'AD' | 'C5' | 'CD' | 'E5' | 'ED'), @DV_content_indicator.
+ contents_86 = ('86' | '8E' | 'A6' | 'AE' | 'C6' | 'CE' | 'E6' | 'EE'), @DV_content_indicator.
+ contents_87 = ('87' | '8F' | 'A7' | 'AF' | 'C7' | 'CF' | 'E7' | 'EF'), @DV_content_indicator.
+ contents_90 = ('90' | '98' | 'B0' | 'B8' | 'D0' | 'D8' | 'F0' | 'F8'), @DV_content_indicator.
+ contents_91 = ('91' | '99' | 'B1' | 'B9' | 'D1' | 'D9' | 'F1' | 'F9'), @DV_content_indicator.
+ contents_92 = ('92' | '9A' | 'B2' | 'BA' | 'D2' | 'DA' | 'F2' | 'FA'), @DV_content_indicator.
+ contents_93 = ('93' | '9B' | 'B3' | 'BB' | 'D3' | 'DB' | 'F3' | 'FB'), @DV_content_indicator.
+ contents_94 = ('94' | '9C' | 'B4' | 'BC' | 'D4' | 'DC' | 'F4' | 'FC'), @DV_content_indicator.
+ contents_95 = ('95' | '9D' | 'B5' | 'BD' | 'D5' | 'DD' | 'F5' | 'FD'), @DV_content_indicator.
+ contents_96 = ('96' | '9E' | 'B6' | 'BE' | 'D6' | 'DE' | 'F6' | 'FE'), @DV_content_indicator.
+ contents_97 = ('97' | '9F' | 'B7' | 'BF' | 'D7' | 'DF' | 'F7' | 'FF'), @DV_content_indicator.
+ Battery = byte, @DV_battery_raw.
+ Fraud = fraud_tm, fraud_date.
+ fraud_tm = byte, @DV_fraud_type.
+ fraud_date = triplet, @DV_fraud_date_raw.
+ Leak = triplet, @DV_leak_date_raw.
+ Backflow = quad, @DV_backflow_raw.
+ Monthly = January, February, March, April, May, June, July, August, September, October, November, December.
+ January = triplet, @DV_january_total_raw.
+ February = triplet, @DV_february_total_raw.
+ March = triplet, @DV_march_total_raw.
+ April = triplet, @DV_april_total_raw.
+ May = triplet, @DV_may_total_raw.
+ June = triplet, @DV_june_total_raw.
+ July = triplet, @DV_july_total_raw.
+ August = triplet, @DV_august_total_raw.
+ September = triplet, @DV_september_total_raw.
+ October = triplet, @DV_october_total_raw.
+ November = triplet, @DV_november_total_raw.
+ December = triplet, @DV_december_total_raw.
+
+ -hex = ['A'-'F';'0'-'9'].
+ -byte = hex, hex.
+ -triplet = byte, byte, byte.
+ -quad = byte, byte, byte, byte.
+
+ DV_content_indicator>dvk = +'01FB05'.
+ DV_fraud_type>dvk = +'01FE0F'.
+ DV_battery_raw>dvk = +'01FB01'.
+ DV_fraud_date_raw>dvk = +'03FB02'.
+ DV_leak_date_raw>dvk = +'03FB03'.
+ DV_backflow_raw>dvk = +'04FB04'.
+ DV_january_total_raw>dvk = +'03FB10'.
+ DV_february_total_raw>dvk = +'03FB11'.
+ DV_march_total_raw>dvk = +'03FB12'.
+ DV_april_total_raw>dvk = +'03FB13'.
+ DV_may_total_raw>dvk = +'03FB14'.
+ DV_june_total_raw>dvk = +'03FB15'.
+ DV_july_total_raw>dvk = +'03FB16'.
+ DV_august_total_raw>dvk = +'03FB17'.
+ DV_september_total_raw>dvk = +'03FB18'.
+ DV_october_total_raw>dvk = +'03FB19'.
+ DV_november_total_raw>dvk = +'03FB1A'.
+ DV_december_total_raw>dvk = +'03FB1B'."
+ }
+ }
+ tests {
+ test {
+ args = 'HydrodigitWater hydrodigit 86868686 NOKEY'
+ telegram = 4E44B4098686868613077AF0004005_2F2F0C1366380000046D27287E2A0F150E00000000C10000D10000E60000FD00000C01002F0100410100540100680100890000A00000B30000002F2F2F2F2F2F
+ json = '{"_":"telegram","April_total_m3":2.53,"August_total_m3":3.4,"December_total_m3":1.79,"February_total_m3":2.09,"January_total_m3":1.93,"July_total_m3":3.21,"June_total_m3":3.03,"March_total_m3":2.3,"May_total_m3":2.68,"November_total_m3":1.6,"October_total_m3":1.37,"September_total_m3":3.6,"backflow_m3":0,"contents":"BACKFLOW BATTERY_VOLTAGE MONTHLY_DATA","id":"86868686","media":"water","driver":"hydrodigit","meter_datetime":"2019-10-30 08:39","name":"HydrodigitWater","timestamp":"1111-11-11T11:11:11Z","total_m3":3.866,"voltage_v":3.7}'
+ fields = 'HydrodigitWater;86868686;3.866;2019-10-30 08:39;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'HydridigitWaterr hydrodigit 03245501 NOKEY'
+ telegram = 2444B4090155240317068C00487AC0000000_0C1335670000046D172EEA280F030000000000
+ json = '{"_":"telegram","contents":"BATTERY_VOLTAGE FRAUD_DATE","fraud_date":null,"fraud_type":"NO_TYPE_INFO","id":"03245501","media":"warm water","driver":"hydrodigit","meter_datetime":"2023-08-10 14:23","name":"HydridigitWaterr","timestamp":"1111-11-11T11:11:11Z","total_m3":6.735,"voltage_v":3.7}'
+ fields = 'HydridigitWaterr;03245501;6.735;2023-08-10 14:23;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'Hydro3 hydrodigit 87654321 NOKEY'
+ comment = 'This is a nice one to showcase the backflow encoding.'
+ telegram = 4644B4092143658713077A9C000000_0C1364390400_046D212F1635_0F152A0F000000440F00C00F00511000D51000B20B00180C007C0C00E60C00560D00D10D00400E00C60E0000
+ json = '{"_":"telegram","April_total_m3":43.09,"August_total_m3":33.02,"December_total_m3":37.82,"February_total_m3":40.32,"January_total_m3":39.08,"July_total_m3":31.96,"June_total_m3":30.96,"March_total_m3":41.77,"May_total_m3":29.94,"November_total_m3":36.48,"October_total_m3":35.37,"September_total_m3":34.14,"backflow_m3":0.015,"contents":"BACKFLOW BATTERY_VOLTAGE MONTHLY_DATA","id":"87654321","media":"water","driver":"hydrodigit","meter_datetime":"2024-05-22 15:33","name":"Hydro3","timestamp":"1111-11-11T11:11:11Z","total_m3":43.964,"voltage_v":3.05}'
+ fields = 'Hydro3;87654321;43.964;2024-05-22 15:33;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'Hydro4 hydrodigit 87654322 NOKEY'
+ comment = 'This one adds a leak date to the definition, plus shows how the monthly data looks before module installation.'
+ telegram = 4944B4092243658713077A7F000000_0C1363020400_046D242C1236_0F950A24042507000000A405006E0700850900CA0B004A0E00FFFFFFFFFFFF020000020000250000B3010095030000
+ json = '{"_":"telegram","April_total_m3":30.18,"August_total_m3":0.02,"December_total_m3":9.17,"February_total_m3":19.02,"January_total_m3":14.44,"July_total_m3":0,"June_total_m3":0,"March_total_m3":24.37,"May_total_m3":36.58,"November_total_m3":4.35,"October_total_m3":0.37,"September_total_m3":0.02,"backflow_m3":0.007,"contents":"BACKFLOW BATTERY_VOLTAGE LEAK_DATE MONTHLY_DATA","id":"87654322","leak_date":"2024-04-25","media":"water","driver":"hydrodigit","meter_datetime":"2024-06-18 12:36","name":"Hydro4","timestamp":"1111-11-11T11:11:11Z","total_m3":40.263,"voltage_v":3.05}'
+ fields = 'Hydro4;87654322;40.263;2024-06-18 12:36;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'HydrodigitWaterrr hydrodigit 23746391 NOKEY'
+ comment = 'This one has a backflow and a leak date, but no monthly data.'
+ telegram = 2144B4099163742315077A400000000C1399999999046D092A30340F050B01000000
+ json = '{"_":"telegram","backflow_m3":0.001,"contents":"BACKFLOW BATTERY_VOLTAGE","id":"23746391","media":"water","driver":"hydrodigit","meter_datetime":"2025-04-16 10:09","name":"HydrodigitWaterrr","timestamp":"1111-11-11T11:11:11Z","total_m3":99999.999,"voltage_v":3.2}'
+ fields = 'HydrodigitWaterrr;23746391;99999.999;2025-04-16 10:09;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'hydro7 hydrodigit 03122061 NOKEY'
+ telegram = 4C44B4096120120317077AB90000000C1330000000046D132E3E360F8F000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
+ json = '{"_":"telegram","backflow_m3":0,"contents":"BACKFLOW BATTERY_VOLTAGE FRAUD_DATE LEAK_DATE","fraud_date":null,"fraud_type":"NO_TYPE_INFO","id":"03122061","leak_date":null,"media":"water","driver":"hydrodigit","meter_datetime":"2025-06-30 14:19","name":"hydro7","timestamp":"1111-11-11T11:11:11Z","total_m3":0.03,"voltage_v":3.7}'
+ fields = 'hydro7;03122061;0.03;2025-06-30 14:19;1111-11-11 11:11.11'
+ }
+ }
+}
diff --git a/components/wmbus_common/drivers/src/hydrus.xmq b/components/wmbus_common/drivers/src/hydrus.xmq
new file mode 100644
index 00000000..f3d3bfb1
--- /dev/null
+++ b/components/wmbus_common/drivers/src/hydrus.xmq
@@ -0,0 +1,224 @@
+// Copyright (C) 2019-2026 Fredrik Öhrström (gpl-3.0-or-later)
+// Copyright (C) 2021 Vincent Privat (gpl-3.0-or-later)
+// Copyright (C) 2026 Magnus Friskytt (gpl-3.0-or-later)
+driver {
+ name = hydrus
+ meter_type = WaterMeter
+ default_fields = name,id,total_m3,total_at_date_m3,status,timestamp
+ detect {
+ mvt = DME,70,07
+ mvt = DME,76,07
+ mvt = HYD,24,07
+ mvt = HYD,8b,07
+ mvt = HYD,8b,06
+ mvt = DME,70,06
+ mvt = DME,70,16
+ }
+ library {
+ use = operating_time_h
+ use = actuality_duration_s
+ use = meter_datetime
+ use = customer
+ use = flow_temperature_c
+ use = external_temperature_c
+ use = total_m3
+ }
+ fields {
+ field {
+ name = status
+ quantity = Text
+ info = 'Status of meter.'
+ attributes = STATUS,INCLUDE_TPL_STATUS
+ match {
+ measurement_type = Instantaneous
+ vif_range = ErrorFlags
+ }
+ lookup {
+ name = ERROR_FLAGS
+ map_type = BitToString
+ mask_bits = 0xffff
+ default_message = OK
+ }
+ }
+ field {
+ name = 'total_tariff{tariff_counter}'
+ quantity = Volume
+ info = 'The total water consumption recorded on tariff # by this meter.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = Volume
+ tariff_nr = 1,2
+ }
+ }
+ field {
+ name = 'total_tariff{tariff_counter}_at_date'
+ quantity = Volume
+ info = 'The total water consumption recorded on tariff # by this meter at billing date.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = Volume
+ storage_nr = 1
+ tariff_nr = 1,2
+ }
+ }
+ field {
+ name = total_at_date
+ quantity = Volume
+ info = 'The total water consumption recorded at date.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = Volume
+ storage_nr = 1
+ }
+ }
+ field {
+ name = at
+ quantity = PointInTime
+ info = 'The last billing period date.'
+ display_unit = date
+ match {
+ measurement_type = Instantaneous
+ vif_range = Date
+ storage_nr = 1
+ }
+ }
+ field {
+ name = target
+ quantity = Volume
+ info = 'The total water consumption recorded at the end of last month.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = Volume
+ storage_nr = 3
+ }
+ }
+ field {
+ name = target
+ quantity = PointInTime
+ info = 'The end of last month.'
+ display_unit = datetime
+ match {
+ measurement_type = Instantaneous
+ vif_range = DateTime
+ storage_nr = 3
+ }
+ }
+ field {
+ name = remaining_battery_life
+ quantity = Time
+ info = 'Remaining battery life in years.'
+ display_unit = y
+ match {
+ measurement_type = Instantaneous
+ vif_range = RemainingBattery
+ }
+ }
+ field {
+ name = flow
+ quantity = Flow
+ info = 'The current water flow.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = VolumeFlow
+ }
+ }
+ field {
+ name = backward
+ quantity = Volume
+ info = 'Total reverse / backward water volume.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = Volume
+ add_combinable = BackwardFlow
+ }
+ }
+ field {
+ name = min_flow
+ quantity = Flow
+ info = 'Minimum recorded water flow during period.'
+ match {
+ measurement_type = Minimum
+ vif_range = VolumeFlow
+ storage_nr = 8
+ }
+ }
+ field {
+ name = max_flow
+ quantity = Flow
+ info = 'Maximum recorded water flow during period.'
+ match {
+ measurement_type = Maximum
+ vif_range = VolumeFlow
+ storage_nr = 8
+ }
+ }
+ field {
+ name = target
+ quantity = PointInTime
+ info = 'Historical billing/target date.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = DateTime
+ storage_nr = 8
+ }
+ }
+ field {
+ name = target
+ quantity = Volume
+ info = 'Total water consumption recorded at target date.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = Volume
+ storage_nr = 8
+ }
+ }
+ }
+ tests {
+ test {
+ args = 'HydrusWater hydrus 64646464 NOKEY'
+ telegram = 4E44A5116464646470077AED004005_2F2F01FD08300C13741100007C1300000000FC101300000000FC201300000000726C00000B3B00000002FD748713025A6800C4016D3B177F2ACC011300020000
+ json = '{"_":"telegram","media":"water","driver":"hydrus","name":"HydrusWater","id":"64646464","total_m3":1.174,"flow_m3h":0,"flow_temperature_c":10.4,"remaining_battery_life_y":13.686797,"status":"OK","target_datetime":"2019-10-31 23:59","target_m3": 0.2,"timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'HydrusWater;64646464;1.174;null;OK;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'HydrusVater hydrus 65656565 NOKEY'
+ telegram = 3E44A5116565656570067AFB0030052F2F_0C13503400000DFD110A383731303134423032410B3B00000002FD74DC15C4016D3B178D29CC0113313400002F2F
+ json = '{"_":"telegram","media":"warm water","driver":"hydrus","name":"HydrusVater","id":"65656565","flow_m3h":0,"customer": "A20B410178","total_m3":3.45,"remaining_battery_life_y":15.321328,"target_datetime":"2020-09-13 23:59","target_m3": 3.431,"status":"OK","timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'HydrusVater;65656565;3.45;null;OK;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'HydrusAES hydrus 64745666 NOKEY'
+ telegram = 6644242328001081640E7266567464A51170071F0050052C411A08674048DD6BA82A0DF79FFD401309179A893A1BE3CE8EDC50C2A45CD7AFEC3B4CE765820BE8056C124A17416C3722985FFFF7FCEB7094901AB3A16294B511B9A740C9F9911352B42A72FB3B0C
+ json = '{"_":"telegram","media":"water","driver":"hydrus","name":"HydrusAES","id":"64745666","total_m3":137.291,"total_tariff1_m3":0,"total_tariff2_m3":137.291,"flow_m3h":0,"flow_temperature_c":24.5,"external_temperature_c":23.9,"total_at_date_m3":128.638,"total_tariff1_at_date_m3":0,"total_tariff2_at_date_m3":128.638,"at_date":"2020-12-31","actuality_duration_s":6673,"operating_time_h":14678,"meter_datetime": "2021-01-23 08:27","status":"OK","timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'HydrusAES;64745666;137.291;128.638;OK;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'HydrusIzarRS hydrus 60897379 NOKEY'
+ comment = 'Hydrus IZAR RS 868 water meter'
+ telegram = 1E4424238B07797389607A8F00107D_041312170100426CBF23441344100100
+ json = '{"_":"telegram","media":"water","driver":"hydrus","name":"HydrusIzarRS","id":"60897379","total_m3":71.442,"at_date": "2021-03-31","total_at_date_m3":69.7,"status":"OK","timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'HydrusIzarRS;60897379;71.442;69.7;OK;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'HydrusIzarRSWarm hydrus 60904720 NOKEY'
+ comment = 'Hydrus IZAR RS 868 water meter warm'
+ telegram = 1E4424238B06204790607A2A0010D8_0413DDC00000426CBF23441382BB0000
+ json = '{"_":"telegram","media":"warm water","driver":"hydrus","name":"HydrusIzarRSWarm","id":"60904720","total_m3":49.373,"total_at_date_m3":48.002,"at_date":"2021-03-31","status":"OK","timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'HydrusIzarRSWarm;60904720;49.373;48.002;OK;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'HydrusFoo hydrus 64641820 NOKEY'
+ comment = 'Negative power values.'
+ telegram = 6344A5112018646470078C00D7900F002C256AB59B00F0F13032019092DE7A6A004007102F2F0C13896729004C1323462400CC101300000000CC201323462400426CDF2C0B3B0200F002FD742F0D025AC100C4016D3B17FE29CC01132841290001FD089F
+ json = '{"_":"telegram","at_date": "2022-12-31","target_datetime": "2023-09-30 23:59","flow_m3h": -0.002,"flow_temperature_c": 19.3,"id": "64641820","media": "water","driver": "hydrus","name": "HydrusFoo","remaining_battery_life_y": 9.240436,"status": "OK","timestamp": "1111-11-11T11:11:11Z","total_at_date_m3":244.623,"target_m3": 294.128,"total_m3": 296.789,"total_tariff1_at_date_m3": 0,"total_tariff2_at_date_m3": 244.623}'
+ fields = 'HydrusFoo;64641820;296.789;244.623;OK;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'Hydrus20 hydrus 86715606 NOKEY'
+ comment = 'Diehl Hydrus 2.0 full telegram'
+ telegram = 6344A5110656718676078C00AE900F002C2593B30F00610CCB29615532257A99004107102F2F_0C13921920000B3B0000000A5A96010A6622020C933C0000000084046D3B375F388C041341102000AB043B0000009B043B72170004FD17000000002F2F2F
+ json = '{"_":"telegram","media":"water","driver":"hydrus","name":"Hydrus20","id":"86715606","backward_m3":0,"external_temperature_c":22.2,"flow_m3h":0,"flow_temperature_c":19.6,"max_flow_m3h":1.772,"min_flow_m3h":0,"status":"OK","target_datetime":"2026-08-31 23:59","target_m3":201.041,"timestamp":"1111-11-11T11:11:11Z","total_m3":201.992}'
+ fields = 'Hydrus20;86715606;201.992;null;OK;1111-11-11 11:11.11'
+ }
+ }
+}
diff --git a/components/wmbus_common/drivers/src/iem3000.xmq b/components/wmbus_common/drivers/src/iem3000.xmq
new file mode 100644
index 00000000..1e14c7df
--- /dev/null
+++ b/components/wmbus_common/drivers/src/iem3000.xmq
@@ -0,0 +1,1751 @@
+// Copyright (C) 2024 Fredrik Öhrström (gpl-3.0-or-later)
+driver {
+ name = iem3000
+ meter_type = ElectricityMeter
+ default_fields = name,id,total_energy_consumption_kwh,timestamp
+ detect {
+ mvt = SEC,13,02
+ mvt = SEC,15,02
+ mvt = SEC,18,02
+ }
+ library {
+ use = firmware_version
+ use = manufacturer
+ use = meter_datetime
+ use = model_version
+ }
+ fields {
+ field {
+ name = status
+ quantity = Text
+ info = 'Status and error flags.'
+ attributes = STATUS,INCLUDE_TPL_STATUS
+ }
+ field {
+ name = error_flags
+ quantity = Text
+ info = 'Error flags.'
+ attributes = INJECT_INTO_STATUS
+ match {
+ measurement_type = Instantaneous
+ vif_range = ErrorFlags
+ }
+ lookup {
+ name = ERROR_FLAGS
+ map_type = IndexToString
+ mask_bits = 0xffffff
+ default_message = ''
+ map {
+ name = CODE_101_EEPROM_ERROR
+ value = 0x000000
+ test = Set
+ }
+ map {
+ name = CODE_102_NO_CALIBRATION_TABLE
+ value = 0x000010
+ test = Set
+ }
+ map {
+ name = CODE_201_MISMATCH_BETWEEN_FREQUENCY_SETTINGS_AND_FREQUENCY_MEASUREMENTS
+ value = 0x000020
+ test = Set
+ }
+ map {
+ name = CODE_202_PHASE_SEQUENCE_REVERSED
+ value = 0x000030
+ test = Set
+ }
+ map {
+ name = CODE_203_PHASE_SEQUENCE_REVERSED
+ value = 0x000040
+ test = Set
+ }
+ map {
+ name = CODE_204_TOTAL_ACTIVE_ENERGY_NEGATIVE_DUE_TO_INCORRECT_V_OR_A_CONNECTIONS
+ value = 0x000050
+ test = Set
+ }
+ map {
+ name = CODE_205_DATE_TIME_RESET_DUE_TO_POWER_FAILUER
+ value = 0x000060
+ test = Set
+ }
+ map {
+ name = CODE_206_PULSE_MISSING_DUE_TO_OVERSPEED_OF_ENERGY_PULSE_OUTPUT
+ value = 0x000070
+ test = Set
+ }
+ map {
+ name = CODE_207_ABNORMAL_INTERNAL_CLOCK_FUNCTION
+ value = 0x000080
+ test = Set
+ }
+ map {
+ name = INTERNAL_DATA_BUS_COMUNICATION_ERROR
+ value = 0x000090
+ test = Set
+ }
+ }
+ }
+ field {
+ name = total_energy_consumption
+ quantity = Energy
+ info = 'Total cumulative active imported energy.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ display_unit = kwh
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyEnergyVIF
+ }
+ }
+ field {
+ name = partial_energy_consumption
+ quantity = Energy
+ info = 'Partial cumulative active imported energy.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyEnergyVIF
+ add_combinable_raw = 0x7f0d
+ }
+ }
+ field {
+ name = partial_reactive_energy_consumption
+ quantity = Reactive_Energy
+ info = 'Partial cumulative reactive imported energy.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ display_unit = kvarh
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyEnergyVIF
+ subunit_nr = 1
+ add_combinable_raw = 0x7f0d
+ }
+ }
+ field {
+ name = 'total_energy_consumption_tariff_{tariff_counter}'
+ quantity = Energy
+ info = 'Total cumulative active imported energy per tariff.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyEnergyVIF
+ tariff_nr = 1,4
+ }
+ }
+ field {
+ name = total_energy_production
+ quantity = Energy
+ info = 'Total cumulative active exported energy.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyEnergyVIF
+ add_combinable_raw = 0x7f09
+ }
+ }
+ field {
+ name = 'total_energy_production_tariff_{tariff_counter}'
+ quantity = Energy
+ info = 'Total cumulative active exported energy per tariff.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyEnergyVIF
+ tariff_nr = 1,4
+ subunit_nr = 1
+ }
+ }
+ field {
+ name = active_tariff
+ quantity = Dimensionless
+ info = 'Active tariff.'
+ vif_scaling = None
+ dif_signedness = Signed
+ display_unit = nr
+ match {
+ difvifkey = 03FF10
+ }
+ }
+ field {
+ name = error_flags
+ quantity = Text
+ info = 'Error flags.'
+ attributes = INJECT_INTO_STATUS
+ match {
+ measurement_type = Instantaneous
+ difvifkey = 07FFA600
+ }
+ lookup {
+ name = ERROR_FLAGS
+ map_type = BitToString
+ mask_bits = 0xffffffffffffffff
+ default_message = OK
+ }
+ }
+ field {
+ name = warning_flags
+ quantity = Text
+ info = 'Warning flags.'
+ attributes = INJECT_INTO_STATUS
+ match {
+ measurement_type = Instantaneous
+ difvifkey = 07FFA700
+ }
+ lookup {
+ name = WARNING_FLAGS
+ map_type = BitToString
+ mask_bits = 0xffffffffffffffff
+ default_message = OK
+ }
+ }
+ field {
+ name = information_flags
+ quantity = Text
+ info = 'Information flags.'
+ match {
+ measurement_type = Instantaneous
+ difvifkey = 07FFA800
+ }
+ lookup {
+ name = INFORMATION_FLAGS
+ map_type = BitToString
+ mask_bits = 0xffffffffffffffff
+ default_message = ''
+ }
+ }
+ field {
+ name = alarm_flags
+ quantity = Text
+ info = 'alarm flags.'
+ attributes = INJECT_INTO_STATUS
+ match {
+ measurement_type = Instantaneous
+ difvifkey = 07FFA900
+ }
+ lookup {
+ name = ALARM_FLAGS
+ map_type = BitToString
+ mask_bits = 0xffffffffffffffff
+ default_message = OK
+ }
+ }
+ field {
+ name = unknown_vif_FFAD
+ quantity = Text
+ info = 'Unknown byte.'
+ match {
+ measurement_type = Instantaneous
+ difvifkey = 01FFAD00
+ }
+ lookup {
+ name = UNKNOWN
+ map_type = BitToString
+ mask_bits = 0xff
+ default_message = OK
+ }
+ }
+ field {
+ name = product_no
+ quantity = Text
+ info = 'The meter device product number.'
+ match {
+ difvifkey = 0DFFAA00
+ }
+ }
+ field {
+ name = power_fail
+ quantity = Dimensionless
+ info = 'Power fail counter.'
+ vif_scaling = None
+ dif_signedness = Signed
+ match {
+ difvifkey = 04FF9800
+ }
+ }
+ field {
+ name = active_consumption
+ quantity = Power
+ info = 'Instantaneous total active imported power.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyPowerVIF
+ }
+ }
+ field {
+ name = active_consumption_l1
+ quantity = Power
+ info = 'Instantaneous active imported power for L1 phase.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyPowerVIF
+ add_combinable_raw = 0x7f01
+ }
+ }
+ field {
+ name = active_consumption_l2
+ quantity = Power
+ info = 'Instantaneous active imported power for L2 phase.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyPowerVIF
+ add_combinable_raw = 0x7f02
+ }
+ }
+ field {
+ name = active_consumption_l3
+ quantity = Power
+ info = 'Instantaneous active imported power for L3 phase.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyPowerVIF
+ add_combinable_raw = 0x7f03
+ }
+ }
+ field {
+ name = reactive_consumption
+ quantity = Power
+ info = 'Instantaneous total reactive imported power.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyPowerVIF
+ subunit_nr = 1
+ }
+ }
+ field {
+ name = reactive_consumption_l1
+ quantity = Power
+ info = 'Instantaneous reactive imported power for L1 phase.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyPowerVIF
+ subunit_nr = 2
+ add_combinable_raw = 0x7f01
+ }
+ }
+ field {
+ name = reactive_consumption_l2
+ quantity = Power
+ info = 'Instantaneous reactive imported power for L2 phase.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyPowerVIF
+ subunit_nr = 2
+ add_combinable_raw = 0x7f02
+ }
+ }
+ field {
+ name = reactive_consumption_l3
+ quantity = Power
+ info = 'Instantaneous reactive imported power for L3 phase.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyPowerVIF
+ subunit_nr = 2
+ add_combinable_raw = 0x7f03
+ }
+ }
+ field {
+ name = apparent_consumption
+ quantity = Power
+ info = 'Instantaneous total apparent imported power.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyPowerVIF
+ subunit_nr = 2
+ }
+ }
+ field {
+ name = apparent_consumption_l1
+ quantity = Power
+ info = 'Instantaneous apparent imported power for L1 phase.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyPowerVIF
+ subunit_nr = 4
+ add_combinable_raw = 0x7f01
+ }
+ }
+ field {
+ name = apparent_consumption_l2
+ quantity = Power
+ info = 'Instantaneous apparent imported power for L2 phase.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyPowerVIF
+ subunit_nr = 4
+ add_combinable_raw = 0x7f02
+ }
+ }
+ field {
+ name = apparent_consumption_l3
+ quantity = Power
+ info = 'Instantaneous apparent imported power for L3 phase.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyPowerVIF
+ subunit_nr = 4
+ add_combinable_raw = 0x7f03
+ }
+ }
+ field {
+ name = voltage_l1_n
+ quantity = Voltage
+ info = 'Instantaneous voltage between L1 and neutral.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ measurement_type = Instantaneous
+ vif_range = Voltage
+ add_combinable_raw = 0x7f01
+ }
+ }
+ field {
+ name = voltage_l2_n
+ quantity = Voltage
+ info = 'Instantaneous voltage between L2 and neutral.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ measurement_type = Instantaneous
+ vif_range = Voltage
+ add_combinable_raw = 0x7f02
+ }
+ }
+ field {
+ name = voltage_l3_n
+ quantity = Voltage
+ info = 'Instantaneous voltage between L3 and neutral.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ measurement_type = Instantaneous
+ vif_range = Voltage
+ add_combinable_raw = 0x7f03
+ }
+ }
+ field {
+ name = voltage_average_ln
+ quantity = Voltage
+ info = 'Average voltage line to neutral.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ measurement_type = Instantaneous
+ vif_range = Voltage
+ add_combinable_raw = 0x7f04
+ }
+ }
+ field {
+ name = voltage_l1_l2
+ quantity = Voltage
+ info = 'Instantaneous voltage between L1 and L2.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ measurement_type = Instantaneous
+ vif_range = Voltage
+ add_combinable_raw = 0x7f05
+ }
+ }
+ field {
+ name = voltage_l2_l3
+ quantity = Voltage
+ info = 'Instantaneous voltage between L2 and L3.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ measurement_type = Instantaneous
+ vif_range = Voltage
+ add_combinable_raw = 0x7f06
+ }
+ }
+ field {
+ name = voltage_l3_l1
+ quantity = Voltage
+ info = 'Instantaneous voltage between L3 and L1.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ measurement_type = Instantaneous
+ vif_range = Voltage
+ add_combinable_raw = 0x7f07
+ }
+ }
+ field {
+ name = voltage_average_ll
+ quantity = Voltage
+ info = 'Average voltage line to line.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ measurement_type = Instantaneous
+ vif_range = Voltage
+ add_combinable_raw = 0x7f08
+ }
+ }
+ field {
+ name = current_l1
+ quantity = Amperage
+ info = 'Instantaneous current in the L1 phase.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ measurement_type = Instantaneous
+ vif_range = Amperage
+ add_combinable_raw = 0x7f01
+ }
+ }
+ field {
+ name = current_l2
+ quantity = Amperage
+ info = 'Instantaneous current in the L2 phase.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ measurement_type = Instantaneous
+ vif_range = Amperage
+ add_combinable_raw = 0x7f02
+ }
+ }
+ field {
+ name = current_l3
+ quantity = Amperage
+ info = 'Instantaneous current in the L3 phase.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ measurement_type = Instantaneous
+ vif_range = Amperage
+ add_combinable_raw = 0x7f03
+ }
+ }
+ field {
+ name = current_average
+ quantity = Amperage
+ info = 'Average current in all phases.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ measurement_type = Instantaneous
+ vif_range = Amperage
+ add_combinable_raw = 0x7f00
+ }
+ }
+ field {
+ name = frequency
+ quantity = Frequency
+ info = 'Frequency of AC'
+ vif_scaling = None
+ dif_signedness = Signed
+ display_unit = hz
+ match {
+ difvifkey = 05FF0B
+ }
+ }
+ field {
+ name = power
+ quantity = Dimensionless
+ info = 'Power factor.'
+ vif_scaling = None
+ dif_signedness = Signed
+ display_unit = factor
+ match {
+ difvifkey = 05FF0A
+ }
+ }
+ field {
+ name = power_l1
+ quantity = Dimensionless
+ info = 'Power factor for phase L1.'
+ vif_scaling = None
+ dif_signedness = Signed
+ display_unit = factor
+ force_scale = 0.001
+ match {
+ difvifkey = 02FFE0FF8100
+ }
+ }
+ field {
+ name = power_l2
+ quantity = Dimensionless
+ info = 'Power factor for phase L2.'
+ vif_scaling = None
+ dif_signedness = Signed
+ display_unit = factor
+ force_scale = 0.001
+ match {
+ difvifkey = 02FFE0FF8200
+ }
+ }
+ field {
+ name = power_l3
+ quantity = Dimensionless
+ info = 'Power factor.'
+ vif_scaling = None
+ dif_signedness = Signed
+ display_unit = factor
+ force_scale = 0.001
+ match {
+ difvifkey = 02FFE0FF8300
+ }
+ }
+ field {
+ name = power_phase_angle
+ quantity = Angle
+ info = 'Total power phase angle.'
+ vif_scaling = None
+ dif_signedness = Signed
+ display_unit = deg
+ force_scale = 0.1
+ match {
+ difvifkey = 02FFD200
+ }
+ }
+ field {
+ name = phase_angle_power_l1
+ quantity = Angle
+ info = 'Power phase angle for phase L1.'
+ vif_scaling = None
+ dif_signedness = Signed
+ display_unit = deg
+ force_scale = 0.1
+ match {
+ difvifkey = 02FFD2FF8100
+ }
+ }
+ field {
+ name = phase_angle_power_l2
+ quantity = Angle
+ info = 'Power phase angle for phase L2.'
+ vif_scaling = None
+ dif_signedness = Signed
+ display_unit = deg
+ force_scale = 0.1
+ match {
+ difvifkey = 02FFD2FF8200
+ }
+ }
+ field {
+ name = phase_angle_power_l3
+ quantity = Angle
+ info = 'Power phase angle for phase L3.'
+ vif_scaling = None
+ dif_signedness = Signed
+ display_unit = deg
+ force_scale = 0.1
+ match {
+ difvifkey = 02FFD2FF8300
+ }
+ }
+ field {
+ name = total_reactive_energy_consumption
+ quantity = Reactive_Energy
+ info = 'Total cumulative reactive kvarh imported energy.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ display_unit = kvarh
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyEnergyVIF
+ subunit_nr = 1
+ }
+ }
+ field {
+ name = 'total_reactive_energy_consumption_tariff_{tariff_counter}'
+ quantity = Reactive_Energy
+ info = 'Total cumulative reactive kvarh imported energy per tariff.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ display_unit = kvarh
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyEnergyVIF
+ subunit_nr = 2
+ tariff_nr = 1,4
+ }
+ }
+ field {
+ name = total_reactive_energy_production
+ quantity = Reactive_Energy
+ info = 'Total cumulative reactive kvarh exported energy.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ display_unit = kvarh
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyEnergyVIF
+ subunit_nr = 1
+ add_combinable_raw = 0x7f09
+ }
+ }
+ field {
+ name = 'total_reactive_energy_production_tariff_{tariff_counter}'
+ quantity = Reactive_Energy
+ info = 'Total cumulative reactive kvarh exported energy per tariff.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ display_unit = kvarh
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyEnergyVIF
+ subunit_nr = 3
+ tariff_nr = 1,4
+ }
+ }
+ field {
+ name = current_quadrant
+ quantity = Dimensionless
+ info = 'The quadrant in which the current is measured.'
+ vif_scaling = None
+ dif_signedness = Signed
+ display_unit = nr
+ match {
+ difvifkey = 01FF9700
+ }
+ }
+ field {
+ name = current_quadrant_l1
+ quantity = Dimensionless
+ info = 'The quadrant in which the current is measured for phase L1.'
+ vif_scaling = None
+ dif_signedness = Signed
+ display_unit = nr
+ match {
+ difvifkey = 01FF97FF8100
+ }
+ }
+ field {
+ name = current_quadrant_l2
+ quantity = Dimensionless
+ info = 'The quadrant in which the current is measured for phase L2.'
+ vif_scaling = None
+ dif_signedness = Signed
+ display_unit = nr
+ match {
+ difvifkey = 01FF97FF8200
+ }
+ }
+ field {
+ name = current_quadrant_l3
+ quantity = Dimensionless
+ info = 'The quadrant in which the current is measured for phase L3.'
+ vif_scaling = None
+ dif_signedness = Signed
+ display_unit = nr
+ match {
+ difvifkey = 01FF97FF8300
+ }
+ }
+ field {
+ name = input_metering_cumulation
+ quantity = Dimensionless
+ info = 'Input metering accumulation.'
+ vif_scaling = None
+ dif_signedness = Signed
+ match {
+ measurement_type = Instantaneous
+ vif_range = CumulationCounter
+ }
+ }
+ field {
+ name = pulse_duration
+ quantity = Dimensionless
+ info = 'Energy pulse duration.'
+ vif_scaling = None
+ dif_signedness = Signed
+ match {
+ measurement_type = Instantaneous
+ difvifkey = 03FF2C
+ }
+ }
+ field {
+ name = pulse_weight
+ quantity = Dimensionless
+ info = 'Energy pulse weight.'
+ vif_scaling = None
+ dif_signedness = Signed
+ match {
+ measurement_type = Instantaneous
+ difvifkey = 05FF2E
+ }
+ }
+ field {
+ name = pulse_constant
+ quantity = Dimensionless
+ info = 'Energy pulse constant.'
+ vif_scaling = None
+ dif_signedness = Signed
+ match {
+ measurement_type = Instantaneous
+ difvifkey = 05FF2F
+ }
+ }
+ field {
+ name = resettable_energy_consumption
+ quantity = Energy
+ info = 'Resettable cumulative active imported energy.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyEnergyVIF
+ add_combinable_raw = 0x7f72
+ }
+ }
+ field {
+ name = resettable_energy_production
+ quantity = Energy
+ info = 'Resettable cumulative active exported energy.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyEnergyVIF
+ subunit_nr = 1
+ add_combinable_raw = 0x7f72
+ }
+ }
+ field {
+ name = resettable_reactive_energy_consumption
+ quantity = Energy
+ info = 'Resettable cumulative reactive imported energy.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyEnergyVIF
+ subunit_nr = 2
+ add_combinable_raw = 0x7f72
+ }
+ }
+ field {
+ name = resettable_reactive_energy_production
+ quantity = Energy
+ info = 'Resettable cumulative reactive exported energy.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyEnergyVIF
+ subunit_nr = 3
+ add_combinable_raw = 0x7f72
+ }
+ }
+ field {
+ name = energy_co2
+ quantity = Mass
+ info = 'Energy in co2.'
+ vif_scaling = None
+ dif_signedness = Signed
+ display_unit = kg
+ force_scale = 0.01
+ match {
+ difvifkey = 0EFFF9C400
+ }
+ }
+ field {
+ name = co2_conversion
+ quantity = Dimensionless
+ info = 'CO2 conversion factor (kg * 10-3 /kWh).'
+ vif_scaling = None
+ dif_signedness = Signed
+ display_unit = factor
+ match {
+ difvifkey = 04FFA400
+ }
+ }
+ field {
+ name = energy_currency
+ quantity = Dimensionless
+ info = 'Energy in currency.'
+ vif_scaling = None
+ dif_signedness = Signed
+ match {
+ difvifkey = 0EFFF9C900
+ }
+ }
+ field {
+ name = currency_conversion
+ quantity = Dimensionless
+ info = 'Currency conversion factor (curr * 10-3 /kWh).'
+ vif_scaling = None
+ dif_signedness = Signed
+ display_unit = factor
+ match {
+ difvifkey = 04FFA500
+ }
+ }
+ field {
+ name = total_apparent_energy_consumption
+ quantity = Apparent_Energy
+ info = 'Total cumulative apparent kvah imported energy.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ display_unit = kvah
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyEnergyVIF
+ subunit_nr = 4
+ }
+ }
+ field {
+ name = total_apparent_energy_production
+ quantity = Apparent_Energy
+ info = 'Total cumulative apparent kvah exported energy.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ display_unit = kvah
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyEnergyVIF
+ subunit_nr = 5
+ }
+ }
+ field {
+ name = total_energy_consumption_l1
+ quantity = Energy
+ info = 'Total imported active energy for L1 phase.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyEnergyVIF
+ add_combinable_raw = 0x7f01
+ }
+ }
+ field {
+ name = total_energy_consumption_l2
+ quantity = Energy
+ info = 'Total imported active energy for L2 phase.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyEnergyVIF
+ add_combinable_raw = 0x7f02
+ }
+ }
+ field {
+ name = total_energy_consumption_l3
+ quantity = Energy
+ info = 'Total imported active energy for L3 phase.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyEnergyVIF
+ add_combinable_raw = 0x7f03
+ }
+ }
+ field {
+ name = total_reactive_energy_consumption_l1
+ quantity = Reactive_Energy
+ info = 'Total imported reactive energy for L1 phase.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ display_unit = kvarh
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyEnergyVIF
+ subunit_nr = 2
+ add_combinable_raw = 0x7f01
+ }
+ }
+ field {
+ name = total_reactive_energy_consumption_l2
+ quantity = Reactive_Energy
+ info = 'Total imported reactive energy for L2 phase.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ display_unit = kvarh
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyEnergyVIF
+ subunit_nr = 2
+ add_combinable_raw = 0x7f02
+ }
+ }
+ field {
+ name = total_reactive_energy_consumption_l3
+ quantity = Reactive_Energy
+ info = 'Total imported reactive energy for L3 phase.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ display_unit = kvarh
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyEnergyVIF
+ subunit_nr = 2
+ add_combinable_raw = 0x7f03
+ }
+ }
+ field {
+ name = total_apparent_energy_consumption_l1
+ quantity = Apparent_Energy
+ info = 'Total imported apparent energy for L1 phase.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ display_unit = kvah
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyEnergyVIF
+ subunit_nr = 4
+ add_combinable_raw = 0x7f01
+ }
+ }
+ field {
+ name = total_apparent_energy_consumption_l2
+ quantity = Apparent_Energy
+ info = 'Total imported apparent energy for L2 phase.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ display_unit = kvah
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyEnergyVIF
+ subunit_nr = 4
+ add_combinable_raw = 0x7f02
+ }
+ }
+ field {
+ name = total_apparent_energy_consumption_l3
+ quantity = Apparent_Energy
+ info = 'Total imported apparent energy for L3 phase.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ display_unit = kvah
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyEnergyVIF
+ subunit_nr = 4
+ add_combinable_raw = 0x7f03
+ }
+ }
+ field {
+ name = total_energy_production_l1
+ quantity = Energy
+ info = 'Total exported active energy for L1 phase.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyEnergyVIF
+ subunit_nr = 1
+ add_combinable_raw = 0x7f01
+ }
+ }
+ field {
+ name = total_energy_production_l2
+ quantity = Energy
+ info = 'Total exported active energy for L2 phase.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyEnergyVIF
+ subunit_nr = 1
+ add_combinable_raw = 0x7f02
+ }
+ }
+ field {
+ name = total_energy_production_l3
+ quantity = Energy
+ info = 'Total exported active energy for L3 phase.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyEnergyVIF
+ subunit_nr = 1
+ add_combinable_raw = 0x7f03
+ }
+ }
+ field {
+ name = total_reactive_energy_production_l1
+ quantity = Reactive_Energy
+ info = 'Total exported reactive energy for L1 phase.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ display_unit = kvarh
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyEnergyVIF
+ subunit_nr = 3
+ add_combinable_raw = 0x7f01
+ }
+ }
+ field {
+ name = total_reactive_energy_production_l2
+ quantity = Reactive_Energy
+ info = 'Total exported reactive energy for L2 phase.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ display_unit = kvarh
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyEnergyVIF
+ subunit_nr = 3
+ add_combinable_raw = 0x7f02
+ }
+ }
+ field {
+ name = total_reactive_energy_production_l3
+ quantity = Reactive_Energy
+ info = 'Total exported reactive energy for L3 phase.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ display_unit = kvarh
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyEnergyVIF
+ subunit_nr = 3
+ add_combinable_raw = 0x7f03
+ }
+ }
+ field {
+ name = total_apparent_energy_production_l1
+ quantity = Apparent_Energy
+ info = 'Total exported apparent energy for L1 phase.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ display_unit = kvah
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyEnergyVIF
+ subunit_nr = 5
+ add_combinable_raw = 0x7f01
+ }
+ }
+ field {
+ name = total_apparent_energy_production_l2
+ quantity = Apparent_Energy
+ info = 'Total exported apparent energy for L2 phase.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ display_unit = kvah
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyEnergyVIF
+ subunit_nr = 5
+ add_combinable_raw = 0x7f02
+ }
+ }
+ field {
+ name = total_apparent_energy_production_l3
+ quantity = Apparent_Energy
+ info = 'Total exported apparent energy for L3 phase.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ display_unit = kvah
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyEnergyVIF
+ subunit_nr = 5
+ add_combinable_raw = 0x7f03
+ }
+ }
+ field {
+ name = total_net_energy
+ quantity = Energy
+ info = 'Active net energy total.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyEnergyVIF
+ subunit_nr = 6
+ }
+ }
+ field {
+ name = total_net_energy_l1
+ quantity = Energy
+ info = 'Active net energy total for phase L1.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyEnergyVIF
+ subunit_nr = 6
+ add_combinable_raw = 0x7f01
+ }
+ }
+ field {
+ name = total_net_energy_l2
+ quantity = Energy
+ info = 'Active net energy total for phase L2.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyEnergyVIF
+ subunit_nr = 6
+ add_combinable_raw = 0x7f02
+ }
+ }
+ field {
+ name = total_net_energy_l3
+ quantity = Energy
+ info = 'Active net energy total for phase L3.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyEnergyVIF
+ subunit_nr = 6
+ add_combinable_raw = 0x7f03
+ }
+ }
+ field {
+ name = total_net_reactive_energy
+ quantity = Reactive_Energy
+ info = 'Reactive net energy total.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ display_unit = kvarh
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyEnergyVIF
+ subunit_nr = 7
+ }
+ }
+ field {
+ name = total_net_reactive_energy_l1
+ quantity = Reactive_Energy
+ info = 'Active net reactive energy total for phase L1.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ display_unit = kvarh
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyEnergyVIF
+ subunit_nr = 7
+ add_combinable_raw = 0x7f01
+ }
+ }
+ field {
+ name = total_net_reactive_energy_l2
+ quantity = Reactive_Energy
+ info = 'Active net reactive energy total for phase L2.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ display_unit = kvarh
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyEnergyVIF
+ subunit_nr = 7
+ add_combinable_raw = 0x7f02
+ }
+ }
+ field {
+ name = total_net_reactive_energy_l3
+ quantity = Reactive_Energy
+ info = 'Active net reactive energy total for phase L3.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ display_unit = kvarh
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyEnergyVIF
+ subunit_nr = 7
+ add_combinable_raw = 0x7f03
+ }
+ }
+ field {
+ name = total_net_apparent_energy
+ quantity = Apparent_Energy
+ info = 'Apparent net energy total.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ display_unit = kvah
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyEnergyVIF
+ subunit_nr = 8
+ }
+ }
+ field {
+ name = total_net_apparent_energy_l1
+ quantity = Apparent_Energy
+ info = 'Active net apparent energy total for phase L1.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ display_unit = kvah
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyEnergyVIF
+ subunit_nr = 8
+ add_combinable_raw = 0x7f01
+ }
+ }
+ field {
+ name = total_net_apparent_energy_l2
+ quantity = Apparent_Energy
+ info = 'Active net apparent energy total for phase L2.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ display_unit = kvah
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyEnergyVIF
+ subunit_nr = 8
+ add_combinable_raw = 0x7f02
+ }
+ }
+ field {
+ name = total_net_apparent_energy_l3
+ quantity = Apparent_Energy
+ info = 'Active net apparent energy total for phase L3.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ display_unit = kvah
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyEnergyVIF
+ subunit_nr = 8
+ add_combinable_raw = 0x7f03
+ }
+ }
+ field {
+ name = last_partial_energy_reset
+ quantity = PointInTime
+ info = 'Date and time of last partial energy reset,'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ display_unit = datetime
+ match {
+ measurement_type = Instantaneous
+ vif_range = DateTime
+ add_combinable_raw = 0x7f0c
+ }
+ }
+ field {
+ name = last_input_metering_reset
+ quantity = PointInTime
+ info = 'Date and time of last input metering reset.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ display_unit = datetime
+ match {
+ measurement_type = Instantaneous
+ vif_range = DateTime
+ add_combinable_raw = 0x7f0e
+ }
+ }
+ field {
+ name = digital_input
+ quantity = Text
+ info = 'Digital input status.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = DigitalInput
+ }
+ lookup {
+ name = INPUT
+ map_type = BitToString
+ mask_bits = 0xffffff
+ default_message = ''
+ }
+ }
+ field {
+ name = digital_input_status
+ quantity = Text
+ info = 'Digital input status.'
+ match {
+ measurement_type = Instantaneous
+ difvifkey = 02FF32
+ }
+ lookup {
+ name = INPUT_STATUS
+ map_type = BitToString
+ mask_bits = 0xff
+ default_message = ''
+ }
+ }
+ field {
+ name = digital_output
+ quantity = Text
+ info = 'Digital output status.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = DigitalOutput
+ }
+ lookup {
+ name = OUTPUT
+ map_type = BitToString
+ mask_bits = 0xffffff
+ default_message = ''
+ }
+ }
+ field {
+ name = digital_output_association
+ quantity = Text
+ info = 'Digital output association.'
+ match {
+ measurement_type = Instantaneous
+ difvifkey = 03FF2D
+ }
+ lookup {
+ name = OUTPUT_ASSOCIATION
+ map_type = BitToString
+ mask_bits = 0xffffff
+ default_message = ''
+ }
+ }
+ field {
+ name = digital_input_association
+ quantity = Text
+ info = 'Digital input association.'
+ match {
+ measurement_type = Instantaneous
+ difvifkey = 03FF30
+ }
+ lookup {
+ name = INPUT_ASSOCIATION
+ map_type = BitToString
+ mask_bits = 0xffffff
+ default_message = ''
+ }
+ }
+ field {
+ name = digital_output_association_2
+ quantity = Text
+ info = 'Digital output association (secondary register).'
+ match {
+ measurement_type = Instantaneous
+ difvifkey = 02FF36
+ }
+ lookup {
+ name = OUTPUT_ASSOCIATION_2
+ map_type = BitToString
+ mask_bits = 0xffff
+ default_message = ''
+ }
+ }
+ field {
+ name = overload_alarm_setup
+ quantity = Text
+ info = 'Overload alarm setup.'
+ match {
+ measurement_type = Instantaneous
+ difvifkey = 02FF34
+ }
+ lookup {
+ name = OVERLOAD_ALARM
+ map_type = BitToString
+ mask_bits = 0xff
+ default_message = ''
+ }
+ }
+ field {
+ name = pickup_setpoint
+ quantity = Dimensionless
+ info = 'Pickup setpoint.'
+ vif_scaling = None
+ dif_signedness = Signed
+ match {
+ difvifkey = 05FF35
+ }
+ }
+ field {
+ name = activated_status
+ quantity = Text
+ info = 'Activated status.'
+ match {
+ measurement_type = Instantaneous
+ difvifkey = 02FF37
+ }
+ lookup {
+ name = ACTIVATED_STATUS
+ map_type = BitToString
+ mask_bits = 0xff
+ default_message = ''
+ }
+ }
+ field {
+ name = unack_status
+ quantity = Text
+ info = 'Unacknowledged status.'
+ match {
+ measurement_type = Instantaneous
+ difvifkey = 02FF38
+ }
+ lookup {
+ name = UNACK_STATUS
+ map_type = BitToString
+ mask_bits = 0xff
+ default_message = ''
+ }
+ }
+ field {
+ name = last_alarm
+ quantity = PointInTime
+ info = 'Date and time of last alarm.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ display_unit = datetime
+ match {
+ measurement_type = Instantaneous
+ vif_range = DateTime
+ add_combinable_raw = 0x7f39
+ }
+ }
+ field {
+ name = last_alarm
+ quantity = Dimensionless
+ info = 'Last alarm value.'
+ vif_scaling = None
+ dif_signedness = Signed
+ display_unit = nr
+ match {
+ difvifkey = 05FF3A
+ }
+ }
+ field {
+ name = operating_time
+ quantity = Time
+ info = 'Operating time.'
+ vif_scaling = None
+ dif_signedness = Signed
+ display_unit = y
+ match {
+ difvifkey = 06FF20
+ }
+ }
+ field {
+ name = phases
+ quantity = Dimensionless
+ info = 'Number of phases.'
+ vif_scaling = None
+ dif_signedness = Signed
+ display_unit = counter
+ match {
+ difvifkey = 03FF21
+ }
+ }
+ field {
+ name = wires
+ quantity = Dimensionless
+ info = 'Number of wires.'
+ vif_scaling = None
+ dif_signedness = Signed
+ display_unit = nr
+ match {
+ difvifkey = 03FF22
+ }
+ }
+ field {
+ name = vts
+ quantity = Dimensionless
+ info = 'Number of voltage transformers VT:s.'
+ vif_scaling = None
+ dif_signedness = Signed
+ display_unit = counter
+ match {
+ difvifkey = 03FF25
+ }
+ }
+ field {
+ name = vt_primary
+ quantity = Dimensionless
+ info = 'Primary voltage transformer VT.'
+ vif_scaling = None
+ dif_signedness = Signed
+ display_unit = nr
+ match {
+ difvifkey = 05FF26
+ }
+ }
+ field {
+ name = vt_secondary
+ quantity = Dimensionless
+ info = 'Secondary voltage transformer VT.'
+ vif_scaling = None
+ dif_signedness = Signed
+ display_unit = nr
+ match {
+ difvifkey = 03FF27
+ }
+ }
+ field {
+ name = cts
+ quantity = Dimensionless
+ info = 'Number of current transformers CT:s.'
+ vif_scaling = None
+ dif_signedness = Signed
+ display_unit = counter
+ match {
+ difvifkey = 03FF28
+ }
+ }
+ field {
+ name = ct_primary
+ quantity = Dimensionless
+ info = 'Primary current transformer CT.'
+ vif_scaling = None
+ dif_signedness = Signed
+ display_unit = nr
+ match {
+ difvifkey = 03FF29
+ }
+ }
+ field {
+ name = ct_secondary
+ quantity = Dimensionless
+ info = 'Secondary current transformer CT.'
+ vif_scaling = None
+ dif_signedness = Signed
+ display_unit = nr
+ match {
+ difvifkey = 03FF2A
+ }
+ }
+ field {
+ name = vt_connection_type
+ quantity = Dimensionless
+ info = 'Voltage transformer connection type.'
+ vif_scaling = None
+ dif_signedness = Signed
+ display_unit = nr
+ match {
+ difvifkey = 03FF2B
+ }
+ }
+ field {
+ name = power_system_configuration
+ quantity = Text
+ info = 'Power system configuration.'
+ match {
+ measurement_type = Instantaneous
+ difvifkey = 03FF23
+ }
+ lookup {
+ name = POWER_SYS_CONFIG
+ map_type = BitToString
+ mask_bits = 0xffffff
+ default_message = ''
+ }
+ }
+ field {
+ name = nominal_frequency
+ quantity = Frequency
+ info = 'Nominal frequency.'
+ vif_scaling = None
+ dif_signedness = Signed
+ display_unit = hz
+ match {
+ difvifkey = 03FF24
+ }
+ }
+ }
+ tests {
+ test {
+ args = 'Schneider1 iem3000 03313062 NOKEY'
+ telegram = 68f4f46808027262303103a34c1502160000000dfd0a126369727463656c452072656469656e6863530dfd0c0820353331334d45690dfd0e073730302e332e3103fd1700000005fddcff01Bec6084205fddcff0251f30B4205fddcff03113d094205fddcff000Bfd094205fdc9ff05aa6ac74305fdc9ff06acfac74305fdc9ff078e3cc74305fdc9ff08c0B2c74305fdc9ff01c7dd674305fdc9ff023e0e654305fdc9ff03c9e1664305fdc9ff04ef99664305aeff01a0edc14005aeff022e8dc34005aeff0303BBc040052e748d914185402ecB3676418580402e02a2Be4105ff0a3e76433f05ff0B64f8474207033230fd01000000001fc116
+ json = '{"_":"telegram","media":"electricity","driver":"iem3000","name":"Schneider1","id":"03313062","active_consumption_kw":18.194069,"active_consumption_l1_kw":6.060257,"active_consumption_l2_kw":6.110984,"active_consumption_l3_kw":6.022829,"apparent_consumption_kw":23.829105,"current_average_a":34.497112,"current_l1_a":34.194084,"current_l2_a":34.987614,"current_l3_a":34.309635,"frequency_hz":49.992569,"power_factor":0.763523,"reactive_consumption_kw":15.388377,"total_energy_consumption_kwh":33370.162,"voltage_average_ll_v":399.396484,"voltage_average_ln_v":230.601303,"voltage_l1_l2_v":398.833313,"voltage_l1_n_v":231.866318,"voltage_l2_l3_v":399.958374,"voltage_l2_n_v":229.055634,"voltage_l3_l1_v":398.473083,"voltage_l3_n_v":230.881973,"error_flags":"CODE_101_EEPROM_ERROR","firmware_version":"1.3.007","manufacturer":"Schneider Electric","model_version":"iEM3135 ","status":"CODE_101_EEPROM_ERROR","timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'Schneider1;03313062;33370.162;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'Schneider2 iem3000 03313062 NOKEY'
+ telegram = 68F4F46808027262303103A34C150250000000_0DFD0A126369727463656C452072656469656E6863530DFD0C0820353331334D45690DFD0E073730302E332E3103FD1700000005FDDCFF01CDFC074205FDDCFF025D670B4205FDDCFF039E9D084205FDDCFF00ED55094205FDC9FF050329C84305FDC9FF0664CEC84305FDC9FF07AADFC74305FDC9FF08B047C84305FDC9FF01AF64684305FDC9FF0260E9654305FDC9FF03B383674305FDC9FF04EC45674305AEFF01ACB6C04005AEFF02E3B4C24005AEFF03BEA7BF40052ED4C4904185402ECF2478418580402E82FBBE4105FF0AA19E423F05FF0B3FCA474207030A6CFD01000000001F_7716
+ json = '{"_":"telegram","media":"electricity","driver":"iem3000","name":"Schneider2","id":"03313062","active_consumption_kw":18.096107,"active_consumption_l1_kw":6.022299,"active_consumption_l2_kw":6.084581,"active_consumption_l3_kw":5.989226,"apparent_consumption_kw":23.872807,"current_average_a":34.333912,"current_l1_a":33.996876,"current_l2_a":34.850941,"current_l3_a":34.153923,"frequency_hz":49.947506,"power_factor":0.760233,"reactive_consumption_kw":15.508986,"total_energy_consumption_kwh":33385.482,"voltage_average_ll_v":400.560059,"voltage_average_ln_v":231.273132,"voltage_l1_l2_v":400.320404,"voltage_l1_n_v":232.393295,"voltage_l2_l3_v":401.612427,"voltage_l2_n_v":229.911621,"voltage_l3_l1_v":399.747375,"voltage_l3_n_v":231.51445,"error_flags":"CODE_101_EEPROM_ERROR","firmware_version":"1.3.007","manufacturer":"Schneider Electric","model_version":"iEM3135 ","status":"CODE_101_EEPROM_ERROR","timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'Schneider2;03313062;33385.482;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'Schneider3 iem3000 03313062 NOKEY'
+ telegram = 68F6F66808027262303103A34C150251000000_0783FF091100000000000000874003987D020200000000874083FF09000000000000000004EDFF0C000001010783FF0D0A6CFD0100000000874083FF0D987D0202000000000783FF016213A900000000000783FF028008AD00000000000783FF033850A7000000000004EDFF0E0000010107FD61000000000000000003FF10000000871003000000000000000087200300000000000000008730030000000000000000878010030000000000000000046D2109292603FF2C64000003FF2D00000005FF2E0000C84205FF2F0000FA4303FF3000000003FD1B00000002FF32000003FD1AFFFF001F_A816
+ json = '{"_":"telegram","media":"electricity","driver":"iem3000","name":"Schneider3","id":"03313062","active_tariff_nr":0,"input_metering_cumulation_counter":0,"last_input_metering_reset_datetime":"2000-01-01 00:00","last_partial_energy_reset_datetime":"2000-01-01 00:00","partial_energy_consumption_kwh":33385.482,"partial_reactive_energy_consumption_kvarh":33717.656,"pulse_constant_counter":500,"pulse_duration_counter":100,"pulse_weight_counter":100,"total_energy_consumption_l1_kwh":11080.546,"total_energy_consumption_l2_kwh":11339.904,"total_energy_consumption_l3_kwh":10965.048,"total_energy_consumption_tariff_1_kwh":0,"total_energy_consumption_tariff_2_kwh":0,"total_energy_consumption_tariff_3_kwh":0,"total_energy_consumption_tariff_4_kwh":0,"total_energy_production_kwh":0.017,"total_reactive_energy_consumption_kvarh":33717.656,"total_reactive_energy_production_kvarh":0,"digital_input":"","digital_input_association":"","digital_input_status":"","digital_output":"OUTPUT_FFFF","digital_output_association":"","meter_datetime":"2017-06-09 09:33","status":"OK","timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'Schneider3;03313062;null;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'Schneider4 iem3000 03313062 NOKEY'
+ telegram = 68c6c66808027262303103a34c15025200000002ff34000005ff350000803f02ff36000002ff37000002ff38000004edff390000010105ff3a0000c0ff06ff200167a700000003ff2103000003ff2204000003ff230B000003ff2432000005037f6902470583ff0996438B3c854003aBB50347854083ff09000000000583ff0d7f690247854083ff0daBB503470583ff0134222d460583ff02a42f31460583ff0336542B4605fd610000000085100300000000852003000000008530030000000085801003000000000fBa16
+ json = '{"_":"telegram","media":"electricity","driver":"iem3000","name":"Schneider4","id":"03313062","input_metering_cumulation_counter":0,"last_alarm_datetime":"2000-01-01 00:00","last_alarm_nr":null,"nominal_frequency_hz":50,"operating_time_y":10970881,"partial_energy_consumption_kwh":33.385496,"partial_reactive_energy_consumption_kvarh":33.717668,"phases_counter":3,"pickup_setpoint_counter":1,"total_energy_consumption_kwh":33.385496,"total_energy_consumption_l1_kwh":11.080551,"total_energy_consumption_l2_kwh":11.33991,"total_energy_consumption_l3_kwh":10.965053,"total_energy_consumption_tariff_1_kwh":0,"total_energy_consumption_tariff_2_kwh":0,"total_energy_consumption_tariff_3_kwh":0,"total_energy_consumption_tariff_4_kwh":0,"total_energy_production_kwh":0.000017,"total_reactive_energy_consumption_kvarh":33.717668,"total_reactive_energy_production_kvarh":0,"wires_nr":4,"activated_status":"","digital_output_association_2":"","overload_alarm_setup":"","power_system_configuration":"POWER_SYS_CONFIG_B","status":"OK","unack_status":"","timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'Schneider4;03313062;33.385496;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'S2_1 iem3000 78563412 NOKEY'
+ telegram = 68f4f46808467212345678a34c1302840000000dfd0a126369727463656c452072656469656e6863530dfd0c0820353331334d45690dfd0e073030302e322e3103fd1740000005fddcff014695d63e05fddcff025a57dB3e05fddcff03392aa73e05fddcff00f35cc83e05fdc9ff050a2cc94305fdc9ff0630B4c94305fdc9ff07f0aec84305fdc9ff08B82fc94305fdc9ff018e01684305fdc9ff02B154694305fdc9ff03B7a0674305fdc9ff045352684305aeff01171c2d3c05aeff02ed0a8e3d05aeff038fBd983B052e493aad3d85402e7efB79Be8580402ef647843e05ff0a3718d63f05ff0Bc31548420703ed748B04000000001fC416
+ json = '{"_": "telegram","active_consumption_kw": 0.084584,"active_consumption_l1_kw": 0.010566,"active_consumption_l2_kw": 0.069357,"active_consumption_l3_kw": 0.004661,"apparent_consumption_kw": 0.258362,"current_average_a": 0.391334,"current_l1_a": 0.419108,"current_l2_a": 0.428401,"current_l3_a": 0.326494,"error_flags": "CODE_203_PHASE_SEQUENCE_REVERSED","firmware_version": "1.2.000","frequency_hz": 50.021252,"id": "78563412","manufacturer": "Schneider Electric","media": "electricity","driver": "iem3000","model_version": "iEM3135 ","name": "S2_1","power_factor": 1.672614,"reactive_consumption_kw": -0.244123,"status": "CODE_203_PHASE_SEQUENCE_REVERSED","timestamp": "1111-11-11T11:11:11Z","total_energy_consumption_kwh": 76248.301,"voltage_average_ll_v": 402.372803,"voltage_average_ln_v": 232.321579,"voltage_l1_l2_v": 402.344055,"voltage_l1_n_v": 232.006073,"voltage_l2_l3_v": 403.407715,"voltage_l2_n_v": 233.330826,"voltage_l3_l1_v": 401.366699,"voltage_l3_n_v": 231.627792}'
+ fields = 'S2_1;78563412;76248.301;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'S2_2 iem3000 78563412 NOKEY'
+ telegram = 68F6F66808467212345678A34C130285000000_0783FF090100000000000000874003D4D9000000000000874083FF09A01ED2060000000004EDFF0C000001010783FF0DED748B0400000000874083FF0DD4D90000000000000783FF0176938001000000000783FF02751D9801000000000783FF0357C872010000000004EDFF0E0000010107FD61000000000000000003FF10000000871003000000000000000087200300000000000000008730030000000000000000878010030000000000000000046D0015060B03FF2C64000003FF2D00000005FF2E0000C84205FF2F0000FA4303FF3000000003FD1B00000002FF32000003FD1AFFFF001F_0816
+ json = '{"_": "telegram","active_tariff_nr": 0,"digital_input": "","digital_input_association": "","digital_input_status": "","digital_output": "OUTPUT_FFFF","digital_output_association": "","id": "78563412","input_metering_cumulation_counter": 0,"last_input_metering_reset_datetime": "2000-01-01 00:00","last_partial_energy_reset_datetime": "2000-01-01 00:00","media": "electricity","driver": "iem3000","meter_datetime": "2000-11-06 21:00","name": "S2_2","partial_energy_consumption_kwh": 76248.301,"partial_reactive_energy_consumption_kvarh": 55.764,"pulse_constant_counter": 500,"pulse_duration_counter": 100,"pulse_weight_counter": 100,"status": "OK","timestamp": "1111-11-11T11:11:11Z","total_energy_consumption_l1_kwh": 25203.574,"total_energy_consumption_l2_kwh": 26746.229,"total_energy_consumption_l3_kwh": 24299.607,"total_energy_consumption_tariff_1_kwh": 0,"total_energy_consumption_tariff_2_kwh": 0,"total_energy_consumption_tariff_3_kwh": 0,"total_energy_consumption_tariff_4_kwh": 0,"total_energy_production_kwh": 0.001,"total_reactive_energy_consumption_kvarh": 55.764,"total_reactive_energy_production_kvarh": 114433.696}'
+ fields = 'S2_2;78563412;null;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'S2_2 iem3000 78563412 NOKEY'
+ telegram = 685B5B6808467212345678a34c13028600000002ff34000005ff350000803f02ff36000002ff37000002ff38000004edff390000010105ff3a0000c0ff06ff2004f81f0f000003ff2103000003ff2204000003ff230B000003ff243200000f8f16
+ json = '{"_": "telegram","activated_status": "","digital_output_association_2": "","id": "78563412","last_alarm_datetime": "2000-01-01 00:00","last_alarm_nr": null,"media": "electricity","driver": "iem3000","name": "S2_2","nominal_frequency_hz": 50,"operating_time_y": 253753348,"overload_alarm_setup": "","phases_counter": 3,"pickup_setpoint_counter": 1,"power_system_configuration": "POWER_SYS_CONFIG_B","status": "OK","timestamp": "1111-11-11T11:11:11Z","unack_status": "","wires_nr": 4}'
+ fields = 'S2_2;78563412;null;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'S3_1 iem3000 11111111 NOKEY'
+ telegram = 68F4F46808177211111111A34C18020D000000_0DFD0A126369727463656C452072656469656E6863530DFD0C0820353332334D45690DFD0E073230302E342E3103FD1740000005FDDCFF011CC9B94105FDDCFF026B8E0B4205FDDCFF0301478B4105FDDCFF005164C94105FDC9FF05A48DC84305FDC9FF06A397C84305FDC9FF07DB26C94305FDC9FF080CC4C84305FDC9FF0190D1674305FDC9FF0290E7664305FDC9FF03F5C5684305FDC9FF0407D5674305AEFF01C08AA54005AEFF025CCBFC4005AEFF037F1F7840052E7799874185402E3BF887C08580402E93CB8B4105FF0A78D7833F05FF0BB6BC4742070304726A16000000001F_8616
+ json = '{"_":"telegram","media":"electricity","driver":"iem3000","name":"S3_1","id":"11111111","active_consumption_kw":16.949934,"active_consumption_l1_kw":5.173187,"active_consumption_l2_kw":7.899824,"active_consumption_l3_kw":3.876922,"apparent_consumption_kw":17.474401,"current_average_a":25.173983,"current_l1_a":23.223198,"current_l2_a":34.88908,"current_l3_a":17.40967,"frequency_hz":49.934288,"power_factor":1.030013,"reactive_consumption_kw":-4.249052,"total_energy_consumption_kwh":376074.756,"voltage_average_ll_v":401.531616,"voltage_average_ln_v":231.832138,"voltage_l1_l2_v":401.106567,"voltage_l1_n_v":231.818604,"voltage_l2_l3_v":401.184662,"voltage_l2_n_v":230.904541,"voltage_l3_l1_v":402.303558,"voltage_l3_n_v":232.77327,"error_flags":"CODE_203_PHASE_SEQUENCE_REVERSED","firmware_version":"1.4.002","manufacturer":"Schneider Electric","model_version":"iEM3235 ","status":"CODE_203_PHASE_SEQUENCE_REVERSED","timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'S3_1;11111111;376074.756;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'S3_2 iem3000 11111111 NOKEY'
+ telegram = 68F6F66808177211111111A34C18020E000000_0783FF0900000000000000008740039BE87D0200000000874083FF09638D87020000000004EDFF0C802001010783FF0D09726A1600000000874083FF0D9BE87D02000000000783FF0102EB3007000000000783FF024A14AD09000000000783FF03B9728C050000000004EDFF0E8020010107FD61000000000000000003FF10000000871003000000000000000087200300000000000000008730030000000000000000878010030000000000000000046DB82E4D0803FF2C64000003FF2D00000005FF2E0000C84205FF2F0000FA4303FF3000000003FD1B00000002FF32000003FD1AFFFF001F_3016
+ json = '{"_": "telegram","active_tariff_nr": 0,"digital_input": "","digital_input_association": "","digital_input_status": "","digital_output": "OUTPUT_FFFF","digital_output_association": "","id": "11111111","input_metering_cumulation_counter": 0,"last_input_metering_reset_datetime": "2000-01-01 00:00","last_partial_energy_reset_datetime": "2000-01-01 00:00","media": "electricity","driver": "iem3000","meter_datetime": "2002-08-13 14:56","name": "S3_2","partial_energy_consumption_kwh": 376074.761,"partial_reactive_energy_consumption_kvarh": 41805.979,"pulse_constant_counter": 500,"pulse_duration_counter": 100,"pulse_weight_counter": 100,"status": "OK","timestamp": "1111-11-11T11:11:11Z","total_energy_consumption_l1_kwh": 120646.402,"total_energy_consumption_l2_kwh": 162337.866,"total_energy_consumption_l3_kwh": 93090.489,"total_energy_consumption_tariff_1_kwh": 0,"total_energy_consumption_tariff_2_kwh": 0,"total_energy_consumption_tariff_3_kwh": 0,"total_energy_consumption_tariff_4_kwh": 0,"total_energy_production_kwh": 0,"total_reactive_energy_consumption_kvarh": 41805.979,"total_reactive_energy_production_kvarh": 42437.987}'
+ fields = 'S3_2;11111111;null;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'S3_3 iem3000 11111111 NOKEY'
+ telegram = 68F1F16808177211111111A34C18020F000000_02FF34000005FF350000803F02FF36000002FF37000002FF38000004EDFF398020010105FF3A0000C0FF06FF20D9292605000003FF2103000003FF2204000003FF230B000003FF24320000050359A1B7480583FF0900000000854003FB4D2347854083FF09FDC525470583FF0D58A1B748854083FF0DFB4D23470583FF0133A3EB470583FF0278881E480583FF033ED1B54705FD6100000000851003000000008520030000000085300300000000858010030000000003FF2500000005FF260000C84203FF2764000003FF2803000003FF292C010003FF2A05000003FF2B0000000F_2e16
+ json = '{"_": "telegram","activated_status": "","ct_primary_nr": 300,"ct_secondary_nr": 5,"cts_counter": 3,"digital_output_association_2": "","id": "11111111","input_metering_cumulation_counter": 0,"last_alarm_datetime": "2000-01-01 00:00","last_alarm_nr": null,"media": "electricity","driver": "iem3000","name": "S3_3","nominal_frequency_hz": 50,"operating_time_y": 86387161,"overload_alarm_setup": "","partial_energy_consumption_kwh": 376.07475,"partial_reactive_energy_consumption_kvarh": 41.80598,"phases_counter": 3,"pickup_setpoint_counter": 1,"power_system_configuration": "POWER_SYS_CONFIG_B","status": "OK","timestamp": "1111-11-11T11:11:11Z","total_energy_consumption_kwh": 376.074781,"total_energy_consumption_l1_kwh": 120.646398,"total_energy_consumption_l2_kwh": 162.337875,"total_energy_consumption_l3_kwh": 93.090484,"total_energy_consumption_tariff_1_kwh": 0,"total_energy_consumption_tariff_2_kwh": 0,"total_energy_consumption_tariff_3_kwh": 0,"total_energy_consumption_tariff_4_kwh": 0,"total_energy_production_kwh": 0,"total_reactive_energy_consumption_kvarh": 41.80598,"total_reactive_energy_production_kvarh": 42.437988,"unack_status": "","vt_connection_type_nr": 0,"vt_primary_nr": 100,"vt_secondary_nr": 100,"vts_counter": 0,"wires_nr": 4}'
+ fields = 'S3_3;11111111;376.074781;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'S4_1 iem3000 77777777 NOKEY'
+ telegram = 68F4F468080C7277777777A34C150274000000_0DFD0A126369727463656C452072656469656E6863530DFD0C0820353333334D45690DFD0E073330302E312E3103FD1750000005FDDCFF010000000005FDDCFF020000000005FDDCFF030000000005FDDCFF000000000005FDC9FF0531B5C64305FDC9FF062BB7C64305FDC9FF074F3DC74305FDC9FF0839E3C64305FDC9FF012D05664305FDC9FF020E40654305FDC9FF03BFB5654305FDC9FF04FFA8654305AEFF010000000005AEFF020000000005AEFF0300000000052E0000000085402E000000008580402E0000000005FF0A0000C0FF05FF0B4531484207035679DA1F000000001F_0116
+ json = '{"_": "telegram","active_consumption_kw": 0,"active_consumption_l1_kw": 0,"active_consumption_l2_kw": 0,"active_consumption_l3_kw": 0,"apparent_consumption_kw": 0,"current_average_a": 0,"current_l1_a": 0,"current_l2_a": 0,"current_l3_a": 0,"error_flags": "CODE_204_TOTAL_ACTIVE_ENERGY_NEGATIVE_DUE_TO_INCORRECT_V_OR_A_CONNECTIONS","firmware_version": "1.1.003","frequency_hz": 50.048115,"id": "77777777","manufacturer": "Schneider Electric","media": "electricity","driver": "iem3000","model_version": "iEM3335 ","name": "S4_1","power_factor": null,"reactive_consumption_kw": 0,"status": "CODE_204_TOTAL_ACTIVE_ENERGY_NEGATIVE_DUE_TO_INCORRECT_V_OR_A_CONNECTIONS","timestamp": "1111-11-11T11:11:11Z","total_energy_consumption_kwh": 534411.606,"voltage_average_ll_v": 397.775177,"voltage_average_ln_v": 229.660141,"voltage_l1_l2_v": 397.415558,"voltage_l1_n_v": 230.020218,"voltage_l2_l3_v": 397.431,"voltage_l2_n_v": 229.250214,"voltage_l3_l1_v": 398.478973,"voltage_l3_n_v": 229.709946}'
+ fields = 'S4_1;77777777;534411.606;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'S4_2 iem3000 77777777 NOKEY'
+ telegram = 68F6F668080C7277777777A34C150275000000_0783FF090700000000000000874003BEF65B0000000000874083FF09027614000000000004EDFF0C000001010783FF0D5679DA1F00000000874083FF0DBEF65B00000000000783FF01C9157E0A000000000783FF028484A70A000000000783FF0364E0B40A0000000004EDFF0E0000010107FD61000000000000000003FF10000000871003000000000000000087200300000000000000008730030000000000000000878010030000000000000000046D0C16050203FF2C64000003FF2D00000005FF2E0000C84205FF2F0000FA4303FF3000000003FD1B00000002FF32000003FD1AFFFF001F_fd16
+ json = '{"_": "telegram","active_tariff_nr": 0,"digital_input": "","digital_input_association": "","digital_input_status": "","digital_output": "OUTPUT_FFFF","digital_output_association": "","id": "77777777","input_metering_cumulation_counter": 0,"last_input_metering_reset_datetime": "2000-01-01 00:00","last_partial_energy_reset_datetime": "2000-01-01 00:00","media": "electricity","driver": "iem3000","meter_datetime": "2000-02-05 22:12","name": "S4_2","partial_energy_consumption_kwh": 534411.606,"partial_reactive_energy_consumption_kvarh": 6026.942,"pulse_constant_counter": 500,"pulse_duration_counter": 100,"pulse_weight_counter": 100,"status": "OK","timestamp": "1111-11-11T11:11:11Z","total_energy_consumption_l1_kwh": 176035.273,"total_energy_consumption_l2_kwh": 178750.596,"total_energy_consumption_l3_kwh": 179626.084,"total_energy_consumption_tariff_1_kwh": 0,"total_energy_consumption_tariff_2_kwh": 0,"total_energy_consumption_tariff_3_kwh": 0,"total_energy_consumption_tariff_4_kwh": 0,"total_energy_production_kwh": 0.007,"total_reactive_energy_consumption_kvarh": 6026.942,"total_reactive_energy_production_kvarh": 1340.93}'
+ fields = 'S4_2;77777777;null;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'S4_3 iem3000 77777777 NOKEY'
+ telegram = 68C6C668080C7277777777A34C150276000000_02FF34000005FF350000803F02FF36000002FF37000002FF38000004EDFF390000010105FF3A0000C0FF06FF2076B3090C000003FF2103000003FF2204000003FF230B000003FF243200000503BA7802490583FF094260E53B8540038957BC45854083FF09C39DA7440583FF0DBA780249854083FF0D8957BC450583FF01D2E82B480583FF02A68F2E480583FF03856A2F4805FD610000000085100300000000852003000000008530030000000085801003000000000F_d016
+ json = '{"_": "telegram","activated_status": "","digital_output_association_2": "","id": "77777777","input_metering_cumulation_counter": 0,"last_alarm_datetime": "2000-01-01 00:00","last_alarm_nr": null,"media": "electricity","driver": "iem3000","name": "S4_3","nominal_frequency_hz": 50,"operating_time_y": 201962358,"overload_alarm_setup": "","partial_energy_consumption_kwh": 534.411625,"partial_reactive_energy_consumption_kvarh": 6.026942,"phases_counter": 3,"pickup_setpoint_counter": 1,"power_system_configuration": "POWER_SYS_CONFIG_B","status": "OK","timestamp": "1111-11-11T11:11:11Z","total_energy_consumption_kwh": 534.411625,"total_energy_consumption_l1_kwh": 176.035281,"total_energy_consumption_l2_kwh": 178.750594,"total_energy_consumption_l3_kwh": 179.626078,"total_energy_consumption_tariff_1_kwh": 0,"total_energy_consumption_tariff_2_kwh": 0,"total_energy_consumption_tariff_3_kwh": 0,"total_energy_consumption_tariff_4_kwh": 0,"total_energy_production_kwh": 0.000007,"total_reactive_energy_consumption_kvarh": 6.026942,"total_reactive_energy_production_kvarh": 1.34093,"unack_status": "","wires_nr": 4}'
+ fields = 'S4_3;77777777;534.411625;1111-11-11 11:11.11'
+ }
+ }
+}
diff --git a/components/wmbus_common/drivers/src/ime.xmq b/components/wmbus_common/drivers/src/ime.xmq
index c68cecfb..fae09c6e 100644
--- a/components/wmbus_common/drivers/src/ime.xmq
+++ b/components/wmbus_common/drivers/src/ime.xmq
@@ -786,28 +786,28 @@ driver {
args = 'MyMeter ime 12345678 NOKEY'
comment = 'Telegram 1'
telegram = 68CBCB6808017278563412A525660200000000_849010FF80843B362A0C00849010FF80843C00000000849010FF81843BD1490700849010FF81843C060000008410FF80843BFE180C008420FF80843B381100008410FF80843C000000008420FF80843C000000008410FF81843B393D07008420FF81843B980C00008410FF81843C060000008420FF81843C0000000084A010FF80843B362A0C0084A010FF80843C0000000084A010FF81843BD149070084A010FF81843C0600000004FF90290000000002FF912B00001F0000000000_4116
- json = '{"_":"telegram","id": "12345678","media": "electricity","meter": "ime","name": "MyMeter","negative_tariff1_active_energy_kwh": 0,"negative_tariff1_reactive_energy_kvarh": 0.06,"negative_tariff2_active_energy_kwh": 0,"negative_tariff2_reactive_energy_kvarh": 0,"negative_total_active_energy_kwh": 0,"negative_total_reactive_energy_kvarh": 0.06,"partial_negative_active_energy_kwh": 0,"partial_negative_reactive_energy_kvarh": 0.06,"partial_positive_active_energy_kwh": 7972.38,"partial_positive_reactive_energy_kvarh": 4776.49,"positive_tariff1_active_energy_kwh": 7928.3,"positive_tariff1_reactive_energy_kvarh": 4744.25,"positive_tariff2_active_energy_kwh": 44.08,"positive_tariff2_reactive_energy_kvarh": 32.24,"positive_total_active_energy_kwh": 7972.38,"positive_total_reactive_energy_kvarh": 4776.49,"pulse_input_counter": 0,"pulse_unit": "WH","status":"OK","timestamp": "1111-11-11T11:11:11Z"}'
+ json = '{"_":"telegram","id": "12345678","media": "electricity","driver": "ime","name": "MyMeter","negative_tariff1_active_energy_kwh": 0,"negative_tariff1_reactive_energy_kvarh": 0.06,"negative_tariff2_active_energy_kwh": 0,"negative_tariff2_reactive_energy_kvarh": 0,"negative_total_active_energy_kwh": 0,"negative_total_reactive_energy_kvarh": 0.06,"partial_negative_active_energy_kwh": 0,"partial_negative_reactive_energy_kvarh": 0.06,"partial_positive_active_energy_kwh": 7972.38,"partial_positive_reactive_energy_kvarh": 4776.49,"positive_tariff1_active_energy_kwh": 7928.3,"positive_tariff1_reactive_energy_kvarh": 4744.25,"positive_tariff2_active_energy_kwh": 44.08,"positive_tariff2_reactive_energy_kvarh": 32.24,"positive_total_active_energy_kwh": 7972.38,"positive_total_reactive_energy_kvarh": 4776.49,"pulse_input_counter": 0,"pulse_unit": "WH","status":"OK","timestamp": "1111-11-11T11:11:11Z"}'
fields = 'MyMeter;12345678;7972.38;1111-11-11 11:11.11'
}
test {
args = 'MyMeter ime 12345678 NOKEY'
comment = 'Telegram 2'
telegram = 688D8D6808017278563412A525660201000000_84B010FF842B06000000848020FF842B00000000849020FF842B0600000084A020FF842B0000000084B010FF852B00000000848020FF852B00000000849020FF852B0000000084A020FF852B0000000084B010FF862B08000000848020FF862B00000000849020FF862B0800000084A020FF862B000000001F0000000000_D116
- json = '{"_":"telegram","active_power_l1_kw": 0,"active_power_l2_kw": 0.006,"active_power_l3_kw": 0,"apparent_power_l1_kva": 0,"apparent_power_l2_kva": 0.008,"apparent_power_l3_kva": 0,"id": "12345678","media": "electricity","meter": "ime","name": "MyMeter","reactive_power_l1_kvar": 0,"reactive_power_l2_kvar": 0,"reactive_power_l3_kvar": 0,"three_phase_total_active_power_kw": 0.006,"three_phase_total_apparent_power_kva": 0.008,"three_phase_total_reactive_power_kvar": 0,"status":"OK","timestamp": "1111-11-11T11:11:11Z"}'
+ json = '{"_":"telegram","active_power_l1_kw": 0,"active_power_l2_kw": 0.006,"active_power_l3_kw": 0,"apparent_power_l1_kva": 0,"apparent_power_l2_kva": 0.008,"apparent_power_l3_kva": 0,"id": "12345678","media": "electricity","driver": "ime","name": "MyMeter","reactive_power_l1_kvar": 0,"reactive_power_l2_kvar": 0,"reactive_power_l3_kvar": 0,"three_phase_total_active_power_kw": 0.006,"three_phase_total_apparent_power_kva": 0.008,"three_phase_total_reactive_power_kvar": 0,"status":"OK","timestamp": "1111-11-11T11:11:11Z"}'
fields = 'MyMeter;12345678;null;1111-11-11 11:11.11'
}
test {
args = 'MyMeter ime 12345678 NOKEY'
comment = 'Telegram 3'
telegram = 6875756808017278563412A525660202000000_848020FF8748F6080000849020FF87489F08000084A020FF8748E2080000848020FF88482D0F0000849020FF8848460F000084A020FF8848640F0000848020FF895900000000849020FF89592600000084A020FF89590000000002FF8A48F4011F0000000000_B116
- json = '{"_":"telegram","current_l1_a": 0,"current_l2_a": 0.038,"current_l3_a": 0,"frequency_hz": 50,"id": "12345678","media": "electricity","meter": "ime","name": "MyMeter","status":"OK","timestamp": "1111-11-11T11:11:11Z","voltage_1_2_v": 388.5,"voltage_1_n_v": 229.4,"voltage_2_3_v": 391,"voltage_2_n_v": 220.7,"voltage_3_1_v": 394,"voltage_3_n_v": 227.4}'
+ json = '{"_":"telegram","current_l1_a": 0,"current_l2_a": 0.038,"current_l3_a": 0,"frequency_hz": 50,"id": "12345678","media": "electricity","driver": "ime","name": "MyMeter","status":"OK","timestamp": "1111-11-11T11:11:11Z","voltage_1_2_v": 388.5,"voltage_1_n_v": 229.4,"voltage_2_3_v": 391,"voltage_2_n_v": 220.7,"voltage_3_1_v": 394,"voltage_3_n_v": 227.4}'
fields = 'MyMeter;12345678;null;1111-11-11 11:11.11'
}
test {
args = 'MyMeter ime 12345678 NOKEY'
comment = 'Telegram 4'
telegram = 685C5C6808017278563412A525660203000000_82B010FF8B28EB0282B010FF8C2B01008410FF8D2B070000008410FF8E2B691D00008420FF8E2BC50D000084B010FF8F21BB260C008410FF8F215A1C0C008420FF8F21610A00000F0000000000_6216
- json = '{"_":"telegram","id": "12345678","max_total_active_power_tariff1_kw": 7.529,"max_total_active_power_tariff2_kw": 3.525,"media": "electricity","meter": "ime","name": "MyMeter","power_factor_sector": "INDUCTIVE","run_meter_tariff1_h": 13228.166667,"run_meter_tariff2_h": 44.283333,"run_meter_total_h": 13272.45,"three_phase_power_factor": 0.747,"status":"OK","timestamp": "1111-11-11T11:11:11Z","total_active_power_requirement_kw": 0.007}'
+ json = '{"_":"telegram","id": "12345678","max_total_active_power_tariff1_kw": 7.529,"max_total_active_power_tariff2_kw": 3.525,"media": "electricity","driver": "ime","name": "MyMeter","power_factor_sector": "INDUCTIVE","run_meter_tariff1_h": 13228.166667,"run_meter_tariff2_h": 44.283333,"run_meter_total_h": 13272.45,"three_phase_power_factor": 0.747,"status":"OK","timestamp": "1111-11-11T11:11:11Z","total_active_power_requirement_kw": 0.007}'
fields = 'MyMeter;12345678;null;1111-11-11 11:11.11'
}
}
diff --git a/components/wmbus_common/drivers/src/iperl.xmq b/components/wmbus_common/drivers/src/iperl.xmq
index 49fbdec1..5302ff5f 100644
--- a/components/wmbus_common/drivers/src/iperl.xmq
+++ b/components/wmbus_common/drivers/src/iperl.xmq
@@ -4,16 +4,23 @@ driver {
meter_type = WaterMeter
default_fields = name,id,total_m3,max_flow_m3h,timestamp
manufacturer = Sensus
- model = iperl
+ model = 'Sensus iPERL'
detect {
mvt = SEN,68,06
mvt = SEN,68,07
mvt = SEN,7c,07
}
fields {
+ field {
+ name = status
+ quantity = Text
+ info = 'Meter status including the tpl status.'
+ attributes = STATUS,INCLUDE_TPL_STATUS
+ }
field {
name = total
quantity = Volume
+ change = Increasing
info = 'The total water consumption.'
match {
measurement_type = Instantaneous
@@ -23,6 +30,7 @@ driver {
field {
name = max_flow
quantity = Flow
+ change = Instant
info = 'The maximum water flow recorded during previous period.'
match {
measurement_type = Instantaneous
@@ -35,14 +43,14 @@ driver {
args = 'MoreWater iperl 12345699 NOKEY'
comment = 'Test iPerl T1 telegram, that after decryption, has 2f2f markers.'
telegram = 1E44AE4C9956341268077A36001000_2F2F0413181E0000023B00002F2F2F2F
- json = '{"_":"telegram","media":"water","meter":"iperl","name":"MoreWater","id":"12345699","total_m3":7.704,"max_flow_m3h":0,"timestamp":"1111-11-11T11:11:11Z"}'
+ json = '{"_":"telegram","media":"water","driver":"iperl","name":"MoreWater","id":"12345699","total_m3":7.704,"max_flow_m3h":0,"status":"OK","timestamp":"1111-11-11T11:11:11Z"}'
fields = 'MoreWater;12345699;7.704;0;1111-11-11 11:11.11'
}
test {
args = 'WaterWater iperl 33225544 NOKEY'
comment = 'Test iPerl T1 telegram not encrypted, which has no 2f2f markers.'
telegram = 1844AE4C4455223368077A55000000_041389E20100023B0000
- json = '{"_":"telegram","media":"water","meter":"iperl","name":"WaterWater","id":"33225544","total_m3":123.529,"max_flow_m3h":0,"timestamp":"1111-11-11T11:11:11Z"}'
+ json = '{"_":"telegram","media":"water","driver":"iperl","name":"WaterWater","id":"33225544","total_m3":123.529,"max_flow_m3h":0,"status":"OK","timestamp":"1111-11-11T11:11:11Z"}'
fields = 'WaterWater;33225544;123.529;0;1111-11-11 11:11.11'
}
}
diff --git a/components/wmbus_common/drivers/src/istaheat.xmq b/components/wmbus_common/drivers/src/istaheat.xmq
index 0e24cddc..a0ed45aa 100644
--- a/components/wmbus_common/drivers/src/istaheat.xmq
+++ b/components/wmbus_common/drivers/src/istaheat.xmq
@@ -1,13 +1,16 @@
/* Copyright (C) 2024 Fredrik Öhrström (gpl-3.0-or-later)
Copyright (C) 2024 Arthur van Dorp (gpl-3.0-or-later)
+ Copyright (C) 2026 Bartosz Biegun (gpl-3.0-or-later)
- Ista sensonic 3 heat meter sending on c1, product number 4030020.
+ Ista sensonic 3 heat meter sending on c1.
+ Tested product numbers: 4030020, 51206.
+ Not-tested possible compatible product numbers: 51200, 51201, 51202, 51207, 51208.
(Can also be used for cold metering, I don't think the device I own is configured for that use case.)
(Attention device owners: Do not click randomly on the single button of the meter. If you are in the
wireless service loop and click wrongly, you will activate istas proprietary wireless protocol. Only ista
- will be able to reset the device and activate the wireless m-bus. To activate the wireless m-bus you have
- to long click until 2A is shown, then wait shortly, click *once", wait until 2B is shown, wait again,
- single click until 2C is shown, then double click. Do not double click in 2A or 2B.)
+ will be able to reset the device and activate the wireless m-bus. To activate the wireless m-bus (aka OMS
+ mode) you have to long click until 2A is shown, then wait shortly, click *once", wait until 2B is shown,
+ wait again, single click until 2C is shown, then double click. Do not double click in 2A or 2B.)
The device measures every 8s, but sends wireless m-bus telegrams every 4 minutes.
AES key has to be obtained from your contractor or directly from ista. */
@@ -17,6 +20,7 @@ driver {
default_fields = name,id,status,total_kwh,target_kwh,timestamp
detect {
mvt = IST,a9,04
+ mvt = IST,a9,0c
}
fields {
field {
@@ -91,14 +95,20 @@ driver {
test {
args = 'HeatItUp istaheat 33503169 NOKEY'
telegram = 5344742669315033A9048C2070900F002C25961200009D6949E80EB1E2707A96003007102F2F_0C0500000000426C00004C050000000082016CFE298C0105000000008C0115000000002F2F2F2F2F2F2F2F2F2F2F
- json = '{"_":"telegram","id": "33503169","last_year_date": "1999-11-30","last_year_kwh": 0,"media": "heat","meter": "istaheat","name": "HeatItUp","status": "OK","target_date": "2023-09-30","target_kwh": 0,"target_m3": 0,"timestamp": "1111-11-11T11:11:11Z","total_kwh": 0}'
+ json = '{"_":"telegram","id": "33503169","last_year_date": "1999-11-30","last_year_kwh": 0,"media": "heat","driver": "istaheat","name": "HeatItUp","status": "OK","target_date": "2023-09-30","target_kwh": 0,"target_m3": 0,"timestamp": "1111-11-11T11:11:11Z","total_kwh": 0}'
fields = 'HeatItUp;33503169;OK;0;0;1111-11-11 11:11.11'
}
test {
args = 'FeelTheHeat istaheat 44503169 NOKEY'
telegram = 5344742669315044A9048C2017900F002C253DCD0000CE827C98B4346AB67A3D003007102F2F_0C0514980400426CFF2C4C052061020082016C1F318C0105581604008C0115142800002F2F2F2F2F2F2F2F2F2F2F
- json = '{"_":"telegram","status":"OK","media":"heat","meter":"istaheat","name":"FeelTheHeat","id":"44503169","last_year_kwh":2612,"last_year_date":"2023-12-31","target_kwh":4165.8,"target_date":"2024-01-31","target_m3":281.4,"total_kwh":4981.4,"timestamp":"1111-11-11T11:11:11Z"}'
+ json = '{"_":"telegram","status":"OK","media":"heat","driver":"istaheat","name":"FeelTheHeat","id":"44503169","last_year_kwh":2612,"last_year_date":"2023-12-31","target_kwh":4165.8,"target_date":"2024-01-31","target_m3":281.4,"total_kwh":4981.4,"timestamp":"1111-11-11T11:11:11Z"}'
fields = 'FeelTheHeat;44503169;OK;4981.4;4165.8;1111-11-11 11:11.11'
}
+ test {
+ args = 'NoHeatYet istaheat 54900708 NOKEY'
+ telegram = 5344742608079054A90C8C20A3900F002C2509530000307906D95D07027E7A09003007102F2F_0C0E00000000426C3F3C4C0E0000000082016C5F378C010E000000008C0115000000002F2F2F2F2F2F2F2F2F2F2F
+ json = '{"_":"telegram","media":"heat volume at inlet","driver":"istaheat","name":"NoHeatYet","id":"54900708","last_year_kwh":0,"last_year_date":"2025-12-31","target_kwh":0,"target_m3":0,"target_date":"2026-07-31","total_kwh":0,"status":"OK","timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'NoHeatYet;54900708;OK;0;0;1111-11-11 11:11.11'
+ }
}
}
diff --git a/components/wmbus_common/drivers/src/istawater.xmq b/components/wmbus_common/drivers/src/istawater.xmq
new file mode 100644
index 00000000..7d1b2b76
--- /dev/null
+++ b/components/wmbus_common/drivers/src/istawater.xmq
@@ -0,0 +1,54 @@
+/* Copyright (C) 2024 Fredrik Öhrström (gpl-3.0-or-later)
+ Copyright (C) 2026 Bartosz Biegun (gpl-3.0-or-later)
+
+ Ista radio net 3 configured in OMS mode sending c1.
+ Tested product numbers: 19322. */
+driver {
+ name = istawater
+ meter_type = WaterMeter
+ default_fields = name,id,total_m3,target_m3,timestamp
+ detect {
+ mvt = IST,31,07
+ mvt = IST,33,07
+ }
+ library {
+ use = total_m3
+ }
+ fields {
+ field {
+ name = target
+ quantity = Volume
+ info = 'The total water consumption for the previous month period.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = Volume
+ storage_nr = 2
+ }
+ }
+ field {
+ name = target
+ quantity = PointInTime
+ info = 'Last day of previous billing month.'
+ display_unit = date
+ match {
+ measurement_type = Instantaneous
+ vif_range = Date
+ storage_nr = 2
+ }
+ }
+ }
+ tests {
+ test {
+ args = 'I istawater 22705729 NOKEY'
+ telegram = 434474262957702231078c2095900f002c25d744B09e30930afB644687f97ad7002007102f2f0c136960320082016c1e398c0113626032002f2f2f2f2f2f2f2f2f2f2f2f
+ json = '{"_": "telegram","id": "22705729","media": "water","driver": "istawater","name": "I","target_date": "2024-09-30","target_m3": 326.062,"timestamp": "1111-11-11T11:11:11Z","total_m3": 326.069}'
+ fields = 'I;22705729;326.069;326.062;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'WaterIsCold istawater 50104916 NOKEY'
+ telegram = 434474261649105033078C208F900F002C258B0B000008383D83832E14A47A8B002007102F2F_0C139166000082016C5F318C011321540000C20F6C4932EA0F3A00002F2F
+ json = '{"_": "telegram","id": "50104916","media": "water","driver": "istawater","name": "WaterIsCold","target_date": "2026-01-31","target_m3": 5.421,"timestamp": "1111-11-11T11:11:11Z","total_m3": 6.691}'
+ fields = 'WaterIsCold;50104916;6.691;5.421;1111-11-11 11:11.11'
+ }
+ }
+}
diff --git a/components/wmbus_common/drivers/src/itron.xmq b/components/wmbus_common/drivers/src/itron.xmq
new file mode 100644
index 00000000..4f10ce18
--- /dev/null
+++ b/components/wmbus_common/drivers/src/itron.xmq
@@ -0,0 +1,113 @@
+// Copyright (C) 2022-2023 Fredrik Öhrström (gpl-3.0-or-later)
+driver {
+ name = itron
+ meter_type = WaterMeter
+ default_fields = name,id,total_m3,target_m3,timestamp
+ detect {
+ mvt = ITW,00,07
+ mvt = ITW,03,07
+ mvt = ITW,33,07
+ mvt = ITW,00,16
+ }
+ library {
+ use = enhanced_id
+ use = meter_datetime
+ use = total_m3
+ use = total_backward_m3
+ use = volume_flow_m3h
+ }
+ fields {
+ field {
+ name = status
+ quantity = Text
+ info = 'Status and error flags.'
+ attributes = STATUS,INCLUDE_TPL_STATUS
+ match {
+ measurement_type = Instantaneous
+ vif_range = ErrorFlags
+ add_combinable = RecordErrorCodeMeterToController
+ }
+ lookup {
+ name = ERROR_FLAGS
+ map_type = BitToString
+ mask_bits = 0xffffff
+ default_message = OK
+ }
+ }
+ field {
+ name = target
+ quantity = Volume
+ info = 'The total water consumption recorded at the end of previous billing period.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = Volume
+ storage_nr = 1
+ }
+ }
+ field {
+ name = target
+ quantity = PointInTime
+ info = 'Date when previous billing period ended.'
+ display_unit = date
+ match {
+ measurement_type = Instantaneous
+ vif_range = Date
+ storage_nr = 1
+ }
+ }
+ field {
+ name = unknowna
+ quantity = Text
+ info = 'Unknown flags.'
+ match {
+ difvifkey = 047F
+ }
+ lookup {
+ name = WOOTA
+ map_type = BitToString
+ mask_bits = 0xffffffff
+ }
+ }
+ field {
+ name = unknownb
+ quantity = Text
+ info = 'Unknown flags.'
+ match {
+ difvifkey = 027F
+ }
+ lookup {
+ name = WOOTB
+ map_type = BitToString
+ mask_bits = 0xffff
+ }
+ }
+ }
+ tests {
+ test {
+ args = 'SomeWater itron 12345698 NOKEY'
+ comment = 'Test ITRON T1 telegram not encrypted, which has no 2f2f markers.'
+ telegram = 384497269856341203077AD90000A0_0413FD110000066D2C1AA1D521004413300F0000426CBF2C047F0000060C027F862A0E79678372082100
+ json = '{"_":"telegram","media":"water","driver":"itron","name":"SomeWater","id":"12345698","enhanced_id":"002108728367","meter_datetime":"2022-01-21 01:26:44","total_m3":4.605,"status":"OK","target_m3":3.888,"target_date":"2021-12-31","unknowna":"WOOTA_C060000","unknownb":"WOOTB_2A86","timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'SomeWater;12345698;4.605;3.888;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'MoreWater itron 18000056 NOKEY'
+ telegram = 46449726560000183307725600001897263307AF0030052F2F_066D0E1015C82A000C13771252000C933C000000000B3B0400004C1361045200426CC12A03FD971C0000002F2F2F
+ json = '{"_":"telegram","media":"water","driver":"itron","name":"MoreWater","id":"18000056","meter_datetime":"2022-10-08 21:16:14","total_m3":521.277,"total_backward_m3":0,"volume_flow_m3h":0.004,"status":"OK","target_m3":520.461,"target_date":"2022-10-01","timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'MoreWater;18000056;521.277;520.461;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'AnyWater itron 20310959 NOKEY'
+ telegram = 384497265909312000077a930000a0041360B50100066d101295f427004413ac570100426cdf2c047f0000060c027f6c2a0e79000000000000
+ json = '{"_":"telegram","enhanced_id":"000000000000","id":"20310959","media":"water","driver":"itron","meter_datetime":"2023-07-20 21:18:16","name":"AnyWater","status":"OK","target_date":"2022-12-31","target_m3":87.98,"timestamp":"1111-11-11T11:11:11Z","total_m3":111.968,"unknowna":"WOOTA_C060000","unknownb":"WOOTB_2A6C"}'
+ fields = 'AnyWater;20310959;111.968;87.98;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'ColdWaterMeter itron 23362098 NOKEY'
+ comment = 'Allmess cold water with Itron Module programmed with type 0x16'
+ telegram = 3A4497269820362300167AF60020A52F2F_04132E100000066D03260DE12B007413FEFEFEFE426C1F01047F1600060C027F9A2A0E79187103002300
+ json = '{"_":"telegram","enhanced_id":"002300037118","id":"23362098","media":"cold water","driver":"itron","meter_datetime":"2023-11-01 13:38:03","name":"ColdWaterMeter","status":"OK","target_date":"2000-01-31","timestamp":"1111-11-11T11:11:11Z","total_m3":4.142,"unknowna":"WOOTA_C060016","unknownb":"WOOTB_2A9A"}'
+ fields = 'ColdWaterMeter;23362098;4.142;null;1111-11-11 11:11.11'
+ }
+ }
+}
diff --git a/components/wmbus_common/drivers/src/itronheat.xmq b/components/wmbus_common/drivers/src/itronheat.xmq
index ced40599..07b84eb0 100644
--- a/components/wmbus_common/drivers/src/itronheat.xmq
+++ b/components/wmbus_common/drivers/src/itronheat.xmq
@@ -62,14 +62,14 @@ driver {
args = 'Heat itronheat 23340485 NOKEY'
comment = 'Allmess UltraMaXX with ITRON EquaScan hMIU RF Module'
telegram = 444497268504342300047AD00030A52F2F_04062C0100000C1429270000066D2D130AE12B007406FEFEFEFE426C1F010D7FEB0E00000006040C995500372F2F0C7951622223
- json = '{"_":"telegram","id": "23340485","last_year_date": "2000-01-31","media": "heat","meter": "itronheat","meter_datetime": "2023-11-01 10:19:45","name": "Heat","status": "OK","timestamp": "1111-11-11T11:11:11Z","total_kwh": 300,"total_m3": 27.29}'
+ json = '{"_":"telegram","id": "23340485","last_year_date": "2000-01-31","media": "heat","driver": "itronheat","meter_datetime": "2023-11-01 10:19:45","name": "Heat","status": "OK","timestamp": "1111-11-11T11:11:11Z","total_kwh": 300,"total_m3": 27.29}'
fields = 'Heat;23340485;OK;300;1111-11-11 11:11.11'
}
test {
args = 'Heat_lower_case itronheat 11755757 NOKEY'
comment = 'Itron Heatmeter with strange MFCT iTW instead of ITW'
telegram = 404497A65757751100047A040000A00406494C0000041348318000066D2B0ED7EE2A0044069A360000426CDF2C0D7FEB0700000006040C5C7E034E0C7942502321
- json = '{"_":"telegram","id": "11755757","last_year_date": "2022-12-31","last_year_kwh": 13978,"media": "heat","meter": "itronheat","meter_datetime": "2023-10-14 23:14:43","name": "Heat_lower_case","status": "OK","timestamp": "1111-11-11T11:11:11Z","total_kwh": 19529,"total_m3": 8401.224}'
+ json = '{"_":"telegram","id": "11755757","last_year_date": "2022-12-31","last_year_kwh": 13978,"media": "heat","driver": "itronheat","meter_datetime": "2023-10-14 23:14:43","name": "Heat_lower_case","status": "OK","timestamp": "1111-11-11T11:11:11Z","total_kwh": 19529,"total_m3": 8401.224}'
fields = 'Heat_lower_case;11755757;OK;19529;1111-11-11 11:11.11'
}
}
diff --git a/components/wmbus_common/drivers/src/iwmtx5.xmq b/components/wmbus_common/drivers/src/iwmtx5.xmq
new file mode 100644
index 00000000..dfa2f41f
--- /dev/null
+++ b/components/wmbus_common/drivers/src/iwmtx5.xmq
@@ -0,0 +1,51 @@
+// Copyright (C) 2023 Fredrik Öhrström (gpl-3.0-or-later)
+driver {
+ name = iwmtx5
+ meter_type = WaterMeter
+ default_fields = name,id,status,total_m3,timestamp
+ detect {
+ mvt = BMT,18,07
+ mvt = BMT,18,06
+ }
+ library {
+ use = meter_datetime
+ use = total_m3
+ }
+ mfct_tpl_status_bits {
+ mask_bits = 0xe0
+ default_message = OK
+ map {
+ name = TAMPER
+ value = 0x40
+ test = set
+ }
+ }
+ fields {
+ field {
+ name = status
+ quantity = Text
+ info = 'Status and error flags.'
+ attributes = STATUS,INCLUDE_TPL_STATUS
+ }
+ }
+ tests {
+ test {
+ args = 'WaterWater iwmtx5 22917370 00000000000000000000000000000000'
+ telegram = 5144b4097073912218078c00247a0308400571e9615249ede52eaae09f61908f027c3877f3330ae9079528b23173ce124bcc255393e60b173c0a9f274c42dd92e4b23c14e8a41f042903358df01dd9268ad4
+ json = '{"_":"telegram","id": "22917370","media": "water","driver": "iwmtx5","meter_datetime": "2023-05-11 10:38:24","name": "WaterWater","status": "PERMANENT_ERROR","timestamp": "1111-11-11T11:11:11Z","total_m3": 0.025}'
+ fields = 'WaterWater;22917370;PERMANENT_ERROR;0.025;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'WarmWater2 iwmtx5 23329344 NOKEY'
+ telegram = 4244B4094493322318068C005B7A1C0000000C13072000000F05170000000000000000000000000000000000000000009D0000C20000C20000C8000000000000000000
+ json = '{"_":"telegram","id": "23329344","media": "warm water","driver": "iwmtx5","name": "WarmWater2","status": "OK","timestamp": "1111-11-11T11:11:11Z","total_m3": 2.007}'
+ fields = 'WarmWater2;23329344;OK;2.007;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'WarmWater3 iwmtx5 24360570 NOKEY'
+ telegram = 4A44B4097005362418068C00647A7D400000066D210A673139000C13589402000F0582030000550200660300C004000006005407009B0800030A00C10A00000000000000010000FC000000
+ json = '{"_": "telegram","id": "24360570","media": "warm water","driver": "iwmtx5","meter_datetime": "2025-09-17 07:10:33","name": "WarmWater3","status": "TAMPER","timestamp": "1111-11-11T11:11:11Z","total_m3": 29.458}'
+ fields = 'WarmWater3;24360570;TAMPER;29.458;1111-11-11 11:11.11'
+ }
+ }
+}
diff --git a/components/wmbus_common/drivers/src/izarv2.xmq b/components/wmbus_common/drivers/src/izarv2.xmq
new file mode 100644
index 00000000..89168aa0
--- /dev/null
+++ b/components/wmbus_common/drivers/src/izarv2.xmq
@@ -0,0 +1,286 @@
+// Copyright (C) 2019 Jacek Tomasiak (gpl-3.0-or-later)
+// Copyright (C) 2020-2025 Fredrik Öhrström (gpl-3.0-or-later)
+// Copyright (C) 2021 Vincent Privat (gpl-3.0-or-later)
+driver {
+ name = izarv2
+ meter_type = WaterMeter
+ default_fields = name,id,status,total_m3,target_m3,timestamp
+ manufacturer = Diehl
+ transform_payload = diehl_prios
+ detect {
+ mvt = HYD,85,07
+ mvt = SAP,*,15
+ mvt = SAP,*,04
+ mvt = SAP,00,07
+ mvt = DME,78,07
+ mvt = DME,78,06
+ mvt = HYD,86,07
+ }
+ fields {
+ field {
+ name = prefix
+ quantity = Text
+ info = 'The alphanumeric prefix printed before serial number on device.'
+ }
+ field {
+ name = serial_number
+ quantity = Text
+ info = 'The meter serial number.'
+ }
+ field {
+ name = manufacture_y
+ quantity = Text
+ info = 'The year during which the meter was manufactured.'
+ }
+ field {
+ name = prios_decoder
+ quantity = Text
+ attributes = HIDE
+ match_entire_payload = true
+ ixml = "decode = with_billing | without_billing.
+ with_billing = alarms_hdr, -skip_byte, -check, volume, billing_block, -byte*.
+ without_billing = alarms_hdr, -skip_byte, -check, volume.
+ alarms_hdr = byte, byte, byte, @DV_alarms.
+ -skip_byte = byte.
+ -check = byte.
+ volume = byte, byte, byte, byte, @DV_total.
+ billing_block = last_month, date.
+ last_month = byte, byte, byte, byte, @DV_last_month.
+ date = byte, byte, @DV_date.
+ -hex = ['A'-'F'; '0'-'9'].
+ -byte = hex, hex.
+ DV_alarms>dvk = +'036C'.
+ DV_total>dvk = +'0413'.
+ DV_last_month>dvk = +'4413'.
+ DV_date>dvk = +'026C'."
+ }
+ field {
+ name = total
+ quantity = Volume
+ info = 'The total water consumption recorded by this meter.'
+ match {
+ difvifkey = 0413
+ }
+ }
+ field {
+ name = target
+ quantity = Volume
+ info = 'The total water consumption recorded at the end of last month.'
+ match {
+ difvifkey = 4413
+ }
+ }
+ field {
+ name = status_raw
+ quantity = Dimensionless
+ attributes = HIDE
+ match {
+ difvifkey = 036C
+ }
+ }
+ field {
+ name = tx_raw
+ quantity = Dimensionless
+ attributes = HIDE
+ calculate = 'status_raw_counter & 15counter'
+ }
+ field {
+ name = bat_raw
+ quantity = Dimensionless
+ attributes = HIDE
+ calculate = '(status_raw_counter >> 8counter) & 31counter'
+ }
+ field {
+ name = target_date_raw
+ quantity = Dimensionless
+ attributes = HIDE
+ match {
+ difvifkey = 026C
+ }
+ }
+ field {
+ name = target_date_day
+ quantity = Dimensionless
+ attributes = HIDE
+ calculate = 'target_date_raw_counter % 32counter'
+ match {
+ difvifkey = 026C
+ }
+ }
+ field {
+ name = target_date_month
+ quantity = Dimensionless
+ attributes = HIDE
+ calculate = '(target_date_raw_counter >> 8counter) % 16counter'
+ match {
+ difvifkey = 026C
+ }
+ }
+ field {
+ name = target_date_year_offset
+ quantity = Dimensionless
+ attributes = HIDE
+ calculate = '((((target_date_raw_counter >> 8counter) & 240counter) >> 1counter) + ((target_date_raw_counter >> 5counter) % 8counter))'
+ match {
+ difvifkey = 026C
+ }
+ }
+ field {
+ name = target_date_year_adjusted
+ quantity = Dimensionless
+ attributes = HIDE
+ calculate = 'target_date_year_offset_counter - ((target_date_year_offset_counter > 80counter) * 100counter)'
+ match {
+ difvifkey = 026C
+ }
+ }
+ field {
+ name = target_date_month_offset
+ quantity = Dimensionless
+ attributes = HIDE
+ calculate = '(target_date_year_adjusted_counter * 12counter) + target_date_month_counter - 1counter'
+ match {
+ difvifkey = 026C
+ }
+ }
+ field {
+ name = target
+ quantity = PointInTime
+ display_unit = date
+ info = 'The date when the meter recorded the most recent billing value.'
+ calculate = "'2000-01-01' +
+ (target_date_month_offset_counter * 1month) +
+ ((target_date_day_counter - 1counter) * 24h)"
+ match {
+ difvifkey = 026C
+ }
+ }
+ field {
+ name = battery
+ quantity = Time
+ display_unit = y
+ info = 'How many more years the battery is expected to last.'
+ calculate = 'bat_raw_counter * 0.5y'
+ }
+ field {
+ name = transmit_period
+ quantity = Time
+ display_unit = s
+ info = 'The period at which the meter transmits its data.'
+ calculate = '(1counter << (tx_raw_counter + 2counter)) * 1s'
+ }
+ field {
+ name = status
+ quantity = Text
+ info = 'Current and previous alarms reported by the meter.'
+ match {
+ difvifkey = 036C
+ }
+ lookup {
+ name = ALARMS
+ map_type = BitToString
+ mask_bits = 0xFFE000
+ default_message = OK
+ map {
+ name = LEAKAGE
+ value = 0x008000
+ test = Set
+ }
+ map {
+ name = METER_BLOCKED
+ value = 0x002000
+ test = Set
+ }
+ map {
+ name = BACK_FLOW
+ value = 0x800000
+ test = Set
+ }
+ map {
+ name = UNDERFLOW
+ value = 0x400000
+ test = Set
+ }
+ map {
+ name = OVERFLOW
+ value = 0x200000
+ test = Set
+ }
+ map {
+ name = SUBMARINE
+ value = 0x100000
+ test = Set
+ }
+ map {
+ name = SENSOR_FRAUD
+ value = 0x080000
+ test = Set
+ }
+ map {
+ name = MECHANICAL_FRAUD
+ value = 0x020000
+ test = Set
+ }
+ map {
+ name = PREV_LEAKAGE
+ value = 0x004000
+ test = Set
+ }
+ map {
+ name = PREV_SENSOR_FRAUD
+ value = 0x040000
+ test = Set
+ }
+ map {
+ name = PREV_MECHANICAL_FRAUD
+ value = 0x010000
+ test = Set
+ }
+ }
+ }
+ }
+ tests {
+ test {
+ comment = 'HYD meter with SAP_PRIOS encoding: prefix, serial, manufacture_y present'
+ args = 'IzarWater izarv2 21242472 NOKEY'
+ telegram = 1944304C72242421D401A2_013D4013DD8B46A4999C1293E582CC
+ json = '{"_":"telegram","media":"water","driver":"izarv2","name":"IzarWater","id":"21242472","prefix":"C19UA","serial_number":"145842","manufacture_y":"2019","total_m3":3.488,"target_m3":3.486,"target_date":"2019-09-30","battery_y":14.5,"transmit_period_s":8,"status":"METER_BLOCKED UNDERFLOW","timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'IzarWater;21242472;METER_BLOCKED UNDERFLOW;3.488;3.486;1111-11-11 11:11.11'
+ }
+ test {
+ comment = 'SAP meter with PRIOS encoding: no prefix/serial/manufacture_y'
+ args = 'IzarWater2 izarv2 66236629 NOKEY'
+ telegram = 2944A511780729662366A20118001378D3B3DB8CEDD77731F25832AAF3DA8CADF9774EA673172E8C61F2
+ json = '{"_":"telegram","media":"water","driver":"izarv2","name":"IzarWater2","id":"66236629","total_m3":16.76,"target_m3":11.84,"target_date":"2019-11-30","battery_y":12,"transmit_period_s":8,"status":"OK","timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'IzarWater2;66236629;OK;16.76;11.84;1111-11-11 11:11.11'
+ }
+ test {
+ comment = 'SAP meter version 0x78'
+ args = 'IzarWater3 izarv2 20481979 NOKEY'
+ telegram = 1944A511780779194820A1_21170013355F8EDB2D03C6912B1E37
+ json = '{"_":"telegram","media":"water","driver":"izarv2","name":"IzarWater3","id":"20481979","total_m3":4.366,"target_m3":0,"target_date":"2020-12-31","battery_y":11.5,"transmit_period_s":8,"status":"OK","timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'IzarWater3;20481979;OK;4.366;0;1111-11-11 11:11.11'
+ }
+ test {
+ comment = 'HYD meter with mfct specific tpl ci field a3'
+ args = 'IzarWater4 izarv2 2124589c NOKEY'
+ telegram = 1944304c9c5824210c04a363140013716577ec59e8663ab0d31c
+ json = '{"_":"telegram","media":"water","driver":"izarv2","name":"IzarWater4","id":"2124589c","prefix":"H19CA","serial_number":"159196","manufacture_y":"2019","total_m3":38.944,"target_m3":38.691,"target_date":"2021-02-01","battery_y":10,"transmit_period_s":32,"status":"OK","timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'IzarWater4;2124589c;OK;38.944;38.691;1111-11-11 11:11.11'
+ }
+ test {
+ comment = 'Ensure non-regression on manufacture year parsing'
+ args = 'IzarWater5 izarv2 20e4ffde NOKEY'
+ telegram = 1944304CDEFFE420CC01A2_63120013258F907B0AFF12529AC33B
+ json = '{"_":"telegram","media":"water","driver":"izarv2","name":"IzarWater5","id":"20e4ffde","prefix":"C15SA","serial_number":"007710","manufacture_y":"2015","total_m3":159.832,"target_m3":157.76,"target_date":"2021-02-01","battery_y":9,"transmit_period_s":32,"status":"OK","timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'IzarWater5;20e4ffde;OK;159.832;157.76;1111-11-11 11:11.11'
+ }
+ test {
+ comment = 'SAP meter with previous leakage alarm'
+ args = 'IzarWater6 izarv2 48500375 NOKEY'
+ telegram = 19442423860775035048A251520015BEB6B2E1ED623A18FC74A5
+ json = '{"_":"telegram","media":"water","driver":"izarv2","name":"IzarWater6","id":"48500375","total_m3":521.602,"target_m3":519.147,"target_date":"2021-11-15","battery_y":9,"transmit_period_s":8,"status":"PREV_LEAKAGE","timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'IzarWater6;48500375;PREV_LEAKAGE;521.602;519.147;1111-11-11 11:11.11'
+ }
+ }
+}
diff --git a/components/wmbus_common/drivers/src/joyh100.xmq b/components/wmbus_common/drivers/src/joyh100.xmq
new file mode 100644
index 00000000..b3506290
--- /dev/null
+++ b/components/wmbus_common/drivers/src/joyh100.xmq
@@ -0,0 +1,257 @@
+// Copyright (C) 2026 Hynek Moravec (gpl-3.0-or-later)
+driver {
+ name = joyh100
+ meter_type = HeatCostAllocationMeter
+ default_fields = name,id,status,consumption_hca,timestamp
+ manufacturer = 'Joy Technology'
+ detect {
+ mvt = JOY,0B,08
+ mvt = JOY,0C,08
+ }
+ // The TPL status byte's bits 0-1 are the standard OMS error code (handled
+ // automatically by INCLUDE_TPL_STATUS). Bit 2 is manufacturer-specific.
+ mfct_tpl_status_bits {
+ mask_bits = 0x04
+ default_message = OK
+ map {
+ name = RADIO_MODULE_BATTERY_LOW
+ value = 0x04
+ test = Set
+ }
+ }
+ library {
+ use = consumption_hca
+ use = target_hca
+ use = meter_datetime
+ use = target_date
+ use = flow_temperature_c
+ use = return_temperature_c
+ }
+ fields {
+ field {
+ name = status
+ quantity = Text
+ info = 'Status and error flags.'
+ attributes = STATUS,INCLUDE_TPL_STATUS
+ match {
+ measurement_type = Instantaneous
+ vif_range = ErrorFlags
+ storage_nr = 4
+ }
+ lookup {
+ name = ERROR_FLAGS
+ map_type = BitToString
+ mask_bits = 0xff
+ default_message = OK
+ map {
+ name = OPEN_COVER
+ value = 0x01
+ test = Set
+ }
+ map {
+ name = MEMORY_ERROR
+ value = 0x02
+ test = Set
+ }
+ map {
+ name = TEMP_SENSOR_ERROR
+ value = 0x04
+ test = Set
+ }
+ map {
+ name = BATTERY_LOW
+ value = 0x08
+ test = Set
+ }
+ }
+ }
+ field {
+ name = open_cover_alarm
+ quantity = PointInTime
+ display_unit = DateLT
+ info = 'Date the cover was last removed. Default (no alarm): current time.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = Date
+ storage_nr = 2
+ }
+ }
+ field {
+ name = temp_sensor_alarm
+ quantity = PointInTime
+ display_unit = DateLT
+ info = 'Date a temperature sensor alarm last triggered. Default (no alarm): current time.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = Date
+ storage_nr = 3
+ }
+ }
+ field {
+ name = historic_1
+ quantity = HCA
+ info = 'HCA units accumulated during the 1st previous month.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = HeatCostAllocation
+ storage_nr = 7
+ }
+ }
+ field {
+ name = historic_2
+ quantity = HCA
+ info = 'HCA units accumulated during the 2nd previous month.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = HeatCostAllocation
+ storage_nr = 8
+ }
+ }
+ field {
+ name = historic_3
+ quantity = HCA
+ info = 'HCA units accumulated during the 3rd previous month.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = HeatCostAllocation
+ storage_nr = 9
+ }
+ }
+ field {
+ name = historic_4
+ quantity = HCA
+ info = 'HCA units accumulated during the 4th previous month.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = HeatCostAllocation
+ storage_nr = 10
+ }
+ }
+ field {
+ name = historic_5
+ quantity = HCA
+ info = 'HCA units accumulated during the 5th previous month.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = HeatCostAllocation
+ storage_nr = 11
+ }
+ }
+ field {
+ name = historic_6
+ quantity = HCA
+ info = 'HCA units accumulated during the 6th previous month.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = HeatCostAllocation
+ storage_nr = 12
+ }
+ }
+ field {
+ name = historic_7
+ quantity = HCA
+ info = 'HCA units accumulated during the 7th previous month.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = HeatCostAllocation
+ storage_nr = 13
+ }
+ }
+ field {
+ name = historic_8
+ quantity = HCA
+ info = 'HCA units accumulated during the 8th previous month.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = HeatCostAllocation
+ storage_nr = 14
+ }
+ }
+ field {
+ name = historic_9
+ quantity = HCA
+ info = 'HCA units accumulated during the 9th previous month.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = HeatCostAllocation
+ storage_nr = 15
+ }
+ }
+ field {
+ name = historic_10
+ quantity = HCA
+ info = 'HCA units accumulated during the 10th previous month.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = HeatCostAllocation
+ storage_nr = 16
+ }
+ }
+ field {
+ name = historic_11
+ quantity = HCA
+ info = 'HCA units accumulated during the 11th previous month.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = HeatCostAllocation
+ storage_nr = 17
+ }
+ }
+ field {
+ name = historic_12
+ quantity = HCA
+ info = 'HCA units accumulated during the 12th previous month.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = HeatCostAllocation
+ storage_nr = 18
+ }
+ }
+ field {
+ name = historic_1_year
+ quantity = HCA
+ info = 'HCA units accumulated during the 1st previous billing year.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = HeatCostAllocation
+ storage_nr = 19
+ }
+ }
+ field {
+ name = historic_2_year
+ quantity = HCA
+ info = 'HCA units accumulated during the 2nd previous billing year.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = HeatCostAllocation
+ storage_nr = 20
+ }
+ }
+ field {
+ name = historic_3_year
+ quantity = HCA
+ info = 'HCA units accumulated during the 3rd previous billing year.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = HeatCostAllocation
+ storage_nr = 21
+ }
+ }
+ }
+ tests {
+ test {
+ args = 'MyHCA joyh100 44332211 00112233445566778899AABBCCDDEEFF'
+ comment = 'JOYH100 long frame with full monthly/yearly history'
+ telegram = 8e44f9a9112233440b087a020080252b2e855912aefbbd4ad91b8fc0fea6c2e4d28ae2c87523d2aefb7f67f953e419c9696c1b91be82607c289d8952b2377ec158bf46ae9df4a9fd0d593ee8fd0ddcc8c58f5efc4f88ab70d5fd65dcb1375e041748595ab514dd4710d26ec4e71f9669af1a9f481279257b85a70659ee0adee900910622bbff4dbf2b4b652bef8db3
+ json = '{"_": "telegram","consumption_hca": 0,"flow_temperature_c": 20.37,"historic_10_hca": 0,"historic_11_hca": 0,"historic_12_hca": 0,"historic_1_hca": 0,"historic_1_year_hca": 0,"historic_2_hca": 0,"historic_2_year_hca": 0,"historic_3_hca": 0,"historic_3_year_hca": 0,"historic_4_hca": 0,"historic_5_hca": 0,"historic_6_hca": 0,"historic_7_hca": 0,"historic_8_hca": 0,"historic_9_hca": 0,"id": "44332211","media": "heat cost allocation","driver": "joyh100","meter_datetime": "2024-01-01 08:06:16","name": "MyHCA","open_cover_alarm_date": "1999-11-30","return_temperature_c": 21.55,"status": "MEMORY_ERROR","target_date": "2023-01-01","target_hca": 0,"temp_sensor_alarm_date": "1999-11-30","timestamp": "1111-11-11T11:11:11Z"}'
+ fields = 'MyHCA;44332211;MEMORY_ERROR;0;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'MyHCA joyh100 91796365 00112233445566778899AABBCCDDEEFF'
+ comment = 'JOYH100 short frame (no history section), device firmware version 0C instead of 0B'
+ telegram = 3e44f9a9656379910c087a71003025e52e9375d18368e6f3fa74b2b5f3ecfe9f7994ea68ed09abfdc77e99ca82ceee7d8a7a69db3a26773ddf6c1196f3f743
+ json = '{"_": "telegram","consumption_hca": 0,"flow_temperature_c": 28.86,"id": "91796365","media": "heat cost allocation","driver": "joyh100","meter_datetime": "2026-08-10 12:06:42","name": "MyHCA","open_cover_alarm_date": "1999-11-30","return_temperature_c": 28.99,"status": "OK","target_date": "2025-01-01","target_hca": 0,"temp_sensor_alarm_date": "1999-11-30","timestamp": "1111-11-11T11:11:11Z"}'
+ fields = 'MyHCA;91796365;OK;0;1111-11-11 11:11.11'
+ }
+ }
+}
diff --git a/components/wmbus_common/drivers/src/kaden.xmq b/components/wmbus_common/drivers/src/kaden.xmq
new file mode 100644
index 00000000..e9944517
--- /dev/null
+++ b/components/wmbus_common/drivers/src/kaden.xmq
@@ -0,0 +1,97 @@
+// Copyright (C) 2026 Fredrik Öhrström (gpl-3.0-or-later)
+driver {
+ name = kaden
+ meter_type = HeatCostAllocationMeter
+ default_fields = name,id,status,consumption_hca,timestamp
+ detect {
+ mvt = VIP,1E,08
+ }
+ library {
+ use = consumption_hca
+ use = target_hca
+ use = meter_datetime
+ use = target_date
+ use = fabrication_no
+ use = 'external_temperature_c|Corrected room temperature.'
+ use = 'flow_temperature_c|Radiator surface temperature.'
+ }
+ fields {
+ field {
+ name = status
+ quantity = Text
+ info = 'Status and error flags'
+ attributes = STATUS,INCLUDE_TPL_STATUS
+ match {
+ measurement_type = Instantaneous
+ vif_range = ErrorFlags
+ }
+ lookup {
+ name = ERROR_FLAGS
+ map_type = BitToString
+ mask_bits = 0xffff
+ default_message = OK
+ map {
+ name = VOLTAGE_INTERRUPTED
+ value = 0x0001
+ test = Set
+ }
+ map {
+ name = SENSOR_T2_OUTSIDE_MEASURING_RANGE
+ value = 0x0004
+ test = Set
+ }
+ map {
+ name = TAMPERING
+ value = 0x0008
+ test = Set
+ }
+ map {
+ name = SENSOR_T3_OUTSIDE_MEASURING_RANGE
+ value = 0x0020
+ test = Set
+ }
+ }
+ }
+ field {
+ name = no_measurement_duration
+ quantity = Time
+ info = 'How many hours the meter has been removed from the element during winter.'
+ display_unit = h
+ match {
+ difvifkey = 0475
+ }
+ }
+ field {
+ name = no_measurement_duration_last_year
+ quantity = Time
+ info = 'How many hours the meter has been removed from the element previous winter.'
+ display_unit = h
+ match {
+ difvifkey = 4475
+ }
+ }
+ field {
+ name = tampering_duration
+ quantity = Time
+ info = 'Total time the device has been removed from the radiator.'
+ display_unit = h
+ match {
+ difvifkey = 0474
+ }
+ }
+ }
+ tests {
+ test {
+ args = 'KadenD10 kaden 23800604 82B0551191F51D66EFCDAB8967452301'
+ telegram = 4e443059040680231e087ac40040050e6aa476257c0c3adae8277edc999b39b38222fcb387a91e94cb6ed47ceec6470f5f686f89a8574415fa262bd43c88f7f153ce3c66e8e44da338a06c62ab21b1
+ json = '{"_": "telegram","consumption_hca": 0,"external_temperature_c": 19.6,"fabrication_no": "23800604","flow_temperature_c": 19.7,"id": "23800604","media": "heat cost allocation","driver": "kaden","meter_datetime": "2026-02-09 04:01","name": "KadenD10","no_measurement_duration_h": 940.016667,"no_measurement_duration_last_year_h": 5088,"status": "TAMPERING","tampering_duration_h": 0.003611,"target_date": "2127-01-01","target_hca": 3,"timestamp": "1111-11-11T11:11:11Z"}'
+ fields = 'KadenD10;23800604;TAMPERING;0;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'KadenC10 kaden 23701267 82B0551191F51D66EFCDAB8967452301'
+ telegram = 4e443059671270231e087a6f0040051183e178afb952391c01f78104bc3fd33a5232ba7e70f514a062dac99059a7c74a55227dfae9d9590145f685f4ae6a62288bbba6eaf92d86797254644a2cdf46
+ json = '{"_": "telegram","consumption_hca": 859,"external_temperature_c": 32,"fabrication_no": "23701267","flow_temperature_c": 39.3,"id": "23701267","media": "heat cost allocation","driver": "kaden","meter_datetime": "2026-02-09 09:41","name": "KadenC10","no_measurement_duration_h": 2.266667,"no_measurement_duration_last_year_h": 1234.483333,"status": "OK","tampering_duration_h": 0,"target_date": "2127-01-01","target_hca": 2264,"timestamp": "1111-11-11T11:11:11Z"}'
+ fields = 'KadenC10;23701267;OK;859;1111-11-11 11:11.11'
+ }
+ }
+}
diff --git a/components/wmbus_common/drivers/src/kadenwater.xmq b/components/wmbus_common/drivers/src/kadenwater.xmq
new file mode 100644
index 00000000..a0726931
--- /dev/null
+++ b/components/wmbus_common/drivers/src/kadenwater.xmq
@@ -0,0 +1,127 @@
+// Copyright (C) 2026 Fredrik Öhrström (gpl-3.0-or-later)
+driver {
+ name = kadenwater
+ meter_type = WaterMeter
+ default_fields = name,id,status,total_m3,timestamp
+ manufacturer = Ecomess
+ detect {
+ mvt = KDN,01,07
+ mvt = VIP,01,07
+ mvt = ECM,01,07
+ }
+ // Water-leak alarm is carried in the unencrypted TPL status byte
+ // (per the Kaden/Ecomess wM-Bus OMS spec, shared with the Picoflux AiR):
+ // bits 0+1 (value 3) -> permanent water-leak alarm (decoded as ALARM by the
+ // standard OMS bits)
+ // bit 5 (0x20) -> during the leak the flow stayed below 100 l/h
+ // bit 6 (0x40) -> during the leak the flow exceeded 100 l/h
+ // (bits 5 and 6 may both be set if the flow varied across the limit)
+ mfct_tpl_status_bits {
+ mask_bits = 0xe0
+ default_message = OK
+ map {
+ name = LEAK_FLOW_LOW
+ value = 0x20
+ test = set
+ }
+ map {
+ name = LEAK_FLOW_HIGH
+ value = 0x40
+ test = set
+ }
+ }
+ library {
+ use = total_m3
+ use = volume_flow_m3h
+ use = actuality_duration_s
+ use = meter_datetime
+ use = fabrication_no
+ }
+ fields {
+ field {
+ name = status
+ quantity = Text
+ info = 'Status and error flags.'
+ attributes = STATUS,INCLUDE_TPL_STATUS
+ match {
+ measurement_type = Instantaneous
+ vif_range = ErrorFlags
+ }
+ lookup {
+ name = ERROR_FLAGS
+ map_type = BitToString
+ mask_bits = 0xffff
+ default_message = OK
+ // Help needed. The meaning of the individual error flag bits
+ // is not documented for the Kaden type 07 water meter.
+ }
+ }
+ field {
+ name = max_flow
+ quantity = Flow
+ info = 'The maximum media volume flow recorded during the measuring period.'
+ match {
+ measurement_type = Maximum
+ vif_range = VolumeFlow
+ }
+ }
+ field {
+ name = volume_b
+ quantity = Volume
+ info = 'Second volume register reported after the total. Purpose is not documented for the Kaden type 07 water meter. Help needed.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = Volume
+ index_nr = 2
+ }
+ }
+ field {
+ name = duration_b
+ quantity = Time
+ display_unit = s
+ info = 'Second actuality duration register (04 74 #2). Purpose is not documented for the Kaden type 07 water meter. Help needed.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = ActualityDuration
+ index_nr = 2
+ }
+ }
+ }
+ tests {
+ test {
+ args = 'MyWater kadenwater 19228217 82B0551191F51D66EFCDAB8967452301'
+ comment = 'Ecomess/Kaden type 07 water meter (81.0976 m3)'
+ telegram = 4e448e2c1782221901077ab5004005339341ab7927f9337cd2175b2f0d121d9a56cbf3fcfcbf0d8ca57d0c6741a03a666d99467924a6dbf4de305960917afde729ac19d62a789dce1c66a323cc29cd
+ json = '{"_":"telegram","media":"water","driver":"kadenwater","name":"MyWater","id":"19228217","actuality_duration_s":0,"duration_b_s":0,"max_flow_m3h":1.715,"total_m3":81.0976,"volume_b_m3":0.0096,"volume_flow_m3h":0,"fabrication_no":"19228217","meter_datetime":"2026-06-13 19:36","status":"OK","timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'MyWater;19228217;OK;81.0976;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'MyWater kadenwater 19227961 82B0551191F51D66EFCDAB8967452301'
+ comment = 'Ecomess/Kaden type 07 water meter (22.761 m3)'
+ telegram = 4e448e2c6179221901077ab50040054d008a1ad5270d9cf92952500bd3938ea37fcac71a45d4349fb13c89cfa4aaebd6ecf9caaf1c0710c1d6b4d98f23109cef5fa6fec93fd6a148730ea51102dbcc
+ json = '{"_":"telegram","media":"water","driver":"kadenwater","name":"MyWater","id":"19227961","actuality_duration_s":0,"duration_b_s":0,"max_flow_m3h":1.496,"total_m3":22.761,"volume_b_m3":0.0013,"volume_flow_m3h":0,"fabrication_no":"19227961","meter_datetime":"2026-06-13 19:36","status":"OK","timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'MyWater;19227961;OK;22.761;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'MyWater kadenwater 19221000 82B0551191F51D66EFCDAB8967452301'
+ comment = 'Same water meter under the Kaden VIP manufacturer code (VIP,01,07) instead of KDN,01,07'
+ telegram = 4e4430590010221901077a71004005f8c0d59e1536f306ffcd58075fda84b6f21fc0f33df92c8cf5fcab599bd1fe73b0db2e5d9e6d8a2e3363d67db2cdaf16c26d57886b70ee0f77fc116753809bb5
+ json = '{"_":"telegram","media":"water","driver":"kadenwater","name":"MyWater","id":"19221000","actuality_duration_s":0,"duration_b_s":0,"max_flow_m3h":0.859,"total_m3":94.6123,"volume_b_m3":0.0088,"volume_flow_m3h":0,"fabrication_no":"19221000","meter_datetime":"2026-06-30 12:39","status":"OK","timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'MyWater;19221000;OK;94.6123;1111-11-11 11:11.11'
+ }
+ test {
+ comment = 'Water-leak alarm: TPL status byte set to 0x63 (synthetic, from the VIP 94.6123 m3 telegram) = leak with flow both below and above the 100 l/h limit'
+ args = 'MyWater kadenwater 19221000 82B0551191F51D66EFCDAB8967452301'
+ telegram = 4e4430590010221901077a71634005f8c0d59e1536f306ffcd58075fda84b6f21fc0f33df92c8cf5fcab599bd1fe73b0db2e5d9e6d8a2e3363d67db2cdaf16c26d57886b70ee0f77fc116753809bb5
+ json = '{"_":"telegram","media":"water","driver":"kadenwater","name":"MyWater","id":"19221000","actuality_duration_s":0,"duration_b_s":0,"max_flow_m3h":0.859,"total_m3":94.6123,"volume_b_m3":0.0088,"volume_flow_m3h":0,"fabrication_no":"19221000","meter_datetime":"2026-06-30 12:39","status":"ALARM LEAK_FLOW_HIGH LEAK_FLOW_LOW","timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'MyWater;19221000;ALARM LEAK_FLOW_HIGH LEAK_FLOW_LOW;94.6123;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'MyWater kadenwater 12345678 9F5213BC13841410BB1410141515E4D5'
+ comment = 'Same water meter under the ECM manufacturer code (ECM,01,07) instead of KDN,01,07 or VIP,01,07'
+ telegram = 7e446d147856341201077a0d007005c927ee43f1102ba4944b43501e2368a0f18a01c1b0d0ef02e69b4451a208ee5be8c419577228b50d39ef0ac642349b2e91b01ec233116fb7fb643e83d78dc039296bf261f23600d9a68ada548aac1945ac8f9f7fbe3b7db1c37d3c35dc34bc97bcb9586d5f292c8d53e20c0d2ccf2dde
+ json = '{"_":"telegram","media":"water","driver":"kadenwater","name":"MyWater","id":"12345678","actuality_duration_s":0,"duration_b_s":0,"max_flow_m3h":2.594,"total_m3":727.7096,"volume_b_m3":0.0047,"volume_flow_m3h":0,"fabrication_no":"0000000000000","meter_datetime":"2026-08-05 14:12","status":"OK","timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'MyWater;12345678;OK;727.7096;1111-11-11 11:11.11'
+ }
+ }
+}
diff --git a/components/wmbus_common/drivers/src/kamheat.xmq b/components/wmbus_common/drivers/src/kamheat.xmq
index 7236b031..d21e5cfc 100644
--- a/components/wmbus_common/drivers/src/kamheat.xmq
+++ b/components/wmbus_common/drivers/src/kamheat.xmq
@@ -1,42 +1,60 @@
-/*
- Copyright (C) 2018-2025 Fredrik Öhrström (gpl-3.0-or-later)
- Copyright (C) 2020 Eric Bus (gpl-3.0-or-later)
- Copyright (C) 2022 thecem (gpl-3.0-or-later)
-
- 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 .
-*/
+/* Copyright (C) 2018-2025 Fredrik Öhrström (gpl-3.0-or-later)
+ Copyright (C) 2020 Eric Bus (gpl-3.0-or-later)
+ Copyright (C) 2022 thecem (gpl-3.0-or-later) */
driver {
name = kamheat
aliases = multical302,multical303,multical403,multical602,multical603,multical803
default_fields = name,id,total_energy_consumption_kwh,total_volume_m3,status,timestamp
meter_type = HeatMeter
detect {
- mvt = KAM,30,04 // 302
- mvt = KAM,30,0d // 302
- mvt = KAM,30,0c // 302
- mvt = KAM,40,04 // 303
- mvt = KAM,40,0c // 303
- mvt = KAM,19,04 // 402
- mvt = KAM,34,04 // 403
- mvt = KAM,34,0a // 403
- mvt = KAM,34,0b // 403
- mvt = KAM,34,0c // 403
- mvt = KAM,34,0d // 403
- mvt = KAM,1c,04 // 602
- mvt = KAM,35,04 // 603
- mvt = KAM,35,0c // 603
- mvt = KAM,39,04 // 803
+ mvt = KAM,30,04
+ // 302
+ mvt = KAM,30,0d
+ // 302
+ mvt = KAM,30,0c
+ // 302
+ mvt = KAM,40,04
+ // 303
+ mvt = KAM,40,0c
+ // 303
+ mvt = KAM,40,0d
+ // 303 heat&cool
+ mvt = KAM,19,04
+ // 402
+ mvt = KAM,34,04
+ // 403
+ mvt = KAM,34,0a
+ // 403
+ mvt = KAM,34,0b
+ // 403
+ mvt = KAM,34,0c
+ // 403
+ mvt = KAM,34,0d
+ // 403
+ mvt = KAM,1c,04
+ // 602
+ mvt = KAM,35,04
+ // 603
+ mvt = KAM,35,0c
+ // 603
+ mvt = KAM,35,0d
+ // 603 heat&cool
+ mvt = KAM,39,04
+ // 803
+ }
+ compact_frame_formats {
+ // KAM,1c,04
+ difvif = 02F9FF15040604EEFF0704EEFF0804148440148480401402FD17026C426C44064414C44014C4804014043B0259025D
+ // KAM,30,04
+ difvif = 030643060314426C022D01FF21
+ // KAM,30,0c
+ difvif = 030E430E0313426C022D01FF21
+ // KAM,34,0c
+ difvif = 040604FF0704FF080414043B0259025D04FF22026C44064414426C
+ // KAM,39,04
+ difvif = 040F04FF0704FF08041484401484804014043B0259025D142D84100F84200F04FF22026C440F4414C44014C4804014426C
+ // MC 303 heat
+ difvif = 0406041404FF0704FF080259025D023B02FF22026C44064414426C
}
library {
use = fabrication_no
@@ -49,369 +67,338 @@ driver {
}
fields {
field {
- name = status
- quantity = Text
- info = 'Status and error flags'
+ name = status
+ quantity = Text
+ info = 'Status and error flags'
attributes = STATUS,INCLUDE_TPL_STATUS
match {
difvifkey = 04FF22
}
lookup {
- name = ERROR_FLAGS
- map_type = BitToString
- mask_bits = 0xffffffff
- default_message = OK
- map {
- name = VOLTAGE_INTERRUPTED
- value = 0x00000001
- test = Set
- }
- map {
- name = LOW_BATTERY_LEVEL
- value = 0x00000002
- test = Set
- }
+ name = ERROR_FLAGS
+ map_type = BitToString
+ mask_bits = 0xffffffff
+ default_message = OK
+ mark_reserved_bits = true
map {
- name = SENSOR_ERROR
- value = 0x00000004
- test = Set
+ name = VOLTAGE_INTERRUPTED
+ bit = 0
+ test = Set
}
map {
- name = SENSOR_T1_ABOVE_MEASURING_RANGE
- value = 0x00000008
- test = Set
+ name = LOW_BATTERY_LEVEL
+ bit = 1
+ test = Set
}
map {
- name = SENSOR_T2_ABOVE_MEASURING_RANGE
- value = 0x00000010
- test = Set
+ name = SENSOR_ERROR
+ bit = 2
+ test = Set
}
map {
- name = SENSOR_T1_BELOW_MEASURING_RANGE
- value = 0x00000020
- test = Set
+ name = SENSOR_T1_ABOVE_MEASURING_RANGE
+ bit = 3
+ test = Set
}
map {
- name = SENSOR_T2_BELOW_MEASURING_RANGE
- value = 0x00000040
- test = Set
+ name = SENSOR_T2_ABOVE_MEASURING_RANGE
+ bit = 4
+ test = Set
}
map {
- name = TEMP_DIFF_WRONG_POLARITY
- value = 0x00000080
- test = Set
+ name = SENSOR_T1_BELOW_MEASURING_RANGE
+ bit = 5
+ test = Set
}
map {
- name = FLOW_SENSOR_WEAK_OR_AIR
- value = 0x00000100
- test = Set
+ name = SENSOR_T2_BELOW_MEASURING_RANGE
+ bit = 6
+ test = Set
}
map {
- name = WRONG_FLOW_DIRECTION
- value = 0x00000200
- test = Set
+ name = TEMP_DIFF_WRONG_POLARITY
+ bit = 7
+ test = Set
}
map {
- name = RESERVED_BIT_10
- value = 0x00000400
- test = Set
+ name = FLOW_SENSOR_WEAK_OR_AIR
+ bit = 8
+ test = Set
}
map {
- name = FLOW_INCREASED
- value = 0x00000800
- test = Set
+ name = WRONG_FLOW_DIRECTION
+ bit = 9
+ test = Set
}
+ // bit 10 is reserved
map {
- name = IN_A1_LEAKAGE_IN_THE_SYSTEM
- value = 0x00001000
- test = Set
+ name = FLOW_INCREASED
+ bit = 11
+ test = Set
}
map {
- name = IN_B1_LEAKAGE_IN_THE_SYSTEM
- value = 0x00002000
- test = Set
+ name = IN_A1_LEAKAGE_IN_THE_SYSTEM
+ bit = 12
+ test = Set
}
map {
- name = IN_A1_A2_EXTERNAL_ALARM
- value = 0x00004000
- test = Set
+ name = IN_B1_LEAKAGE_IN_THE_SYSTEM
+ bit = 13
+ test = Set
}
map {
- name = IN_B1_B2_EXTERNAL_ALARM
- value = 0x00008000
- test = Set
+ name = IN_A1_A2_EXTERNAL_ALARM
+ bit = 14
+ test = Set
}
map {
- name = V1_COMMUNICATION_ERROR
- value = 0x00010000
- test = Set
+ name = IN_B1_B2_EXTERNAL_ALARM
+ bit = 15
+ test = Set
}
map {
- name = V1_WRONG_PULSE_FIGURE
- value = 0x00020000
- test = Set
+ name = V1_COMMUNICATION_ERROR
+ bit = 16
+ test = Set
}
map {
- name = IN_A2_LEAKAGE_IN_THE_SYSTEM
- value = 0x00040000
- test = Set
+ name = V1_WRONG_PULSE_FIGURE
+ bit = 17
+ test = Set
}
map {
- name = IN_B2_LEAKAGE_IN_THE_SYSTEM
- value = 0x00080000
- test = Set
+ name = IN_A2_LEAKAGE_IN_THE_SYSTEM
+ bit = 18
+ test = Set
}
map {
- name = T3_ABOVE_MEASURING_RANGE_OR_SWITCHED_OFF
- value = 0x00100000
- test = Set
+ name = IN_B2_LEAKAGE_IN_THE_SYSTEM
+ bit = 19
+ test = Set
}
map {
- name = T3_BELOW_MEASURING_RANGE_OR_SWITCHED_OFF
- value = 0x00200000
- test = Set
+ name = T3_ABOVE_MEASURING_RANGE_OR_SWITCHED_OFF
+ bit = 20
+ test = Set
}
map {
- name = V2_COMMUNICATION_ERROR
- value = 0x00400000
- test = Set
+ name = T3_BELOW_MEASURING_RANGE_OR_SWITCHED_OFF
+ bit = 21
+ test = Set
}
map {
- name = V2_WRONG_PULSE_FIGURE
- value = 0x00800000
- test = Set
+ name = V2_COMMUNICATION_ERROR
+ bit = 22
+ test = Set
}
map {
- name = V2_AIR
- value = 0x01000000
- test = Set
+ name = V2_WRONG_PULSE_FIGURE
+ bit = 23
+ test = Set
}
map {
- name = V2_WRONG_FLOW_DIRECTION
- value = 0x02000000
- test = Set
+ name = V2_AIR
+ bit = 24
+ test = Set
}
map {
- name = RESERVED_BIT_26
- value = 0x04000000
- test = Set
+ name = V2_WRONG_FLOW_DIRECTION
+ bit = 25
+ test = Set
}
+ // bit 26 is reserved
map {
- name = V2_INCREASED_FLOW
- value = 0x08000000
- test = Set
+ name = V2_INCREASED_FLOW
+ bit = 27
+ test = Set
}
map {
- name = V1_V2_BURST_WATER_LOSS
- value = 0x10000000
- test = Set
+ name = V1_V2_BURST_WATER_LOSS
+ bit = 28
+ test = Set
}
map {
- name = V1_V2_BURST_WATER_PENETRATION
- value = 0x20000000
- test = Set
+ name = V1_V2_BURST_WATER_PENETRATION
+ bit = 29
+ test = Set
}
map {
- name = V1_V2_LEAKAGE_WATER_LOSS
- value = 0x40000000
- test = Set
+ name = V1_V2_LEAKAGE_WATER_LOSS
+ bit = 30
+ test = Set
}
map {
- name = V1_V2_LEAKAGE_WATER_PENETRATION
- value = 0x80000000
- test = Set
+ name = V1_V2_LEAKAGE_WATER_PENETRATION
+ bit = 31
+ test = Set
}
}
}
field {
- name = status16
- quantity = Text
- info = 'Status and error flags'
+ name = status16
+ quantity = Text
+ info = 'Status and error flags'
attributes = INJECT_INTO_STATUS,HIDE
match {
difvifkey = 02FF22
}
lookup {
- name = ERROR_FLAGS
- map_type = BitToString
- mask_bits = 0xffff
- default_message = OK
- map {
- name = VOLTAGE_INTERRUPTED
- value = 0x0001
- test = Set
- }
- map {
- name = LOW_BATTERY_LEVEL
- value = 0x0002
- test = Set
- }
- map {
- name = RESERVED_BIT_2
- value = 0x0004
- test = Set
- }
- map {
- name = SENSOR_T1_ABOVE_MEASURING_RANGE
- value = 0x0008
- test = Set
- }
- map {
- name = SENSOR_T2_ABOVE_MEASURING_RANGE
- value = 0x0010
- test = Set
- }
+ name = ERROR_FLAGS
+ map_type = BitToString
+ mask_bits = 0xffff
+ default_message = OK
+ mark_reserved_bits = true
map {
- name = SENSOR_T1_BELOW_MEASURING_RANGE
- value = 0x0020
- test = Set
+ name = VOLTAGE_INTERRUPTED
+ bit = 0
+ test = Set
}
map {
- name = SENSOR_T2_BELOW_MEASURING_RANGE
- value = 0x0040
- test = Set
+ name = LOW_BATTERY_LEVEL
+ bit = 1
+ test = Set
}
+ // bit 2 is reserved
map {
- name = TEMP_DIFF_WRONG_POLARITY
- value = 0x0080
- test = Set
+ name = SENSOR_T1_ABOVE_MEASURING_RANGE
+ bit = 3
+ test = Set
}
map {
- name = FLOW_SENSOR_WEAK_OR_AIR
- value = 0x0100
- test = Set
+ name = SENSOR_T2_ABOVE_MEASURING_RANGE
+ bit = 4
+ test = Set
}
map {
- name = WRONG_FLOW_DIRECTION
- value = 0x0200
- test = Set
+ name = SENSOR_T1_BELOW_MEASURING_RANGE
+ bit = 5
+ test = Set
}
map {
- name = RESERVED_BIT_10
- value = 0x0400
- test = Set
+ name = SENSOR_T2_BELOW_MEASURING_RANGE
+ bit = 6
+ test = Set
}
map {
- name = FLOW_INCREASED
- value = 0x0800
- test = Set
+ name = TEMP_DIFF_WRONG_POLARITY
+ bit = 7
+ test = Set
}
map {
- name = RESERVED_BIT_12
- value = 0x1000
- test = Set
+ name = FLOW_SENSOR_WEAK_OR_AIR
+ bit = 8
+ test = Set
}
map {
- name = RESERVED_BIT_13
- value = 0x2000
- test = Set
+ name = WRONG_FLOW_DIRECTION
+ bit = 9
+ test = Set
}
+ // bit 10 is reserved
map {
- name = RESERVED_BIT_14
- value = 0x4000
- test = Set
- }
- map {
- name = RESERVED_BIT_15
- value = 0x8000
- test = Set
+ name = FLOW_INCREASED
+ bit = 11
+ test = Set
}
+ // bits 12-15 are reserved
}
}
field {
- name = status_narrow
- quantity = Text
- info = 'Status and error flags'
+ name = status_narrow
+ quantity = Text
+ info = 'Status and error flags'
attributes = INJECT_INTO_STATUS,HIDE
match {
measurement_type = Instantaneous
- vif_range = ErrorFlags
+ vif_range = ErrorFlags
}
lookup {
- name = NARROW_ERROR_FLAGS
- map_type = BitToString
- mask_bits = 0xffff
- default_message = OK
+ name = NARROW_ERROR_FLAGS
+ map_type = BitToString
+ mask_bits = 0xffff
+ default_message = OK
+ mark_reserved_bits = true
map {
- name = VOLTAGE_INTERRUPTED
- value = 0x0001
- test = Set
+ name = VOLTAGE_INTERRUPTED
+ bit = 0
+ test = Set
}
map {
- name = SENSOR_T1_OUTSIDE_MEASURING_RANGE
- value = 0x0008
- test = Set
+ name = SENSOR_T1_OUTSIDE_MEASURING_RANGE
+ bit = 3
+ test = Set
}
map {
- name = SENSOR_T2_OUTSIDE_MEASURING_RANGE
- value = 0x0004
- test = Set
+ name = SENSOR_T2_OUTSIDE_MEASURING_RANGE
+ bit = 2
+ test = Set
}
map {
- name = SENSOR_T3_OUTSIDE_MEASURING_RANGE
- value = 0x0020
- test = Set
+ name = SENSOR_T3_OUTSIDE_MEASURING_RANGE
+ bit = 5
+ test = Set
}
map {
- name = LEAKAGE_COLD_WATER
- value = 0x0040
- test = Set
+ name = LEAKAGE_COLD_WATER
+ bit = 6
+ test = Set
}
map {
- name = LEAKAGE_HOT_WATER
- value = 0x0100
- test = Set
+ name = LEAKAGE_HOT_WATER
+ bit = 8
+ test = Set
}
map {
- name = BURST_WATER_LOSS
- value = 0x0200
- test = Set
+ name = BURST_WATER_LOSS
+ bit = 9
+ test = Set
}
map {
- name = V1_COMMUNICATION_ERROR
- value = 0x0010
- test = Set
+ name = V1_COMMUNICATION_ERROR
+ bit = 4
+ test = Set
}
map {
- name = V2_COMMUNICATION_ERROR
- value = 0x0400
- test = Set
+ name = V2_COMMUNICATION_ERROR
+ bit = 10
+ test = Set
}
map {
- name = V1_WRONG_PULSE_FIGURE
- value = 0x0800
- test = Set
+ name = V1_WRONG_PULSE_FIGURE
+ bit = 11
+ test = Set
}
map {
- name = V2_WRONG_PULSE_FIGURE
- value = 0x0080
- test = Set
+ name = V2_WRONG_PULSE_FIGURE
+ bit = 7
+ test = Set
}
map {
- name = V1_AIR
- value = 0x1000
- test = Set
+ name = V1_AIR
+ bit = 12
+ test = Set
}
map {
- name = V2_AIR
- value = 0x2000
- test = Set
+ name = V2_AIR
+ bit = 13
+ test = Set
}
map {
- name = V1_WRONG_FLOW_DIRECTION
- value = 0x4000
- test = Set
+ name = V1_WRONG_FLOW_DIRECTION
+ bit = 14
+ test = Set
}
map {
- name = V2_WRONG_FLOW_DIRECTION
- value = 0x8000
- test = Set
+ name = V2_WRONG_FLOW_DIRECTION
+ bit = 15
+ test = Set
}
}
}
field {
name = total_energy_consumption
quantity = Energy
+ change = Increasing
info = 'The total energy consumption recorded by this meter.'
match {
measurement_type = Instantaneous
@@ -421,6 +408,7 @@ driver {
field {
name = total_energy_backward
quantity = Energy
+ change = Increasing
info = 'The total backwards energy consumption recorded by this meter.'
match {
measurement_type = Instantaneous
@@ -431,6 +419,7 @@ driver {
field {
name = total_volume
quantity = Volume
+ change = Increasing
info = 'The volume of water (3/68/Volume V1).'
match {
measurement_type = Instantaneous
@@ -440,6 +429,7 @@ driver {
field {
name = 'total_volume_subunit{subunit_counter}'
quantity = Volume
+ change = Increasing
info = 'The volume of water for a subunit.'
match {
measurement_type = Instantaneous
@@ -502,25 +492,116 @@ driver {
}
}
field {
- name = forward_energy
- quantity = Energy
- info = 'The forward energy of the water (4/97/Energy E8).'
- vif_scaling = None
+ name = forward_energy
+ quantity = Energy
+ info = 'The forward energy of the water (4/97/Energy E8).'
+ vif_scaling = None
display_unit = m3c
match {
difvifkey = 04FF07
}
}
field {
- name = return_energy
- quantity = Energy
- info = 'The return energy of the water (5/110/Energy E9).'
- vif_scaling = None
+ name = return_energy
+ quantity = Energy
+ info = 'The return energy of the water (5/110/Energy E9).'
+ vif_scaling = None
display_unit = m3c
match {
difvifkey = 04FF08
}
}
+ field {
+ name = total_energy_cooling
+ quantity = Energy
+ change = Increasing
+ info = 'The cooling energy E3 of a combined heat and cooling meter (63/E3 HC).'
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyEnergyVIF
+ add_combinable = Mfct02
+ }
+ }
+ field {
+ name = target_energy_cooling
+ quantity = Energy
+ info = 'The cooling energy E3 of a combined heat and cooling meter at the set date.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyEnergyVIF
+ add_combinable = Mfct02
+ storage_nr = 1
+ }
+ }
+ field {
+ name = target_forward_energy
+ quantity = Energy
+ info = 'The forward energy E8 at the set date.'
+ vif_scaling = None
+ display_unit = m3c
+ match {
+ difvifkey = 44FF07
+ }
+ }
+ field {
+ name = target_return_energy
+ quantity = Energy
+ info = 'The return energy E9 at the set date.'
+ vif_scaling = None
+ display_unit = m3c
+ match {
+ difvifkey = 44FF08
+ }
+ }
+ field {
+ name = target_max_power
+ quantity = Power
+ info = 'The maximum power at the set date.'
+ match {
+ measurement_type = Maximum
+ vif_range = AnyPowerVIF
+ storage_nr = 1
+ }
+ }
+ field {
+ name = target_max_flow
+ quantity = Flow
+ info = 'The maximum flow at the set date.'
+ match {
+ measurement_type = Maximum
+ vif_range = VolumeFlow
+ storage_nr = 1
+ }
+ }
+ field {
+ name = meter_type
+ quantity = Text
+ info = 'Kamstrup meter type and variant, e.g. 1B02 = MULTICAL 603 heat/cooling.'
+ readable_string = Normal
+ match {
+ difvifkey = 02FF1A
+ }
+ }
+ field {
+ name = module_type_config
+ quantity = Dimensionless
+ info = 'M-Bus module type, configuration and datagram profile, e.g. 2000101.'
+ vif_scaling = None
+ display_unit = nr
+ match {
+ difvifkey = 04FF16
+ }
+ }
+ field {
+ name = module_software
+ quantity = Dimensionless
+ info = 'M-Bus module software number and revision.'
+ vif_scaling = None
+ display_unit = nr
+ match {
+ difvifkey = 04FF17
+ }
+ }
field {
name = target_energy
quantity = Energy
@@ -531,6 +612,17 @@ driver {
storage_nr = 1
}
}
+ field {
+ name = target_energy_backward
+ quantity = Energy
+ info = 'The cooling energy consumption recorded by this meter at the set date (63 Cooling energy E3).'
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyEnergyVIF
+ add_combinable = BackwardFlow
+ storage_nr = 1
+ }
+ }
field {
name = target_volume
quantity = Volume
@@ -552,7 +644,6 @@ driver {
subunit_nr = 1,4
}
}
-
field {
name = operating_time
quantity = Time
@@ -564,148 +655,239 @@ driver {
}
}
field {
- name = enhanced_id
- quantity = Text
+ name = enhanced_id
+ quantity = Text
+ readable_string = Normal
match {
difvifkey = 02F9FF15
}
- }
- field {
- name = va
- quantity = Dimensionless
- vif_scaling = None
+ }
+ field {
+ name = va
+ quantity = Dimensionless
+ vif_scaling = None
display_unit = counter
match {
difvifkey = 04EEFF07
}
- }
- field {
- name = vb
- quantity = Dimensionless
- vif_scaling = None
+ }
+ field {
+ name = vb
+ quantity = Dimensionless
+ vif_scaling = None
display_unit = counter
match {
difvifkey = 04EEFF08
}
- }
+ }
}
tests {
test {
- args = 'MyHeater multical302 67676767 NOKEY'
+ args = 'MyHeater kamheat 67676767 NOKEY'
+ comment = Multical302
telegram = 2E442D2C6767676730048D2039D1684020_BCDB7803062C000043060000000314630000426C7F2A022D130001FF2100
- json = '{"_":"telegram","id": "67676767","media": "heat","meter": "kamheat","name": "MyHeater","power_kw": 1.9,"status": "OK","target_date": "2019-10-31","target_energy_kwh": 0,"timestamp": "1111-11-11T11:11:11Z","total_energy_consumption_kwh": 44,"total_volume_m3": 0.99}'
+ json = '{"_":"telegram","id": "67676767","media": "heat","driver": "kamheat","name": "MyHeater","power_kw": 1.9,"status": "OK","target_date": "2019-10-31","target_energy_kwh": 0,"timestamp": "1111-11-11T11:11:11Z","total_energy_consumption_kwh": 44,"total_volume_m3": 0.99}'
fields = 'MyHeater;67676767;44;0.99;OK;1111-11-11 11:11.11'
}
test {
- args = 'MyHeater multical302 67676767 NOKEY'
- comment = 'Test second compressed telegram.'
+ args = 'MyHeater kamheat 67676767 NOKEY'
+ comment = 'Multical302 Test second compressed telegram.'
telegram = 2E442D2C6767676730048D2039D1684020_BCDB7803062C000043060000000314630000426C7F2A022D130001FF2100,25442D2C6767676730048D203AD2684020_D81579E7F1D5902C00000000006300007F2A130000
- json = '{"_":"telegram","id": "67676767","media": "heat","meter": "kamheat","name": "MyHeater","power_kw": 1.9,"status": "OK","target_date": "2019-10-31","target_energy_kwh": 0,"timestamp": "1111-11-11T11:11:11Z","total_energy_consumption_kwh": 44,"total_volume_m3": 0.99}'
+ json = '{"_":"telegram","id": "67676767","media": "heat","driver": "kamheat","name": "MyHeater","power_kw": 1.9,"status": "OK","target_date": "2019-10-31","target_energy_kwh": 0,"timestamp": "1111-11-11T11:11:11Z","total_energy_consumption_kwh": 44,"total_volume_m3": 0.99}'
fields = 'MyHeater;67676767;44;0.99;OK;1111-11-11 11:11.11'
}
test {
- args = 'MyHeaterMj multical302 46464646 NOKEY'
- comment = 'Using mj kwh'
+ args = 'MyHeaterMj kamheat 46464646 NOKEY'
+ comment = 'Multical302 Using mj kwh'
telegram = 2E442D2C46464646300C8D207A70EA6021B1C178_030FC51000430F9210000314072B05426CBE2B022D0C0001FF2100
- json = '{"_":"telegram","id": "46464646","media": "heat volume at inlet","meter": "kamheat","name": "MyHeaterMj","power_kw": 1.2,"status": "OK","target_date": "2021-11-30","target_energy_kwh": 11783.333333,"timestamp": "1111-11-11T11:11:11Z","total_energy_consumption_kwh": 11925,"total_volume_m3": 3386.95}'
+ json = '{"_":"telegram","id": "46464646","media": "heat volume at inlet","driver": "kamheat","name": "MyHeaterMj","power_kw": 1.2,"status": "OK","target_date": "2021-11-30","target_energy_kwh": 11783.333333,"timestamp": "1111-11-11T11:11:11Z","total_energy_consumption_kwh": 11925,"total_volume_m3": 3386.95}'
fields = 'MyHeaterMj;46464646;11925;3386.95;OK;1111-11-11 11:11.11'
}
test {
- args = 'MyHeaterMj multical302 46464646 NOKEY'
- comment = 'Test second compressed telegram.'
+ args = 'MyHeaterMj kamheat 46464646 NOKEY'
+ comment = 'Multical302 Test second compressed telegram.'
telegram = 2E442D2C46464646300C8D207A70EA6021B1C178_030FC51000430F9210000314072B05426CBE2B022D0C0001FF2100,25442D2C46464646300C8D20D3E2EB60212B6D79E26DCD65_C51000921000152B05BE2B0C0000
- json = '{"_":"telegram","id": "46464646","media": "heat volume at inlet","meter": "kamheat","name": "MyHeaterMj","power_kw": 1.2,"status": "OK","target_date": "2021-11-30","target_energy_kwh": 11783.333333,"timestamp": "1111-11-11T11:11:11Z","total_energy_consumption_kwh": 11925,"total_volume_m3": 3387.09}'
+ json = '{"_":"telegram","id": "46464646","media": "heat volume at inlet","driver": "kamheat","name": "MyHeaterMj","power_kw": 1.2,"status": "OK","target_date": "2021-11-30","target_energy_kwh": 11783.333333,"timestamp": "1111-11-11T11:11:11Z","total_energy_consumption_kwh": 11925,"total_volume_m3": 3387.09}'
fields = 'MyHeaterMj;46464646;11925;3387.09;OK;1111-11-11 11:11.11'
}
test {
- args = 'Heat multical603 36363636 NOKEY'
+ args = 'Heat kamheat 36363636 NOKEY'
+ comment = Multical603
telegram = 42442D2C3636363635048D20E18025B62087D078_0406A500000004FF072B01000004FF089C000000041421020000043B120000000259D014025D000904FF2200000000
- json = '{"_":"telegram","media":"heat","meter":"kamheat","name":"Heat","id":"36363636","status":"OK","total_energy_consumption_kwh":165,"total_volume_m3":5.45,"volume_flow_m3h":0.018,"t1_temperature_c":53.28,"t2_temperature_c":23.04,"forward_energy_m3c":299,"return_energy_m3c":156,"timestamp":"1111-11-11T11:11:11Z"}'
- fields = 'Heat;36363636;165;5.45;OK;1111-11-11 11:11.11'
+ json = '{"_":"telegram","media":"heat","driver":"kamheat","name":"Heat","id":"36363636","status":"OK","total_energy_consumption_kwh":165,"total_volume_m3":5.45,"volume_flow_m3h":0.018,"t1_temperature_c":53.28,"t2_temperature_c":23.04,"forward_energy_m3c":299,"return_energy_m3c":156,"timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'Heat;36363636;165;5.45;OK;1111-11-11 11:11.11'
}
test {
- args = 'HeatInlet multical603 66666666 NOKEY'
+ args = 'HeatInlet kamheat 66666666 NOKEY'
+ comment = Multical603
telegram = 5A442D2C66666666350C8D2066D0E16420C6A178_0406051C000004FF07393D000004FF08AE2400000414F7680000043B47000000042D1600000002596D14025DFD0804FF22000000000422E61A0000143B8C010000142D7C000000
- json = '{"_":"telegram","media":"heat volume at inlet","meter":"kamheat","name":"HeatInlet","id":"66666666","on_time_h":6886,"status":"OK","total_energy_consumption_kwh":7173,"total_volume_m3":268.71,"volume_flow_m3h":0.071,"power_kw":2.2,"max_power_kw":12.4,"t1_temperature_c":52.29,"t2_temperature_c":23.01,"max_flow_m3h":0.396,"forward_energy_m3c":15673,"return_energy_m3c":9390,"timestamp":"1111-11-11T11:11:11Z"}'
- fields = 'HeatInlet;66666666;7173;268.71;OK;1111-11-11 11:11.11'
+ json = '{"_":"telegram","media":"heat volume at inlet","driver":"kamheat","name":"HeatInlet","id":"66666666","on_time_h":6886,"status":"OK","total_energy_consumption_kwh":7173,"total_volume_m3":268.71,"volume_flow_m3h":0.071,"power_kw":2.2,"max_power_kw":12.4,"t1_temperature_c":52.29,"t2_temperature_c":23.01,"max_flow_m3h":0.396,"forward_energy_m3c":15673,"return_energy_m3c":9390,"timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'HeatInlet;66666666;7173;268.71;OK;1111-11-11 11:11.11'
}
test {
- args = 'My403Cooling multical403 78780102 NOKEY'
+ args = 'My403Cooling kamheat 78780102 NOKEY'
+ comment = Multical403
telegram = 88442D2C02017878340A8D208D529C132037FC78_040E2D0A000004FF07F8FF000004FF08401801000413C1900500844014000000008480401400000000043BED0000000259BC06025DCD07142DE7FFFFFF84100E0000000084200E0000000004FF2200000000026C9228440E5F0300004413960D0200C4401400000000C480401400000000426C8128
- json = '{"_":"telegram","forward_energy_m3c": 65528,"id": "78780102","max_power_kw": -2.5,"media": "cooling load volume at outlet","meter": "kamheat","meter_date": "2020-08-18","name": "My403Cooling","return_energy_m3c": 71744,"status": "OK","t1_temperature_c": 17.24,"t2_temperature_c": 19.97,"target_date": "2020-08-01","target_energy_kwh": 239.722222,"target_volume_m3": 134.55,"timestamp": "1111-11-11T11:11:11Z","total_energy_consumption_kwh": 723.611111,"total_volume_m3": 364.737,"volume_flow_m3h": 0.237,"target_volume_subunit1_m3": 0,"target_volume_subunit2_m3": 0,"total_volume_subunit1_m3": 0,"total_volume_subunit2_m3": 0,"timestamp": "1111-11-11T11:11:11Z"}'
- fields = 'My403Cooling;78780102;723.611111;364.737;OK;1111-11-11 11:11.11'
+ json = '{"_":"telegram","forward_energy_m3c": 65528,"id": "78780102","max_power_kw": -2.5,"media": "cooling load volume at outlet","driver": "kamheat","meter_date": "2020-08-18","name": "My403Cooling","return_energy_m3c": 71744,"status": "OK","t1_temperature_c": 17.24,"t2_temperature_c": 19.97,"target_date": "2020-08-01","target_energy_kwh": 239.722222,"target_volume_m3": 134.55,"timestamp": "1111-11-11T11:11:11Z","total_energy_consumption_kwh": 723.611111,"total_volume_m3": 364.737,"volume_flow_m3h": 0.237,"target_volume_subunit1_m3": 0,"target_volume_subunit2_m3": 0,"total_volume_subunit1_m3": 0,"total_volume_subunit2_m3": 0,"timestamp": "1111-11-11T11:11:11Z"}'
+ fields = 'My403Cooling;78780102;723.611111;364.737;OK;1111-11-11 11:11.11'
}
test {
- args = 'My403Cooling multical403 78780102 NOKEY'
+ args = 'My403Cooling kamheat 78780102 NOKEY'
+ comment = Multical403
telegram = 88442D2C02017878340A8D208D529C132037FC78_040E2D0A000004FF07F8FF000004FF08401801000413C1900500844014000000008480401400000000043BED0000000259BC06025DCD07142DE7FFFFFF84100E0000000084200E0000000004FF2200000000026C9228440E5F0300004413960D0200C4401400000000C480401400000000426C8128,5B442D2C02017878340A8D2096809C1320EF2B7934147ED7_2D0A0000FAFF000043180100CE9005000000000000000000EE000000BA06CB07E7FFFFFF00000000000000000000000092285F030000960D020000000000000000008128
- json = '{"_":"telegram","forward_energy_m3c": 65530,"id": "78780102","max_power_kw": -2.5,"media": "cooling load volume at outlet","meter": "kamheat","meter_date": "2020-08-18","name": "My403Cooling","return_energy_m3c": 71747,"status": "OK","t1_temperature_c": 17.22,"t2_temperature_c": 19.95,"target_date": "2020-08-01","target_energy_kwh": 239.722222,"target_volume_m3": 134.55,"timestamp": "1111-11-11T11:11:11Z","total_energy_consumption_kwh": 723.611111,"total_volume_m3": 364.75,"volume_flow_m3h": 0.238,"target_volume_subunit1_m3": 0,"target_volume_subunit2_m3": 0,"total_volume_subunit1_m3": 0,"total_volume_subunit2_m3": 0}'
- fields = 'My403Cooling;78780102;723.611111;364.75;OK;1111-11-11 11:11.11'
+ json = '{"_":"telegram","forward_energy_m3c": 65530,"id": "78780102","max_power_kw": -2.5,"media": "cooling load volume at outlet","driver": "kamheat","meter_date": "2020-08-18","name": "My403Cooling","return_energy_m3c": 71747,"status": "OK","t1_temperature_c": 17.22,"t2_temperature_c": 19.95,"target_date": "2020-08-01","target_energy_kwh": 239.722222,"target_volume_m3": 134.55,"timestamp": "1111-11-11T11:11:11Z","total_energy_consumption_kwh": 723.611111,"total_volume_m3": 364.75,"volume_flow_m3h": 0.238,"target_volume_subunit1_m3": 0,"target_volume_subunit2_m3": 0,"total_volume_subunit1_m3": 0,"total_volume_subunit2_m3": 0}'
+ fields = 'My403Cooling;78780102;723.611111;364.75;OK;1111-11-11 11:11.11'
}
test {
- args = 'Heato multical602 78152801 NOKEY'
+ args = 'Heato kamheat 78152801 NOKEY'
+ comment = Multical602
telegram = 7A442D2C012815781C048D206450E76322344678_02F9FF1511130406690B010004EEFF07C1BC020004EEFF0890D401000414A925040084401400000000848040140000000002FD170000026CB929426CBF284406100A01004414D81A0400C4401400000000C480401400000000043B3900000002592A17025D2912
- json = '{"_":"telegram","id": "78152801","media": "heat","meter": "kamheat","meter_date": "2021-09-25","name": "Heato","status": "OK","t1_temperature_c": 59.3,"t2_temperature_c": 46.49,"target_date": "2021-08-31","target_energy_kwh": 68112,"target_volume_m3": 2690.16,"timestamp": "1111-11-11T11:11:11Z","total_energy_consumption_kwh": 68457,"total_volume_m3": 2717.85,"volume_flow_m3h": 0.057, "va_counter": 179393,"vb_counter": 119952,"enhanced_id": "1311","target_volume_subunit1_m3": 0,"target_volume_subunit2_m3": 0,"total_volume_subunit1_m3": 0,"total_volume_subunit2_m3": 0}'
- fields = 'Heato;78152801;68457;2717.85;OK;1111-11-11 11:11.11'
+ json = '{"_":"telegram","id": "78152801","media": "heat","driver": "kamheat","meter_date": "2021-09-25","name": "Heato","status": "OK","t1_temperature_c": 59.3,"t2_temperature_c": 46.49,"target_date": "2021-08-31","target_energy_kwh": 68112,"target_volume_m3": 2690.16,"timestamp": "1111-11-11T11:11:11Z","total_energy_consumption_kwh": 68457,"total_volume_m3": 2717.85,"volume_flow_m3h": 0.057, "va_counter": 179393,"vb_counter": 119952,"enhanced_id": "1311","target_volume_subunit1_m3": 0,"target_volume_subunit2_m3": 0,"total_volume_subunit1_m3": 0,"total_volume_subunit2_m3": 0}'
+ fields = 'Heato;78152801;68457;2717.85;OK;1111-11-11 11:11.11'
}
test {
- args = 'Heato multical602 78152801 NOKEY'
+ args = 'Heato kamheat 78152801 NOKEY'
+ comment = Multical602
telegram = 7A442D2C012815781C048D206450E76322344678_02F9FF1511130406690B010004EEFF07C1BC020004EEFF0890D401000414A925040084401400000000848040140000000002FD170000026CB929426CBF284406100A01004414D81A0400C4401400000000C480401400000000043B3900000002592A17025D2912,4F442D2C012815781C048D206551E76322BE767900843005_1113690B0100C1BC020090D40100A925040000000000000000000000B929BF28100A0100D81A04000000000000000000390000002A172912
- json = '{"_":"telegram","id": "78152801","media": "heat","meter": "kamheat","meter_date": "2021-09-25","name": "Heato","status": "OK","t1_temperature_c": 59.3,"t2_temperature_c": 46.49,"target_date": "2021-08-31","target_energy_kwh": 68112,"target_volume_m3": 2690.16,"timestamp": "1111-11-11T11:11:11Z","total_energy_consumption_kwh": 68457,"total_volume_m3": 2717.85,"volume_flow_m3h": 0.057,"enhanced_id": "1311","va_counter": 179393,"vb_counter": 119952,"target_volume_subunit1_m3": 0,"target_volume_subunit2_m3": 0,"total_volume_subunit1_m3": 0,"total_volume_subunit2_m3": 0}'
- fields = 'Heato;78152801;68457;2717.85;OK;1111-11-11 11:11.11'
+ json = '{"_":"telegram","id": "78152801","media": "heat","driver": "kamheat","meter_date": "2021-09-25","name": "Heato","status": "OK","t1_temperature_c": 59.3,"t2_temperature_c": 46.49,"target_date": "2021-08-31","target_energy_kwh": 68112,"target_volume_m3": 2690.16,"timestamp": "1111-11-11T11:11:11Z","total_energy_consumption_kwh": 68457,"total_volume_m3": 2717.85,"volume_flow_m3h": 0.057,"enhanced_id": "1311","va_counter": 179393,"vb_counter": 119952,"target_volume_subunit1_m3": 0,"target_volume_subunit2_m3": 0,"total_volume_subunit1_m3": 0,"total_volume_subunit2_m3": 0}'
+ fields = 'Heato;78152801;68457;2717.85;OK;1111-11-11 11:11.11'
}
test {
- args = 'Heater multical803 80808081 NOKEY'
+ args = 'Heater kamheat 80808081 NOKEY'
+ comment = Multical803
telegram = 88442D2C8180808039048D208640513220EA7978_040FA000000004FF070200000004FF08090000000414FF000000844014000000008480401400000000043B0000000002590000025D0000142D0000000084100F0000000084200F0000000004FF2260000100026C892B440F00000000441400000000C4401400000000C480401400000000426C812B
- json = '{"_":"telegram","forward_energy_m3c": 2,"id": "80808081","max_power_kw": 0,"media": "heat","meter": "kamheat","meter_date": "2020-11-09","name": "Heater","return_energy_m3c": 9,"status": "SENSOR_T1_BELOW_MEASURING_RANGE SENSOR_T2_BELOW_MEASURING_RANGE V1_COMMUNICATION_ERROR","t1_temperature_c": 0,"t2_temperature_c": 0,"target_date": "2020-11-01","target_energy_kwh": 0,"target_volume_m3": 0,"timestamp": "1111-11-11T11:11:11Z","total_energy_consumption_kwh": 444.444444,"total_volume_m3": 2.55,"volume_flow_m3h": 0,"target_volume_subunit1_m3": 0,"target_volume_subunit2_m3": 0,"total_volume_subunit1_m3": 0,"total_volume_subunit2_m3": 0}'
- fields = 'Heater;80808081;444.444444;2.55;SENSOR_T1_BELOW_MEASURING_RANGE SENSOR_T2_BELOW_MEASURING_RANGE V1_COMMUNICATION_ERROR;1111-11-11 11:11.11'
+ json = '{"_":"telegram","forward_energy_m3c": 2,"id": "80808081","max_power_kw": 0,"media": "heat","driver": "kamheat","meter_date": "2020-11-09","name": "Heater","return_energy_m3c": 9,"status": "SENSOR_T1_BELOW_MEASURING_RANGE SENSOR_T2_BELOW_MEASURING_RANGE V1_COMMUNICATION_ERROR","t1_temperature_c": 0,"t2_temperature_c": 0,"target_date": "2020-11-01","target_energy_kwh": 0,"target_volume_m3": 0,"timestamp": "1111-11-11T11:11:11Z","total_energy_consumption_kwh": 444.444444,"total_volume_m3": 2.55,"volume_flow_m3h": 0,"target_volume_subunit1_m3": 0,"target_volume_subunit2_m3": 0,"total_volume_subunit1_m3": 0,"total_volume_subunit2_m3": 0}'
+ fields = 'Heater;80808081;444.444444;2.55;SENSOR_T1_BELOW_MEASURING_RANGE SENSOR_T2_BELOW_MEASURING_RANGE V1_COMMUNICATION_ERROR;1111-11-11 11:11.11'
}
test {
- args = 'Kamstrup_303 kamheat 78787878 NOKEY'
+ args = 'Kamstrup_303 kamheat 78787878 NOKEY'
telegram = 5E442D2C78787878400C7A6E0050252F2F_04056C2B000004138A0B070004FF07C657020004FF08FD36020002594B09025DFA08023B000002FF220000026CF42144052F000000441302AD0000426CE1212F2F2F2F2F2F2F2F2F2F2F2F2F2F2F
- json = '{"_":"telegram","media":"heat volume at inlet","meter":"kamheat","name":"Kamstrup_303","id":"78787878","status":"OK","total_energy_consumption_kwh":1111.6,"total_volume_m3":461.706,"volume_flow_m3h":0,"t1_temperature_c":23.79,"t2_temperature_c":22.98,"forward_energy_m3c":153542,"return_energy_m3c":145149,"meter_date":"2023-01-20","target_energy_kwh":4.7,"target_volume_m3":44.29,"target_date":"2023-01-01","timestamp":"1111-11-11T11:11:11Z"}'
- fields = 'Kamstrup_303;78787878;1111.6;461.706;OK;1111-11-11 11:11.11'
+ json = '{"_":"telegram","media":"heat volume at inlet","driver":"kamheat","name":"Kamstrup_303","id":"78787878","status":"OK","total_energy_consumption_kwh":1111.6,"total_volume_m3":461.706,"volume_flow_m3h":0,"t1_temperature_c":23.79,"t2_temperature_c":22.98,"forward_energy_m3c":153542,"return_energy_m3c":145149,"meter_date":"2023-01-20","target_energy_kwh":4.7,"target_volume_m3":44.29,"target_date":"2023-01-01","timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'Kamstrup_303;78787878;1111.6;461.706;OK;1111-11-11 11:11.11'
}
test {
- args = 'Kamstrup_403_mbus kamheat 77447744 NOKEY'
+ args = 'Kamstrup_403_mbus kamheat 77447744 NOKEY'
telegram = 68464668084a72447744772d2c3404060000000406ce86000004ff073444020004ff08f8ce0100041411680300043B0f02000002593c19025da41104ff220000000004a5ff21c7d02700d916
- json = '{"_":"telegram","forward_energy_m3c": 148532,"id": "77447744","media": "heat","meter": "kamheat","name": "Kamstrup_403_mbus","return_energy_m3c": 118520,"status": "OK","t1_temperature_c": 64.6,"t2_temperature_c": 45.16,"timestamp": "1111-11-11T11:11:11Z","total_energy_consumption_kwh": 34510,"total_volume_m3": 2232.49,"volume_flow_m3h": 0.527,"operating_time_h": 43489.183333}'
- fields = 'Kamstrup_403_mbus;77447744;34510;2232.49;OK;1111-11-11 11:11.11'
+ json = '{"_":"telegram","forward_energy_m3c": 148532,"id": "77447744","media": "heat","driver": "kamheat","name": "Kamstrup_403_mbus","return_energy_m3c": 118520,"status": "OK","t1_temperature_c": 64.6,"t2_temperature_c": 45.16,"timestamp": "1111-11-11T11:11:11Z","total_energy_consumption_kwh": 34510,"total_volume_m3": 2232.49,"volume_flow_m3h": 0.527,"operating_time_h": 43489.183333}'
+ fields = 'Kamstrup_403_mbus;77447744;34510;2232.49;OK;1111-11-11 11:11.11'
}
test {
- args = 'Kamstrup_402_wmbus kamheat 62215006 NOKEY'
+ args = 'Kamstrup_402_wmbus kamheat 62215006 NOKEY'
telegram = 40442D2C0650216219048D2083A4E1162306FF78_040F2C3F000004FF07DBA40D0004FF08860B0D000414BA33140002FD170000043B620000000259A21E025DFA1B
- json = '{"_":"telegram","media":"heat","meter":"kamheat","name":"Kamstrup_402_wmbus","id":"62215006","forward_energy_m3c":894171,"return_energy_m3c":854918,"t1_temperature_c":78.42,"t2_temperature_c":71.62,"total_energy_consumption_kwh":44922.222222,"total_volume_m3":13239.62,"volume_flow_m3h":0.098,"status":"OK","timestamp":"1111-11-11T11:11:11Z"}'
- fields = 'Kamstrup_402_wmbus;62215006;44922.222222;13239.62;OK;1111-11-11 11:11.11'
+ json = '{"_":"telegram","media":"heat","driver":"kamheat","name":"Kamstrup_402_wmbus","id":"62215006","forward_energy_m3c":894171,"return_energy_m3c":854918,"t1_temperature_c":78.42,"t2_temperature_c":71.62,"total_energy_consumption_kwh":44922.222222,"total_volume_m3":13239.62,"volume_flow_m3h":0.098,"status":"OK","timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'Kamstrup_402_wmbus;62215006;44922.222222;13239.62;OK;1111-11-11 11:11.11'
}
test {
- args = 'Kamstrup_MC603_mbus kamheat 32323232 NOKEY'
+ args = 'Kamstrup_MC603_mbus kamheat 32323232 NOKEY'
telegram = 68c9c96808e672323232322d2c35041900000004fB006083000004ff074006010004ff08299400000416984e010084401400000000848040140000000004225043000034221c0000000259c91f025d4f1102617a0e042e30020000142e65030000043c24050000143ce308000004ff2200000000046d2e2B0f3144fB00007d000044ff07Bdf9000044ff08308d00004416B73f0100c4401400000000c480401400000000542ed9020000543ce8090000426c013102ff1a011B0c783032858404ff16e5841e0004ff17c1d5B400a516
- json = '{"_":"telegram","fabrication_no": "84853230", "flow_return_temperature_difference_c": 37.06, "forward_energy_m3c": 67136, "id": "32323232", "max_flow_m3h": 22.75, "max_power_kw": 869, "media": "heat", "meter": "kamheat", "meter_datetime": "2024-01-15 11:46", "name": "Kamstrup_MC603_mbus", "on_time_at_error_h": 28, "on_time_h": 17232, "power_kw": 560, "return_energy_m3c": 37929, "status": "OK", "t1_temperature_c": 81.37, "t2_temperature_c": 44.31, "target_date": "2024-01-01", "target_energy_kwh": 3200000, "target_volume_m3": 81847, "timestamp": "1111-11-11T11:11:11Z", "total_energy_consumption_kwh": 3363200, "total_volume_m3": 85656, "volume_flow_m3h": 13.16,"target_volume_subunit1_m3": 0,"target_volume_subunit2_m3": 0,"total_volume_subunit1_m3": 0,"total_volume_subunit2_m3": 0}'
- fields = 'Kamstrup_MC603_mbus;32323232;3363200;85656;OK;1111-11-11 11:11.11'
- }
- test {
- args = 'KMHEAT kamheat 85412440 NOKEY'
- telegram = 5e442d2c4024418535047ae10050252f2f04fB091300000004167500000004ff2200000000043ca301000002599c1d025dB00e844014000000008480401400000000042eB9000000026c0534426c013444fB0900000000543c000000002f2f
- json = '{"_":"telegram","id": "85412440","media": "heat","meter": "kamheat","meter_date": "2024-04-05","name": "KMHEAT","power_kw": 185,"status": "OK","t1_temperature_c": 75.8,"t2_temperature_c": 37.6,"target_date": "2024-04-01","target_energy_kwh": 0,"timestamp": "1111-11-11T11:11:11Z","total_energy_consumption_kwh": 5277.777778,"total_volume_m3": 117,"volume_flow_m3h": 4.19,"total_volume_subunit1_m3": 0,"total_volume_subunit2_m3": 0}'
- fields = 'KMHEAT;85412440;5277.777778;117;OK;1111-11-11 11:11.11'
- }
- test {
- args = 'KM2 kamheat 85007420 NOKEY'
- telegram = 689f9f68081472207400852d2c350cad00000004fb08c304000004ff07f21c000004ff084d1100000415fe29000084401400000000848040140000000004228829000034220a00000002598511025dab100261da00042d00000000142d08000000043b00000000143b6000000004ff2200000000046d0d2c0238542dc9010000543be1050000426c013102ff1a011b0c782074008504ff16850b200004ff17c9ff0e01cb16
- json = '{"_":"telegram","fabrication_no": "85007420","flow_return_temperature_difference_c": 2.18,"forward_energy_m3c": 7410,"id": "85007420","max_flow_m3h": 0.096,"max_power_kw": 0.8,"media": "heat volume at inlet","meter": "kamheat","meter_datetime": "2024-08-02 12:13","name": "KM2","on_time_at_error_h": 10,"on_time_h": 10632,"power_kw": 0,"return_energy_m3c": 4429,"status": "OK","t1_temperature_c": 44.85,"t2_temperature_c": 42.67,"target_date": "2024-01-01","timestamp": "1111-11-11T11:11:11Z","total_energy_consumption_kwh": 33861.111111,"total_volume_m3": 1075,"volume_flow_m3h": 0,"total_volume_subunit1_m3": 0,"total_volume_subunit2_m3": 0}'
- fields = 'KM2;85007420;33861.111111;1075;OK;1111-11-11 11:11.11'
- }
- test {
- args = 'Kamstrup_MC303_Heat_AND_COOLING kamheat 30303030 NOKEY'
- telegram = 68888868083072303030302d2c400d0000000004069a00000004863c0000000004146d0f000004ff077205000004ff08ec0400000322890a0033220000000259720b025d040b02616e00022d0e00122db400023b6804123b7a0402ff22000044060000000044863c0000000044140000000044ff070000000044ff0800000000522d0000523b0000426c00006C16
- json = '{"_":"telegram","media":"heat/cooling load","meter":"kamheat","name":"Kamstrup_MC303_Heat_AND_COOLING","id":"30303030","flow_return_temperature_difference_c":1.1,"forward_energy_m3c":1394,"max_flow_m3h":1.146,"max_power_kw":18,"on_time_h":2697,"on_time_at_error_h":0,"power_kw":1.4,"return_energy_m3c":1260,"t1_temperature_c":29.3,"t2_temperature_c":28.2,"target_energy_kwh":0,"target_volume_m3":0,"total_energy_backward_kwh":0,"total_energy_consumption_kwh":154,"total_volume_m3":39.49,"volume_flow_m3h":1.128,"status":"OK","target_date":"1999-11-30","timestamp":"1111-11-11T11:11:11Z"}'
- fields = 'Kamstrup_MC303_Heat_AND_COOLING;30303030;154;39.49;OK;1111-11-11 11:11.11'
+ json = '{"_":"telegram","fabrication_no": "84853230", "flow_return_temperature_difference_c": 37.06, "forward_energy_m3c": 67136, "id": "32323232", "max_flow_m3h": 22.75, "max_power_kw": 869, "media": "heat", "driver": "kamheat", "meter_datetime": "2024-01-15 11:46", "name": "Kamstrup_MC603_mbus", "on_time_at_error_h": 28, "on_time_h": 17232, "power_kw": 560, "return_energy_m3c": 37929, "status": "OK", "t1_temperature_c": 81.37, "t2_temperature_c": 44.31, "target_date": "2024-01-01", "target_energy_kwh": 3200000, "target_volume_m3": 81847, "timestamp": "1111-11-11T11:11:11Z", "total_energy_consumption_kwh": 3363200, "total_volume_m3": 85656, "volume_flow_m3h": 13.16,"target_volume_subunit1_m3": 0,"target_volume_subunit2_m3": 0,"total_volume_subunit1_m3": 0,"total_volume_subunit2_m3": 0,"meter_type": "1B01","module_software_nr": 11851201,"module_type_config_nr": 2000101,"target_forward_energy_m3c": 63933,"target_max_flow_m3h": 25.36,"target_max_power_kw": 729,"target_return_energy_m3c": 36144}'
+ fields = 'Kamstrup_MC603_mbus;32323232;3363200;85656;OK;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'KMHEAT kamheat 85412440 NOKEY'
+ telegram = 5e442d2c4024418535047ae10050252f2f04fB091300000004167500000004ff2200000000043ca301000002599c1d025dB00e844014000000008480401400000000042eB9000000026c0534426c013444fB0900000000543c000000002f2f
+ json = '{"_":"telegram","id": "85412440","media": "heat","driver": "kamheat","meter_date": "2024-04-05","name": "KMHEAT","power_kw": 185,"status": "OK","t1_temperature_c": 75.8,"t2_temperature_c": 37.6,"target_date": "2024-04-01","target_energy_kwh": 0,"timestamp": "1111-11-11T11:11:11Z","total_energy_consumption_kwh": 5277.777778,"total_volume_m3": 117,"volume_flow_m3h": 4.19,"total_volume_subunit1_m3": 0,"total_volume_subunit2_m3": 0,"target_max_flow_m3h": 0}'
+ fields = 'KMHEAT;85412440;5277.777778;117;OK;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'KM2 kamheat 85007420 NOKEY'
+ telegram = 689f9f68081472207400852d2c350cad00000004fb08c304000004ff07f21c000004ff084d1100000415fe29000084401400000000848040140000000004228829000034220a00000002598511025dab100261da00042d00000000142d08000000043b00000000143b6000000004ff2200000000046d0d2c0238542dc9010000543be1050000426c013102ff1a011b0c782074008504ff16850b200004ff17c9ff0e01cb16
+ json = '{"_":"telegram","fabrication_no": "85007420","flow_return_temperature_difference_c": 2.18,"forward_energy_m3c": 7410,"id": "85007420","max_flow_m3h": 0.096,"max_power_kw": 0.8,"media": "heat volume at inlet","driver": "kamheat","meter_datetime": "2024-08-02 12:13","name": "KM2","on_time_at_error_h": 10,"on_time_h": 10632,"power_kw": 0,"return_energy_m3c": 4429,"status": "OK","t1_temperature_c": 44.85,"t2_temperature_c": 42.67,"target_date": "2024-01-01","timestamp": "1111-11-11T11:11:11Z","total_energy_consumption_kwh": 33861.111111,"total_volume_m3": 1075,"volume_flow_m3h": 0,"total_volume_subunit1_m3": 0,"total_volume_subunit2_m3": 0,"meter_type": "1B01","module_software_nr": 17760201,"module_type_config_nr": 2100101,"target_max_flow_m3h": 1.505,"target_max_power_kw": 45.7}'
+ fields = 'KM2;85007420;33861.111111;1075;OK;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'Kamstrup_MC303_Heat_AND_COOLING kamheat 30303030 NOKEY'
+ telegram = 68888868083072303030302d2c400d0000000004069a00000004863c0000000004146d0f000004ff077205000004ff08ec0400000322890a0033220000000259720b025d040b02616e00022d0e00122db400023b6804123b7a0402ff22000044060000000044863c0000000044140000000044ff070000000044ff0800000000522d0000523b0000426c00006C16
+ json = '{"_":"telegram","media":"heat/cooling load","driver":"kamheat","name":"Kamstrup_MC303_Heat_AND_COOLING","id":"30303030","flow_return_temperature_difference_c":1.1,"forward_energy_m3c":1394,"max_flow_m3h":1.146,"max_power_kw":18,"on_time_h":2697,"on_time_at_error_h":0,"power_kw":1.4,"return_energy_m3c":1260,"t1_temperature_c":29.3,"t2_temperature_c":28.2,"target_energy_kwh":0,"target_volume_m3":0,"total_energy_backward_kwh":0,"total_energy_consumption_kwh":154,"total_volume_m3":39.49,"volume_flow_m3h":1.128,"status":"OK","target_date":"1999-11-30","target_energy_backward_kwh":0,"timestamp":"1111-11-11T11:11:11Z","target_forward_energy_m3c": 0,"target_max_flow_m3h": 0,"target_max_power_kw": 0,"target_return_energy_m3c": 0}'
+ fields = 'Kamstrup_MC303_Heat_AND_COOLING;30303030;154;39.49;OK;1111-11-11 11:11.11'
+ }
+ test {
+ comment = 'KAM,30,04 - Long Telegram'
+ args = 'kamheat_30_04 kamheat 67947613 NOKEY'
+ telegram = 2E442D2C1376946730048D20EA00A80B20E1347803068E010043060000000314CA0900426CFFFF022D010001FF2100
+ json = '{"_":"telegram","media":"heat","driver":"kamheat","name":"kamheat_30_04","id":"67947613","power_kw":0.1,"target_date":null,"target_energy_kwh":0,"total_energy_consumption_kwh":398,"total_volume_m3":25.06,"status":"OK","timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'kamheat_30_04;67947613;398;25.06;OK;1111-11-11 11:11.11'
+ }
+ test {
+ comment = 'KAM,30,04 - Compact Telegram'
+ args = 'kamheat_30_04 kamheat 67947613 NOKEY'
+ telegram = 25442D2C1376946730048D20E9F1A70B20154D79E7F1887F8E0100000000CA0900FFFF010000
+ json = '{"_":"telegram","media":"heat","driver":"kamheat","name":"kamheat_30_04","id":"67947613","power_kw":0.1,"target_date":null,"target_energy_kwh":0,"total_energy_consumption_kwh":398,"total_volume_m3":25.06,"status":"OK","timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'kamheat_30_04;67947613;398;25.06;OK;1111-11-11 11:11.11'
+ }
+ test {
+ comment = 'KAM,30,0c - Long Telegram'
+ args = 'kamheat_30_0c kamheat 67051653 NOKEY'
+ telegram = 2E442D2C53160567300C8D2093D055F824AD3A78030E90DE00430EE9DD00031334EE17426C1F33022D000001FF2100
+ json = '{"_":"telegram","media":"heat volume at inlet","driver":"kamheat","name":"kamheat_30_0c","id":"67051653","power_kw":0,"target_date":"2024-03-31","target_energy_kwh":15780.277778,"total_energy_consumption_kwh":15826.666667,"total_volume_m3":1568.308,"status":"OK","timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'kamheat_30_0c;67051653;15826.666667;1568.308;OK;1111-11-11 11:11.11'
+ }
+ test {
+ comment = 'KAM,30,0c - Compact Telegram'
+ args = 'kamheat_30_0c kamheat 67051653 NOKEY'
+ telegram = 25442D2C53160567300C8D2092C155F824389079C252C47190DE00E9DD0034EE171F33000000
+ json = '{"_":"telegram","media":"heat volume at inlet","driver":"kamheat","name":"kamheat_30_0c","id":"67051653","power_kw":0,"target_date":"2024-03-31","target_energy_kwh":15780.277778,"total_energy_consumption_kwh":15826.666667,"total_volume_m3":1568.308,"status":"OK","timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'kamheat_30_0c;67051653;15826.666667;1568.308;OK;1111-11-11 11:11.11'
+ }
+ test {
+ comment = 'KAM,34,0c - Long Telegram'
+ args = 'kamheat_34_0c kamheat 71054899 NOKEY'
+ telegram = 56442D2C99480571340C8D2019708C8322C88F780406AF7D000004FF076556020004FF08D7E701000414CC7A0500043B0000000002598F0C025D920D04FF2200000000026CDF214406AF7D00004414CC7A0500426CC121
+ json = '{"_":"telegram","media":"heat volume at inlet","driver":"kamheat","name":"kamheat_34_0c","id":"71054899","forward_energy_m3c":153189,"return_energy_m3c":124887,"t1_temperature_c":32.15,"t2_temperature_c":34.74,"target_date":"2022-01-01","target_energy_kwh":32175,"target_volume_m3":3591.16,"total_energy_consumption_kwh":32175,"total_volume_m3":3591.16,"volume_flow_m3h":0,"meter_date":"2022-01-31","status":"OK","timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'kamheat_34_0c;71054899;32175;3591.16;OK;1111-11-11 11:11.11'
+ }
+ test {
+ comment = 'KAM,34,0c - Compact Telegram'
+ args = 'kamheat_34_0c kamheat 71054899 NOKEY'
+ telegram = 3F442D2C99480571340C8D201B728C8322BE3D794DB3E88DAF7D000065560200D7E70100CC7A050000000000810C930D00000000DF21AF7D0000CC7A0500C121
+ json = '{"_":"telegram","media":"heat volume at inlet","driver":"kamheat","name":"kamheat_34_0c","id":"71054899","forward_energy_m3c":153189,"return_energy_m3c":124887,"t1_temperature_c":32.01,"t2_temperature_c":34.75,"target_date":"2022-01-01","target_energy_kwh":32175,"target_volume_m3":3591.16,"total_energy_consumption_kwh":32175,"total_volume_m3":3591.16,"volume_flow_m3h":0,"meter_date":"2022-01-31","status":"OK","timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'kamheat_34_0c;71054899;32175;3591.16;OK;1111-11-11 11:11.11'
+ }
+ test {
+ comment = 'KAM,39,04 - Long Telegram'
+ args = 'kamheat_39_04 kamheat 79806477 NOKEY'
+ telegram = 88442D2C7764807939048D20864051322084C178040F0000000004FF070000000004FF0800000000041400000000844014000000008480401400000000043B0000000002590000025D0000142D0000000084100F0000000084200F0000000004FF2260000100026C892B440F00000000441400000000C4401400000000C480401400000000426C812B
+ json = '{"_":"telegram","media":"heat","driver":"kamheat","name":"kamheat_39_04","id":"79806477","forward_energy_m3c":0,"max_power_kw":0,"return_energy_m3c":0,"t1_temperature_c":0,"t2_temperature_c":0,"target_date":"2020-11-01","target_energy_kwh":0,"target_volume_m3":0,"target_volume_subunit1_m3":0,"target_volume_subunit2_m3":0,"total_energy_consumption_kwh":0,"total_volume_m3":0,"total_volume_subunit1_m3":0,"total_volume_subunit2_m3":0,"volume_flow_m3h":0,"meter_date":"2020-11-09","status":"SENSOR_T1_BELOW_MEASURING_RANGE SENSOR_T2_BELOW_MEASURING_RANGE V1_COMMUNICATION_ERROR","timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'kamheat_39_04;79806477;0;0;SENSOR_T1_BELOW_MEASURING_RANGE SENSOR_T2_BELOW_MEASURING_RANGE V1_COMMUNICATION_ERROR;1111-11-11 11:11.11'
+ }
+ test {
+ comment = 'KAM,39,04 - Compact Telegram'
+ args = 'kamheat_39_04 kamheat 79806477 NOKEY'
+ telegram = 5B442D2C7764807939048D20870055322006F779A2E76FFD000000000000000000000000000000000000000000000000000000000000000000000000000000000000000060000100892B00000000000000000000000000000000812B
+ json = '{"_":"telegram","media":"heat","driver":"kamheat","name":"kamheat_39_04","id":"79806477","forward_energy_m3c":0,"max_power_kw":0,"return_energy_m3c":0,"t1_temperature_c":0,"t2_temperature_c":0,"target_date":"2020-11-01","target_energy_kwh":0,"target_volume_m3":0,"target_volume_subunit1_m3":0,"target_volume_subunit2_m3":0,"total_energy_consumption_kwh":0,"total_volume_m3":0,"total_volume_subunit1_m3":0,"total_volume_subunit2_m3":0,"volume_flow_m3h":0,"meter_date":"2020-11-09","status":"SENSOR_T1_BELOW_MEASURING_RANGE SENSOR_T2_BELOW_MEASURING_RANGE V1_COMMUNICATION_ERROR","timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'kamheat_39_04;79806477;0;0;SENSOR_T1_BELOW_MEASURING_RANGE SENSOR_T2_BELOW_MEASURING_RANGE V1_COMMUNICATION_ERROR;1111-11-11 11:11.11'
+ }
+ test {
+ comment = 'KAM,35,0d - MC603 heat/cooling over wired M-Bus, cooling energy E3 as 8F FF 02'
+ args = 'MC603_heat_cool kamheat 11223344 NOKEY'
+ telegram = 68d7d768082a72443322112d2c350d07000000040f26af0000048fff020000000004ff07b27a020004ff0860d60000041448df020084401400000000848040140000000004227943000034222fb9000002594813025d05090261430a042d40000000142d31000000043bd3000000143ba400000004ff2200000000046d30294739440f14ae0000448fff020000000044ff07c975020044ff0807d40000441483d50200c4401400000000c480401400000000542d31000000543ba4000000426c413902ff1a021b0c784433221104ff16e6841e0004ff172d000f010616
+ json = '{"_":"telegram","driver":"kamheat","fabrication_no":"11223344","flow_return_temperature_difference_c":26.27,"forward_energy_m3c":162482,"id":"11223344","max_flow_m3h":0.164,"max_power_kw":4.9,"media":"heat/cooling load","meter_datetime":"2026-09-07 09:48","meter_type":"1B02","module_software_nr":17760301,"module_type_config_nr":2000102,"name":"MC603_heat_cool","on_time_at_error_h":47407,"on_time_h":17273,"power_kw":6.4,"return_energy_m3c":54880,"status":"OK","t1_temperature_c":49.36,"t2_temperature_c":23.09,"target_date":"2026-09-01","target_energy_cooling_kwh":0,"target_energy_kwh":123788.888889,"target_forward_energy_m3c":161225,"target_max_flow_m3h":0.164,"target_max_power_kw":4.9,"target_return_energy_m3c":54279,"target_volume_m3":1857.31,"target_volume_subunit1_m3":0,"target_volume_subunit2_m3":0,"timestamp":"1111-11-11T11:11:11Z","total_energy_consumption_kwh":124550,"total_energy_cooling_kwh":0,"total_volume_m3":1882.32,"total_volume_subunit1_m3":0,"total_volume_subunit2_m3":0,"volume_flow_m3h":0.211}'
+ fields = 'MC603_heat_cool;11223344;124550;1882.32;OK;1111-11-11 11:11.11'
+ }
+ test {
+ comment = 'KAM,1c,04 - Long Telegram'
+ args = 'kamheat_1c_04 kamheat 78152801 NOKEY'
+ telegram = 7A442D2C012815781C048D206C70E7632234467802F9FF1511130406690B010004EEFF07C1BC020004EEFF0890D401000414A925040084401400000000848040140000000002FD170000026CB929426CBF284406100A01004414D81A0400C4401400000000C480401400000000043B3900000002592A17025D2912
+ json = '{"_":"telegram","media":"heat","driver":"kamheat","name":"kamheat_1c_04","id":"78152801","t1_temperature_c":59.3,"t2_temperature_c":46.49,"target_date":"2021-08-31","target_energy_kwh":68112,"target_volume_m3":2690.16,"target_volume_subunit1_m3":0,"target_volume_subunit2_m3":0,"total_energy_consumption_kwh":68457,"total_volume_m3":2717.85,"total_volume_subunit1_m3":0,"total_volume_subunit2_m3":0,"va_counter":179393,"vb_counter":119952,"volume_flow_m3h":0.057,"enhanced_id":"1311","meter_date":"2021-09-25","status":"OK","timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'kamheat_1c_04;78152801;68457;2717.85;OK;1111-11-11 11:11.11'
+ }
+ test {
+ comment = 'KAM,1c,04 - Compact Telegram'
+ args = 'kamheat_1c_04 kamheat 78152801 NOKEY'
+ telegram = 4F442D2C012815781C048D207171E76322BE7679008430051113690B0100C1BC020090D40100A925040000000000000000000000B929BF28100A0100D81A04000000000000000000390000002A172912
+ json = '{"_":"telegram","media":"heat","driver":"kamheat","name":"kamheat_1c_04","id":"78152801","t1_temperature_c":59.3,"t2_temperature_c":46.49,"target_date":"2021-08-31","target_energy_kwh":68112,"target_volume_m3":2690.16,"target_volume_subunit1_m3":0,"target_volume_subunit2_m3":0,"total_energy_consumption_kwh":68457,"total_volume_m3":2717.85,"total_volume_subunit1_m3":0,"total_volume_subunit2_m3":0,"va_counter":179393,"vb_counter":119952,"volume_flow_m3h":0.057,"enhanced_id":"1311","meter_date":"2021-09-25","status":"OK","timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'kamheat_1c_04;78152801;68457;2717.85;OK;1111-11-11 11:11.11'
}
}
test {
- args = '303_wrong_flow kamheat 83076912 NOKEY'
+ args = '303_wrong_flow kamheat 83076912 NOKEY'
telegram = 52442d2c1269078340048d2067e08d1620d2ca7804060000000004140000000004ff070000000004ff080000000002592618025d2816023B000002ff220002026c393B440600000000441400000000426c213B
- json = '{"_":"telegram","media":"heat","meter":"kamheat","name":"303_wrong_flow","id":"83076912","forward_energy_m3c":0,"return_energy_m3c":0,"t1_temperature_c":61.82,"t2_temperature_c":56.72,"target_date":"2025-11-01","target_energy_kwh":0,"target_volume_m3":0,"total_energy_consumption_kwh":0,"total_volume_m3":0,"volume_flow_m3h":0,"meter_date":"2025-11-25","status":"WRONG_FLOW_DIRECTION","timestamp":"1111-11-11T11:11:11Z"
-}'
+ json = '{"_":"telegram","media":"heat","driver":"kamheat","name":"303_wrong_flow","id":"83076912","forward_energy_m3c":0,"return_energy_m3c":0,"t1_temperature_c":61.82,"t2_temperature_c":56.72,"target_date":"2025-11-01","target_energy_kwh":0,"target_volume_m3":0,"total_energy_consumption_kwh":0,"total_volume_m3":0,"volume_flow_m3h":0,"meter_date":"2025-11-25","status":"WRONG_FLOW_DIRECTION","timestamp":"1111-11-11T11:11:11Z"
+ }'
+ }
+ test {
+ args = '303_heat_cool kamheat 83010647 NOKEY'
+ telegram = 5e442d2c47060183400d7a160050252f2f0406772e000004863c8c01000004149585020004ff0797d9000004ff08B6B200000259190a025d270a023B000002ff220000026c4a374406772e000044863c6d0100004414de810200426c41372f
+ json = '{"_":"telegram","media":"heat/cooling load","driver":"kamheat","name":"","id":"83010647","forward_energy_m3c":55703,"return_energy_m3c":45750,"t1_temperature_c":25.85,"t2_temperature_c":25.99,"target_date":"2026-07-01","target_energy_kwh":11895,"target_energy_backward_kwh":365,"target_volume_m3":1643.18,"total_energy_backward_kwh":396,"total_energy_consumption_kwh":11895,"total_volume_m3":1652.69,"volume_flow_m3h":0,"meter_date":"2026-07-10","status":"OK","timestamp":"2026-07-11T10:50:30Z"}'
}
}
diff --git a/components/wmbus_common/drivers/src/kampress.xmq b/components/wmbus_common/drivers/src/kampress.xmq
index d202b6af..737e787b 100644
--- a/components/wmbus_common/drivers/src/kampress.xmq
+++ b/components/wmbus_common/drivers/src/kampress.xmq
@@ -6,6 +6,9 @@ driver {
detect {
mvt = KAM,01,18
}
+ compact_frame_formats {
+ difvif = 22691269026905FF0905FF0A02FD17
+ }
fields {
field {
name = status
@@ -106,24 +109,18 @@ driver {
}
tests {
test {
+ comment = 'Long Frame'
args = 'Pressing kampress 77000317 NOKEY'
telegram = 32442D2C1703007701188D280080E39322DB8F78_22696600126967000269660005FF091954A33A05FF0A99BD823A02FD170800
- json = '{"_":"telegram","media":"pressure","meter":"kampress","name":"Pressing","id":"77000317","status":"LOW","pressure_bar":1.02,"max_pressure_bar":1.03,"min_pressure_bar":1.02,"alfa_counter":0.001246,"beta_counter":0.000997,"timestamp":"1111-11-11T11:11:11Z"}'
- fields = 'Pressing;77000317;LOW;1.02;1.03;1.02;1111-11-11 11:11.11'
- }
- test {
- args = 'Pressing kampress 77000317 NOKEY'
- telegram = '32442D2C1703007701188D280080E39322DB8F78_22696600126967000269660005FF091954A33A05FF0A99BD823A02FD170800
- 27442D2C1703007701188D280194E393226EC679DE735657_660067006600962B913A21B9423A0800'
- json = '{"_":"telegram","media":"pressure","meter":"kampress","name":"Pressing","id":"77000317","status":"LOW","pressure_bar":1.02,"max_pressure_bar":1.03,"min_pressure_bar":1.02,"alfa_counter":0.001108,"beta_counter":0.000743,"timestamp":"1111-11-11T11:11:11Z"}'
+ json = '{"_":"telegram","media":"pressure","driver":"kampress","name":"Pressing","id":"77000317","status":"LOW","pressure_bar":1.02,"max_pressure_bar":1.03,"min_pressure_bar":1.02,"alfa_counter":0.001246,"beta_counter":0.000997,"timestamp":"1111-11-11T11:11:11Z"}'
fields = 'Pressing;77000317;LOW;1.02;1.03;1.02;1111-11-11 11:11.11'
}
test {
+ comment = 'Compact Frame'
args = 'Pressing kampress 77000317 NOKEY'
- telegram = '32442D2C1703007701188D280080E39322DB8F78_22696600126967000269660005FF091954A33A05FF0A99BD823A02FD170800
- 27442D2C1703007701188D289554F295224ED579DE73188A_650066006600E80EA43A6B97A3BA0800'
- json = '{"_":"telegram","media":"pressure","meter":"kampress","name":"Pressing","id":"77000317","status":"LOW","pressure_bar":1.02,"max_pressure_bar":1.02,"min_pressure_bar":1.01,"alfa_counter":0.001252,"beta_counter":-0.001248,"timestamp":"1111-11-11T11:11:11Z"}'
- fields = 'Pressing;77000317;LOW;1.02;1.02;1.01;1111-11-11 11:11.11'
+ telegram = 27442D2C1703007701188D28FE48E39322E8F179DE735DD3660066006600E007883AA7F738BA0800
+ json = '{"_":"telegram","media":"pressure","driver":"kampress","name":"Pressing","id":"77000317","status":"LOW","pressure_bar":1.02,"max_pressure_bar":1.02,"min_pressure_bar":1.02,"alfa_counter":0.001038,"beta_counter":-0.000706,"timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'Pressing;77000317;LOW;1.02;1.02;1.02;1111-11-11 11:11.11'
}
}
}
diff --git a/components/wmbus_common/drivers/src/kamwater.xmq b/components/wmbus_common/drivers/src/kamwater.xmq
new file mode 100644
index 00000000..28111122
--- /dev/null
+++ b/components/wmbus_common/drivers/src/kamwater.xmq
@@ -0,0 +1,803 @@
+// Copyright (C) 2017-2026 Fredrik Öhrström (gpl-3.0-or-later)
+driver {
+ name = kamwater
+ aliases = multical21,flowiq2200
+ meter_type = WaterMeter
+ default_fields = name,id,status,total_m3,target_m3,timestamp
+ manufacturer = Kamstrup
+ detect {
+ // multical21
+ mvt = KAM,1b,06
+ // multical21
+ mvt = KAM,1b,16
+ // flowiq2200
+ mvt = KAW,3a,16
+ // flowiq2200
+ mvt = KAW,3c,16
+ mvt = KAW,3c,06
+ // flowiq2200
+ mvt = KAM,1d,16
+ // KWM2230 / 3230, warm
+ mvt = KAW,42,06
+ // KWM2230 / 3230, cold
+ mvt = KAW,42,16
+ }
+ compact_frame_formats {
+ difvif = 02FF2004134413615B6167
+ difvif = 02FF20041392013BA1015B8101E7FF0F
+ difvif = 04FF234413523B06FF1B426C61675167023B04138101E7FF0F
+ difvif = 02FF2004134413426C523B615B616751678101E7FF0F06FF1B
+ // Unknown if KamHeat or KamWater
+ difvif = 02FF2004134413A1015B8101E7FF0F
+ difvif = 02FF2004134413615B5167
+ difvif = 02FF2004134413
+ difvif = 02FF200413523B
+ // Kamstrup KWM2231
+ difvif = 04FF2304134413426C023B92013BA2013B06FF1BA1015B91015BA10167
+ }
+ library {
+ use = total_m3
+ use = target_m3
+ use = target_date
+ }
+ flags {
+ flag {
+ name = BURST
+ info = 'The flow of water has been consistently high for half an hour, which indicates a pipe burst.'
+ }
+ flag {
+ name = DRY
+ info = 'The is no water in the meter. Nothing will be measured.'
+ }
+ flag {
+ name = LEAK
+ info = 'The flow of water has not stopped for at least one continuous hour during the last 24 hours.'
+ }
+ flag {
+ name = REVERSE
+ info = 'The water flows through the meter in the wrong direction.'
+ }
+ flag {
+ name = TAMPER
+ info = 'Attempt of fraud. The meter is no longer valid for billing.'
+ }
+ flag {
+ name = LOW_BATTERY
+ info = 'The battery is running low. Less than 6 months remaining.'
+ }
+ flag {
+ name = FLOW_ABOVE_Q4
+ info = 'The water flow has exceeded the upper flow limit Q4 which can cause too high error rate.'
+ }
+ flag {
+ name = NO_CONSUMPTION
+ info = 'No flow of water has occurred for the last 15 days.'
+ }
+ flag {
+ name = LOW_AMBIENT_TEMPERATURE
+ info = 'The ambient temperature is below the configured lower temperature limit.'
+ }
+ flag {
+ name = HIGH_AMBIENT_TEMPERATURE
+ info = 'The ambient temperature is above the configured upper temperature limit.'
+ }
+ }
+ templates {
+ template_field {
+ name = duration_bucket
+ lookup {
+ name = DURATION
+ map_type = IndexToString
+ mask_bits = 0x7
+ map {
+ name = ''
+ value = 0x0
+ test = Set
+ }
+ map {
+ name = '1-8 hours'
+ value = 0x1
+ test = Set
+ }
+ map {
+ name = '9-24 hours'
+ value = 0x2
+ test = Set
+ }
+ map {
+ name = '2-3 days'
+ value = 0x3
+ test = Set
+ }
+ map {
+ name = '4-7 days'
+ value = 0x4
+ test = Set
+ }
+ map {
+ name = '8-14 days'
+ value = 0x5
+ test = Set
+ }
+ map {
+ name = '15-21 days'
+ value = 0x6
+ test = Set
+ }
+ map {
+ name = '22-31 days'
+ value = 0x7
+ test = Set
+ }
+ }
+ }
+ template_field {
+ // Common status fields for modern status fields:
+ // - 04FF23 Info Cold environment
+ // - 04FF24 Info Warm environment
+ // The datasheet also mentions 16bit fields (02FF25, 12FF25), but they are
+ // only partially compatible and we have no recursive inheritance.
+ name = x2xx_status
+ lookup {
+ name = ERROR_FLAGS
+ map_type = BitToString
+ // In these status fields, buts 0-10 are the instant info (with
+ // bits 8 and reserved, and bit 6's different meaning between
+ // meters configured for hot/cold *environment*). Bits 11-31
+ // are for time indication. But since bits 29-31 are reserved,
+ // let's roll them back into the "status" part and not "time
+ // indication" part. That way, all parts of the 32bit field are
+ // now covered.
+ mask_bits = 0xe00003ff
+ default_message = OK
+ map {
+ name = DRY
+ bit = 0
+ test = Set
+ }
+ map {
+ name = REVERSE
+ bit = 1
+ test = Set
+ }
+ map {
+ name = LEAK
+ bit = 2
+ test = Set
+ }
+ map {
+ name = BURST
+ bit = 3
+ test = Set
+ }
+ map {
+ name = TAMPER
+ bit = 4
+ test = Set
+ }
+ map {
+ name = LOW_BATTERY
+ bit = 5
+ test = Set
+ }
+ // bit 6 is different and will be filed in later
+ map {
+ name = FLOW_ABOVE_Q4
+ bit = 7
+ test = Set
+ }
+ // bit 8 is unused
+ map {
+ name = NO_CONSUMPTION
+ bit = 9
+ test = Set
+ }
+ // bit 10 is unused
+ }
+ }
+ }
+ fields {
+ field {
+ // A fake field which never matches, but propagates the TPL error bits.
+ // This is required, because the *driver* cannot really define multiple
+ // fields with INCLUDE_TPL_STATUS. If there was more than one such field,
+ // the matchers would accidentally overwrite their own values. So, create
+ // this virtual one, and let the "real" fields (of which there are multiple
+ // alternatives in the driver, and only one of them will match in a real
+ // packet) propagate thir stuff.
+ name = status
+ quantity = Text
+ attributes = STATUS,INCLUDE_TPL_STATUS
+ }
+ field {
+ name = status_cold_env
+ quantity = Text
+ template = x2xx_status
+ info = 'Status and error flags.'
+ attributes = INJECT_INTO_STATUS,HIDE
+ match {
+ // "Info Cold environment"
+ difvifkey = 04FF23
+ }
+ lookup {
+ name = ERROR_FLAGS
+ map_type = BitToString
+ default_message = OK
+ mark_reserved_bits = true
+ map {
+ name = LOW_AMBIENT_TEMPERATURE
+ bit = 6
+ test = Set
+ }
+ }
+ }
+ field {
+ name = status_hot_env
+ quantity = Text
+ template = x2xx_status
+ info = 'Status and error flags.'
+ attributes = INJECT_INTO_STATUS,HIDE
+ match {
+ // "Info Hot environment"
+ difvifkey = 04FF24
+ }
+ lookup {
+ name = ERROR_FLAGS
+ map_type = BitToString
+ default_message = OK
+ mark_reserved_bits = true
+ map {
+ name = HIGH_AMBIENT_TEMPERATURE
+ bit = 6
+ test = Set
+ }
+ }
+ }
+ field {
+ name = status16
+ quantity = Text
+ info = 'Status and error flags.'
+ attributes = INJECT_INTO_STATUS,HIDE
+ match {
+ difvifkey = 02FF20
+ }
+ lookup {
+ name = ERROR_FLAGS
+ map_type = BitToString
+ mask_bits = 0x000f
+ default_message = OK
+ map {
+ name = DRY
+ bit = 0
+ test = Set
+ }
+ map {
+ name = REVERSE
+ bit = 1
+ test = Set
+ }
+ map {
+ name = LEAK
+ bit = 2
+ test = Set
+ }
+ map {
+ name = BURST
+ bit = 3
+ test = Set
+ }
+ }
+ }
+ field {
+ name = current_status
+ quantity = Text
+ info = 'Status of meter. This field will go away use status instead.'
+ deprecated_by = 'status, update before 2026-12-01'
+ match {
+ difvifkey = 02FF20
+ }
+ lookup {
+ name = ERROR_FLAGS
+ map_type = BitToString
+ mask_bits = 0x000f
+ map {
+ name = DRY
+ bit = 0
+ test = Set
+ }
+ map {
+ name = REVERSE
+ bit = 1
+ test = Set
+ }
+ map {
+ name = LEAK
+ bit = 2
+ test = Set
+ }
+ map {
+ name = BURST
+ bit = 3
+ test = Set
+ }
+ }
+ }
+ field {
+ name = flow
+ quantity = Flow
+ info = 'The current flow of water through the meter.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = VolumeFlow
+ }
+ }
+ field {
+ name = min_flow_last_month
+ quantity = Flow
+ info = 'The minimum flow recorded during the last month.'
+ match {
+ measurement_type = Minimum
+ vif_range = VolumeFlow
+ storage_nr = 1
+ }
+ }
+ field {
+ name = min_flow_last_day
+ quantity = Flow
+ info = 'The minimum flow recorded during the last day.'
+ match {
+ measurement_type = Minimum
+ vif_range = VolumeFlow
+ storage_nr = 2
+ }
+ }
+ // there's no `min_flow_last_year` in the docs
+ field {
+ name = max_flow_last_month
+ quantity = Flow
+ info = 'The maximum flow recorded during the last month.'
+ match {
+ measurement_type = Maximum
+ vif_range = VolumeFlow
+ storage_nr = 1
+ }
+ }
+ field {
+ name = max_flow_last_day
+ quantity = Flow
+ info = 'The maximum flow recorded during the last day.'
+ match {
+ measurement_type = Maximum
+ vif_range = VolumeFlow
+ storage_nr = 2
+ }
+ }
+ field {
+ name = max_flow_last_year
+ quantity = Flow
+ info = 'The maximum flow recorded during the last year.'
+ match {
+ measurement_type = Maximum
+ vif_range = VolumeFlow
+ storage_nr = 3
+ }
+ }
+ field {
+ name = min_flow_temperature_last_month
+ quantity = Temperature
+ info = 'The lowest water temperature during the last month.'
+ match {
+ measurement_type = Minimum
+ vif_range = FlowTemperature
+ storage_nr = 1
+ }
+ }
+ field {
+ name = min_flow_temperature_last_day
+ quantity = Temperature
+ info = 'The lowest water temperature during the last day.'
+ match {
+ measurement_type = Minimum
+ vif_range = FlowTemperature
+ storage_nr = 2
+ }
+ }
+ field {
+ name = min_flow_temperature_last_year
+ quantity = Temperature
+ info = 'The lowest water temperature during the last year.'
+ match {
+ measurement_type = Minimum
+ vif_range = FlowTemperature
+ storage_nr = 3
+ }
+ }
+ field {
+ name = min_external_temperature_last_month
+ quantity = Temperature
+ info = 'The lowest external temperature outside of the meter during the last month.'
+ match {
+ measurement_type = Minimum
+ vif_range = ExternalTemperature
+ storage_nr = 1
+ }
+ }
+ field {
+ name = min_external_temperature_last_day
+ quantity = Temperature
+ info = 'The lowest external temperature outside of the meter during the last day.'
+ match {
+ measurement_type = Minimum
+ vif_range = ExternalTemperature
+ storage_nr = 2
+ }
+ }
+ field {
+ name = min_external_temperature_last_year
+ quantity = Temperature
+ info = 'The lowest external temperature outside of the meter during the last year.'
+ match {
+ measurement_type = Minimum
+ vif_range = ExternalTemperature
+ storage_nr = 3
+ }
+ }
+ field {
+ name = max_flow_temperature_last_month
+ quantity = Temperature
+ info = 'The maximum water temperature during the last month.'
+ match {
+ measurement_type = Maximum
+ vif_range = FlowTemperature
+ storage_nr = 1
+ }
+ }
+ field {
+ // Kamstrup docs don't show this field explicitly, but hey, the values are supposed to be examples,
+ // and the test suite actually checks for it explicitly
+ name = max_flow_temperature_last_day
+ quantity = Temperature
+ info = 'The maximum water temperature during the last day.'
+ match {
+ measurement_type = Maximum
+ vif_range = FlowTemperature
+ storage_nr = 2
+ }
+ }
+ field {
+ name = max_flow_temperature_last_year
+ quantity = Temperature
+ info = 'The maximum water temperature during the last year.'
+ match {
+ measurement_type = Maximum
+ vif_range = FlowTemperature
+ storage_nr = 3
+ }
+ }
+ field {
+ name = max_external_temperature_last_month
+ quantity = Temperature
+ info = 'The maximum temperature recorded during previous month.'
+ match {
+ measurement_type = Maximum
+ vif_range = ExternalTemperature
+ storage_nr = 1
+ }
+ }
+ field {
+ // Kamstrup docs don't show this field explicitly, but hey, the values are supposed to be examples, so let's include it
+ name = max_external_temperature_last_day
+ quantity = Temperature
+ info = 'The maximum temperature recorded during previous day.'
+ match {
+ measurement_type = Maximum
+ vif_range = ExternalTemperature
+ storage_nr = 2
+ }
+ }
+ field {
+ name = max_external_temperature_last_year
+ quantity = Temperature
+ info = 'The maximum temperature recorded during previous year.'
+ match {
+ measurement_type = Maximum
+ vif_range = ExternalTemperature
+ storage_nr = 3
+ }
+ }
+ field {
+ name = time_dry
+ quantity = Text
+ template = duration_bucket
+ info = 'Amount of time the meter has been dry.'
+ match {
+ difvifkey = 02FF20
+ }
+ lookup {
+ pre_shift_right = 4
+ }
+ }
+ field {
+ name = time_dry
+ quantity = Text
+ template = duration_bucket
+ info = 'Amount of time the meter has been dry.'
+ match {
+ difvifkey = 04FF23
+ }
+ lookup {
+ pre_shift_right = 11
+ }
+ }
+ field {
+ name = time_dry
+ quantity = Text
+ template = duration_bucket
+ info = 'Amount of time the meter has been dry.'
+ match {
+ difvifkey = 04FF24
+ }
+ lookup {
+ pre_shift_right = 11
+ }
+ }
+ field {
+ name = time_reversed
+ quantity = Text
+ template = duration_bucket
+ info = 'Amount of time the meter has been reversed.'
+ match {
+ difvifkey = 02FF20
+ }
+ lookup {
+ pre_shift_right = 7
+ }
+ }
+ field {
+ name = time_reversed
+ quantity = Text
+ template = duration_bucket
+ info = 'Amount of time the meter has been reversed.'
+ match {
+ difvifkey = 04FF23
+ }
+ lookup {
+ pre_shift_right = 14
+ }
+ }
+ field {
+ name = time_reversed
+ quantity = Text
+ template = duration_bucket
+ info = 'Amount of time the meter has been reversed.'
+ match {
+ difvifkey = 04FF24
+ }
+ lookup {
+ pre_shift_right = 14
+ }
+ }
+ field {
+ name = time_leaking
+ quantity = Text
+ template = duration_bucket
+ info = 'Amount of time the meter has been leaking.'
+ match {
+ difvifkey = 02FF20
+ }
+ lookup {
+ pre_shift_right = 10
+ }
+ }
+ field {
+ name = time_leaking
+ quantity = Text
+ template = duration_bucket
+ info = 'Amount of time the meter has been leaking.'
+ match {
+ difvifkey = 04FF23
+ }
+ lookup {
+ pre_shift_right = 17
+ }
+ }
+ field {
+ name = time_leaking
+ quantity = Text
+ template = duration_bucket
+ info = 'Amount of time the meter has been leaking.'
+ match {
+ difvifkey = 04FF24
+ }
+ lookup {
+ pre_shift_right = 17
+ }
+ }
+ field {
+ name = time_bursting
+ quantity = Text
+ template = duration_bucket
+ info = 'Amount of time the meter has been bursting.'
+ match {
+ difvifkey = 02FF20
+ }
+ lookup {
+ pre_shift_right = 13
+ }
+ }
+ field {
+ name = time_bursting
+ quantity = Text
+ template = duration_bucket
+ info = 'Amount of time the meter has been bursting.'
+ match {
+ difvifkey = 04FF23
+ }
+ lookup {
+ pre_shift_right = 20
+ }
+ }
+ field {
+ name = time_bursting
+ quantity = Text
+ template = duration_bucket
+ info = 'Amount of time the meter has been bursting.'
+ match {
+ difvifkey = 04FF24
+ }
+ lookup {
+ pre_shift_right = 20
+ }
+ }
+ field {
+ name = time_low_ambient_temperature
+ quantity = Text
+ template = duration_bucket
+ info = 'Amount of time the ambient temperature was too low.'
+ match {
+ difvifkey = 04FF23
+ }
+ lookup {
+ pre_shift_right = 23
+ }
+ }
+ field {
+ name = time_high_ambient_temperature
+ quantity = Text
+ template = duration_bucket
+ info = 'Amount of time the ambient temperature was too high.'
+ match {
+ difvifkey = 04FF24
+ }
+ lookup {
+ pre_shift_right = 23
+ }
+ }
+ field {
+ name = time_flow_above_q4
+ quantity = Text
+ template = duration_bucket
+ info = 'Amount of time the flow was above the Q_4 threshold.'
+ match {
+ difvifkey = 04FF23
+ }
+ lookup {
+ pre_shift_right = 26
+ }
+ }
+ field {
+ name = time_flow_above_q4
+ quantity = Text
+ template = duration_bucket
+ info = 'Amount of time the flow was above the Q_4 threshold.'
+ match {
+ difvifkey = 04FF24
+ }
+ lookup {
+ pre_shift_right = 26
+ }
+ }
+ field {
+ /* The internal structure of this field is not known, it's likely not "just a number", so
+ let's treat it as a magic blob, do not encode it as a number, and don't indicate any units. */
+ name = accoustic_noise
+ quantity = Text
+ info = 'Acoustic noise'
+ match {
+ difvifkey = 06FF1B
+ }
+ }
+ }
+ tests {
+ test {
+ args = 'MyTapWater kamwater 76348799 NOKEY'
+ telegram = 2A442D2C998734761B168D2091D37CAC21576C78_02FF207100041308190000441308190000615B7F616713
+ json = '{"_":"telegram","media":"cold water","driver":"kamwater","name":"MyTapWater","id":"76348799","min_external_temperature_last_month_c":19,"min_flow_temperature_last_month_c":127,"target_m3":6.408,"total_m3":6.408,"current_status":"DRY","current_status_deprecated_by": "status, update before 2026-12-01","status":"DRY","time_bursting":"","time_dry":"22-31 days","time_leaking":"","time_reversed":"","timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'MyTapWater;76348799;DRY;6.408;6.408;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'MyTapWater kamwater 76348799 NOKEY'
+ telegram = 23442D2C998734761B168D2087D19EAD217F1779EDA86AB6_710008190000081900007F13
+ json = '{"_":"telegram","media":"cold water","driver":"kamwater","name":"MyTapWater","id":"76348799","min_external_temperature_last_month_c":19,"min_flow_temperature_last_month_c":127,"target_m3":6.408,"total_m3":6.408,"current_status":"DRY","current_status_deprecated_by": "status, update before 2026-12-01","status":"DRY","time_bursting":"","time_dry":"22-31 days","time_leaking":"","time_reversed":"","timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'MyTapWater;76348799;DRY;6.408;6.408;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'Vadden kamwater 44556677 NOKEY'
+ telegram = 2D442D2C776655441B168D2083B48D3A20_46887802FF20000004132F4E000092013B3D01A1015B028101E7FF0F03
+ json = '{"_":"telegram","media":"cold water","driver":"kamwater","name":"Vadden","id":"44556677","max_flow_last_day_m3h":0.317,"min_flow_temperature_last_day_c":2,"total_m3":20.015,"current_status":"","current_status_deprecated_by": "status, update before 2026-12-01","status":"OK","time_bursting":"","time_dry":"","time_leaking":"","time_reversed":"","timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'Vadden;44556677;OK;20.015;null;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'Vadden kamwater 44556677 NOKEY'
+ telegram = 21442D2C776655441B168D2079CC8C3A20_F4307912C40DFF00002F4E00003D010203
+ json = '{"_":"telegram","media":"cold water","driver":"kamwater","name":"Vadden","id":"44556677","max_flow_last_day_m3h":0.317,"min_flow_temperature_last_day_c":2,"total_m3":20.015,"current_status":"","current_status_deprecated_by": "status, update before 2026-12-01","status":"OK","time_bursting":"","time_dry":"","time_leaking":"","time_reversed":"","timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'Vadden;44556677;OK;20.015;null;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'VATTEN kamwater 52525252 NOKEY'
+ telegram = 4D44372C525252523A168D203894DF7920F93278_04FF23000000000413AEAC0000441364A80000426C812A023B000092013BEF01A2013B000006FF1B067000097000A1015B0C91015B14A1016713
+ json = '{"_":"telegram","media":"cold water","driver":"kamwater","name":"VATTEN","id":"52525252","flow_m3h":0,"max_flow_last_day_m3h":0.495,"max_flow_temperature_last_day_c":20,"min_external_temperature_last_day_c":19,"min_flow_last_day_m3h":0,"min_flow_temperature_last_day_c":12,"target_m3":43.108,"target_date":"2020-10-01","time_bursting":"","time_dry":"","time_flow_above_q4":"","time_leaking":"","time_low_ambient_temperature":"","time_reversed":"","total_m3":44.206,"accoustic_noise":"067000097000","status":"OK","timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'VATTEN;52525252;OK;44.206;43.108;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'Votten kamwater 23813076 NOKEY'
+ telegram = 4744372C763081233C168D2056D1ED11205A3C78_04FF2300080000441300000000523B000006FF1B08900008F0FF426CC12B61670A51671B023B000004131F0F00008101E7FF0F13
+ json = '{"_":"telegram","media":"cold water","driver":"kamwater","name":"Votten","id":"23813076","flow_m3h":0,"max_external_temperature_last_month_c":27,"target_m3":0,"max_flow_last_month_m3h": 0,"target_date":"2022-11-01","total_m3":3.871,"accoustic_noise":"08900008F0FF","min_external_temperature_last_month_c":10,"status":"OK","time_bursting":"","time_dry":"1-8 hours","time_flow_above_q4":"","time_leaking":"","time_low_ambient_temperature":"","time_reversed":"","timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'Votten;23813076;OK;3.871;0;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'Votten kamwater 23813076 NOKEY'
+ telegram = 3244372C763081233C168D2057D2ED11205a817905095480_0008000000000000000008900008F0FFC12B0A1B23001F0F000013
+ json = '{"_":"telegram","media":"cold water","driver":"kamwater","name":"Votten","id":"23813076","min_external_temperature_last_month_c":10,"flow_m3h":0.035,"max_external_temperature_last_month_c":27,"max_flow_last_month_m3h": 0,"target_m3":0,"target_date":"2022-11-01","total_m3":3.871,"accoustic_noise":"08900008F0FF","status":"OK","time_bursting":"","time_dry":"1-8 hours","time_flow_above_q4":"","time_leaking":"","time_low_ambient_temperature":"","time_reversed":"","timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'Votten;23813076;OK;3.871;0;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'MoreWater kamwater 54084283 NOKEY'
+ telegram = 2f44372c834208543c168d209e926f9820903279d4364649000062ed000034c9000041373303110f1716077000077000
+ json = '{"_": "telegram","current_status": "","current_status_deprecated_by": "status, update before 2026-12-01","accoustic_noise": "077000077000","min_flow_temperature_last_month_c": 17,"id": "54084283","max_flow_last_month_m3h": 0.819,"max_external_temperature_last_month_c": 23,"media": "cold water","driver": "kamwater","min_external_temperature_last_month_c": 15,"name": "MoreWater","status": "OK","target_date": "2026-07-01","target_m3": 51.508,"time_bursting": "","time_dry": "","time_leaking": "","time_reversed": "","timestamp": "1111-11-11T11:11:11Z","total_m3": 60.77}'
+ fields = 'MoreWater;54084283;OK;60.77;51.508;1111-11-11 11:11.11'
+ }
+ test {
+ comment = 'Security mode 10 (OMS security profile D), AES-CCM with message counter. Radio corrupted telegram, the aes-ccm tag does not verify, the data is decoded anyway and marked FAILED_DECODE.'
+ args = 'MoreDWater kamwater 53820306 BF902DD6DFADAFE83E4AE6832B5FA14C'
+ telegram = 4544372c060382533c16_8c20307a3010ff2a1001dc2e0d00cf1a85672e138d6c0af61338403fdeb528ab264e2c61e41b06073550aa296ae9815c8a7a9868ec6c624cfbbcecb3
+ json = '{"_":"telegram","accoustic_noise":"FFFFFFFFFFFF","driver":"kamwater","id":"53820306","media":"cold water","name":"MoreDWater","status":"TEMPORARY_ERROR FAILED_DECODE","target_m3":0,"timestamp":"1111-11-11T11:11:11Z","total_m3":0}'
+ fields = 'MoreDWater;53820306;TEMPORARY_ERROR FAILED_DECODE;0;0;1111-11-11 11:11.11'
+ }
+ test {
+ comment = 'Security mode 10 (OMS security profile D), AES-CCM with message counter, valid tag.'
+ args = 'MoreDWater kamwater 53820306 BF902DD6DFADAFE83E4AE6832B5FA14C'
+ telegram = 4544372c060382533c16_8c20307a3010ff2a10018c570e006da7632471f42b0a3ceccd461353573ed02b87118bc5ffe982f7f00a0274543bc51938f8b9c8bb8928114d60788a
+ json = '{"_":"telegram","accoustic_noise":"FFFFFFFFFFFF","driver":"kamwater","id":"53820306","media":"cold water","name":"MoreDWater","status":"TEMPORARY_ERROR","target_m3":0,"timestamp":"1111-11-11T11:11:11Z","total_m3":0}'
+ fields = 'MoreDWater;53820306;TEMPORARY_ERROR;0;0;1111-11-11 11:11.11'
+ }
+ test {
+ comment = 'synthetic: cold env, dry, low ambient with some duration'
+ args = 'synth-cold-dry-low kamwater 52525252 NOKEY'
+ telegram = 2044372C525252523A168D203894DF7920_1b42_78_04FF2301000001
+ json = '{"_":"telegram","media":"cold water","driver":"kamwater","id":"52525252","name":"synth-cold-dry-low","status":"DRY","time_bursting":"","time_dry":"","time_flow_above_q4":"","time_leaking":"","time_low_ambient_temperature":"9-24 hours","time_reversed":"","timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'synth-cold-dry-low;52525252;DRY;null;null;1111-11-11 11:11.11'
+ }
+ test {
+ comment = 'synthetic: hot env, dry, high ambient with some duration'
+ args = 'synth-hot-dry-high kamwater 52525252 NOKEY'
+ telegram = 2044372C525252523A168D203894DF7920_898b_78_04FF2401000001
+ json = '{"_":"telegram","media":"cold water","driver":"kamwater","id":"52525252","name":"synth-hot-dry-high","status":"DRY","time_bursting":"","time_dry":"","time_flow_above_q4":"","time_leaking":"","time_high_ambient_temperature":"9-24 hours","time_reversed":"","timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'synth-hot-dry-high;52525252;DRY;null;null;1111-11-11 11:11.11'
+ }
+ test {
+ comment = 'syntetic: error propagation from TPL with no status'
+ args = 'synth-tpl-no-status kamwater 52525252 NOKEY'
+ telegram = 1944372C525252523A168D203894DF7920_5053_7a44020000_441301000000
+ json = '{"_":"telegram","media":"cold water","driver":"kamwater","id":"52525252","name":"synth-tpl-no-status","status":"ERROR","target_m3":0.001,"timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'synth-tpl-no-status;52525252;ERROR;null;0.001;1111-11-11 11:11.11'
+ }
+ test {
+ comment = 'syntetic: error propagation from TPL with different error in status'
+ args = 'synth-tpl-dry kamwater 52525252 NOKEY'
+ telegram = 1944372C525252523A168D203894DF7920_4bf4_7a44020000_04FF2301000000
+ json = '{"_":"telegram","media":"cold water","driver":"kamwater","id":"52525252","name":"synth-tpl-dry","status": "DRY ERROR","time_bursting":"","time_dry":"","time_flow_above_q4":"","time_leaking":"","time_low_ambient_temperature":"","time_reversed":"","timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'synth-tpl-dry;52525252;DRY ERROR;null;null;1111-11-11 11:11.11'
+ }
+ }
+}
diff --git a/components/wmbus_common/drivers/src/lansendw.xmq b/components/wmbus_common/drivers/src/lansendw.xmq
new file mode 100644
index 00000000..bdc9de04
--- /dev/null
+++ b/components/wmbus_common/drivers/src/lansendw.xmq
@@ -0,0 +1,84 @@
+// Copyright (C) 2020-2022 Fredrik Öhrström (gpl-3.0-or-later)
+driver {
+ name = lansendw
+ meter_type = DoorWindowDetector
+ default_fields = name,id,status,timestamp
+ detect {
+ mvt = LAS,07,1d
+ }
+ fields {
+ field {
+ name = status
+ quantity = Text
+ info = 'The state (OPEN/CLOSED) for the door/window.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = DigitalInput
+ }
+ lookup {
+ name = INPUT_FLAGS
+ map_type = IndexToString
+ mask_bits = 0xffff
+ map {
+ name = CLOSED
+ value = 0x11
+ test = Set
+ }
+ map {
+ name = OPEN
+ value = 0x55
+ test = Set
+ }
+ }
+ }
+ field {
+ name = error_flags
+ quantity = Text
+ info = 'Error flags.'
+ attributes = STATUS,INCLUDE_TPL_STATUS
+ match {
+ measurement_type = Instantaneous
+ vif_range = ErrorFlags
+ }
+ lookup {
+ name = ERROR_FLAGS
+ map_type = BitToString
+ mask_bits = 0xffff
+ default_message = OK
+ }
+ }
+ field {
+ name = a
+ quantity = Dimensionless
+ info = 'How many times the door/window has been opened or closed.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = Dimensionless
+ }
+ }
+ field {
+ name = b
+ quantity = Dimensionless
+ info = 'The current number of counted pulses from counter b.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = Dimensionless
+ subunit_nr = 1
+ }
+ }
+ }
+ tests {
+ test {
+ args = 'Dooro lansendw 00010205 NOKEY'
+ telegram = 2e44333005020100071d7ab54800002f2f02fd1b110002fd971d01000efd3a2200000000008e40fd3a000000000000
+ json = '{"_":"telegram","media":"reserved","driver":"lansendw","name":"Dooro","id":"00010205","status":"CLOSED","a_counter":22,"b_counter":0,"error_flags":"ERROR_FLAGS_1 PERMANENT_ERROR UNKNOWN_40","timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'Dooro;00010205;CLOSED;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'Dooro lansendw 00010205 NOKEY'
+ telegram = 2e44333005020100071d7ab66800002f2f02fd1b550002fd971d01000efd3a2300000000008e40fd3a000000000000
+ json = '{"_":"telegram","media":"reserved","driver":"lansendw","name":"Dooro","id":"00010205","status":"OPEN","a_counter":23,"b_counter":0,"error_flags":"ERROR_FLAGS_1 PERMANENT_ERROR UNKNOWN_60","timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'Dooro;00010205;OPEN;1111-11-11 11:11.11'
+ }
+ }
+}
diff --git a/components/wmbus_common/drivers/src/lansenpu.xmq b/components/wmbus_common/drivers/src/lansenpu.xmq
new file mode 100644
index 00000000..a8e34102
--- /dev/null
+++ b/components/wmbus_common/drivers/src/lansenpu.xmq
@@ -0,0 +1,69 @@
+// Copyright (C) 2020-2022 Fredrik Öhrström (gpl-3.0-or-later)
+driver {
+ name = lansenpu
+ meter_type = PulseCounter
+ default_fields = name,id,status,a_counter,b_counter,timestamp
+ detect {
+ mvt = LAS,14,00
+ mvt = LAS,1b,00
+ mvt = LAS,0b,02
+ }
+ mfct_tpl_status_bits {
+ mask_bits = 0xe0
+ default_message = OK
+ map {
+ name = SABOTAGE_ENCLOSURE
+ value = 0x40
+ test = set
+ }
+ }
+ fields {
+ field {
+ name = status
+ quantity = Text
+ info = 'Status and error flags.'
+ attributes = STATUS,INCLUDE_TPL_STATUS
+ }
+ /* Doubles have a 52 bit significand 11 bit exp and 1 bit sign,
+ so double is good for incremental pulses up to 2^52 counts
+ which is approx 4.5*10^15
+ The values sent by this meter are 12 digit bcd, ie at most 10^13-1 counts.
+ so they fit inside a double. */
+ field {
+ name = a
+ info = 'The current number of counted pulses from counter a.'
+ quantity = Dimensionless
+ match {
+ difvifkey = 0EFD3A
+ }
+ }
+ field {
+ name = b
+ info = 'The current number of counted pulses from counter b.'
+ quantity = Dimensionless
+ match {
+ difvifkey = 8E40FD3A
+ }
+ }
+ }
+ tests {
+ test {
+ args = 'COUNTA lansenpu 00010206 NOKEY'
+ telegram = 234433300602010014007a8e0400002f2f0efd3a1147000000008e40fd3a341200000000
+ json = '{"_":"telegram","media":"other","driver":"lansenpu","name":"COUNTA","id":"00010206","status":"POWER_LOW","a_counter":4711,"b_counter":1234,"timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'COUNTA;00010206;POWER_LOW;4711;1234;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'COUNTB lansenpu 00023750 NOKEY'
+ telegram = 1A443330503702000B027AD7000020|2F2F8E40FD3A700800000000
+ json = '{"_":"telegram","media":"electricity","driver":"lansenpu","name":"COUNTB","id":"00023750","status":"OK","b_counter":870,"timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'COUNTB;00023750;OK;null;870;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'COUNTB lansenpu 00023750 NOKEY'
+ telegram = 1A443330503702000B027AD74c0020|2F2F8E40FD3A700800000000
+ json = '{"_":"telegram","media":"electricity","driver":"lansenpu","name":"COUNTB","id":"00023750","status":"PERMANENT_ERROR POWER_LOW SABOTAGE_ENCLOSURE","b_counter":870,"timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'COUNTB;00023750;PERMANENT_ERROR POWER_LOW SABOTAGE_ENCLOSURE;null;870;1111-11-11 11:11.11'
+ }
+ }
+}
diff --git a/components/wmbus_common/drivers/src/lansenrp.xmq b/components/wmbus_common/drivers/src/lansenrp.xmq
new file mode 100644
index 00000000..c57d4ed0
--- /dev/null
+++ b/components/wmbus_common/drivers/src/lansenrp.xmq
@@ -0,0 +1,172 @@
+/* Copyright (C) 2020-2022 Fredrik Öhrström (gpl-3.0-or-later)
+
+ @see https://www.lansensystems.com/media/1282/mbus_data_format_lan-wmbus-r4_v11_rev_3.pdf */
+driver {
+ name = lansenrp
+ meter_type = Repeater
+ default_fields = name,id,status,total_routed_messages_counter,used_router_slots_counter,is_repeater_listening,timestamp
+ detect {
+ mvt = LAS,0b,32
+ }
+ library {
+ use = software_version
+ use = meter_datetime
+ use = battery_v
+ }
+ fields {
+ field {
+ name = status
+ quantity = Text
+ info = 'Meter status from tpl status field.'
+ attributes = STATUS,INCLUDE_TPL_STATUS
+ }
+ field {
+ name = total_routed_messages
+ quantity = Dimensionless
+ info = 'Number of total routed messages since power up'
+ match {
+ measurement_type = Instantaneous
+ vif_range = Dimensionless
+ }
+ }
+ field {
+ name = used_router_slots
+ quantity = Dimensionless
+ info = 'Used router slots (maximum 936)'
+ match {
+ measurement_type = Instantaneous
+ vif_range = Dimensionless
+ subunit_nr = 1
+ }
+ }
+ field {
+ name = is_repeater_listening
+ quantity = Text
+ info = 'Is the repeater listening (YES/NO)'
+ match {
+ measurement_type = Instantaneous
+ vif_range = Dimensionless
+ subunit_nr = 2
+ }
+ lookup {
+ name = INPUT_BITS
+ map_type = IndexToString
+ mask_bits = 0x01
+ map {
+ name = NO
+ value = 0x00
+ test = Set
+ }
+ map {
+ name = YES
+ value = 0x01
+ test = Set
+ }
+ }
+ }
+ field {
+ name = seconds_to_mode_change
+ quantity = Dimensionless
+ info = 'Seconds to mode change (Listen -> Sleep or Sleep -> Listen). Maximum 32767 seconds'
+ match {
+ measurement_type = Instantaneous
+ vif_range = Dimensionless
+ subunit_nr = 3
+ }
+ }
+ field {
+ name = listen_timer_value
+ quantity = Dimensionless
+ info = 'Value on parameter "Listen timer"'
+ match {
+ measurement_type = Instantaneous
+ vif_range = Dimensionless
+ storage_nr = 1
+ }
+ }
+ field {
+ name = pause_timer_value
+ quantity = Dimensionless
+ info = 'Value on parameter "Pause timer"'
+ match {
+ measurement_type = Instantaneous
+ vif_range = Dimensionless
+ storage_nr = 2
+ }
+ }
+ field {
+ name = start_time_value
+ quantity = Dimensionless
+ info = 'Value on parameter "Start time", shown as minutes after midnight (-1=Not used)'
+ match {
+ measurement_type = Instantaneous
+ vif_range = Dimensionless
+ storage_nr = 4
+ }
+ }
+ field {
+ name = repeater_listening_on_weekdays
+ quantity = Text
+ info = 'Shows which weekday(s) repeater is listening (MO/TU/WE/TH/FR/SA/SU)'
+ match {
+ measurement_type = Instantaneous
+ vif_range = Dimensionless
+ storage_nr = 3
+ }
+ lookup {
+ name = INPUT_BITS
+ map_type = BitToString
+ mask_bits = 0xffff
+ map {
+ name = SU
+ value = 0x01
+ test = Set
+ }
+ map {
+ name = MO
+ value = 0x02
+ test = Set
+ }
+ map {
+ name = TU
+ value = 0x04
+ test = Set
+ }
+ map {
+ name = WE
+ value = 0x08
+ test = Set
+ }
+ map {
+ name = TH
+ value = 0x10
+ test = Set
+ }
+ map {
+ name = FR
+ value = 0x20
+ test = Set
+ }
+ map {
+ name = SA
+ value = 0x40
+ test = Set
+ }
+ }
+ }
+ }
+ tests {
+ test {
+ args = 'REPEAT lansenrp 00035946 NOKEY'
+ telegram = 54443330465903000B327A2B0000402F2F04FD3A946709008240FD3A600002FD0F9500818040FD3A0084C040FD3A8838000042FD3A28008201FD3A8C05C101FD3A7F8202FD3A3804066D35122EFB2B0002FD46D00C
+ json = '{"_":"telegram","media":"reserved","driver":"lansenrp","name":"REPEAT","id":"00035946","battery_v":3.28,"listen_timer_value_counter":40,"pause_timer_value_counter":1420,"seconds_to_mode_change_counter":14472,"start_time_value_counter":1080,"total_routed_messages_counter":616340,"used_router_slots_counter":96,"is_repeater_listening":"NO","meter_datetime":"2023-11-27 14:18:53","repeater_listening_on_weekdays":"FR MO SA SU TH TU WE","software_version":"0095","status":"OK","timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'REPEAT;00035946;OK;616340;96;NO;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'REPEAT lansenrp 00035946 NOKEY'
+ telegram = 54443330465903000B327A2B0400402F2F04FD3A946709008240FD3A600002FD0F9500818040FD3A0184C040FD3A8838000042FD3A28008201FD3A8C05C101FD3A088202FD3A3804066D35122EFB2B0002FD46D00C
+ json = '{"_":"telegram","media":"reserved","driver":"lansenrp","name":"REPEAT","id":"00035946","battery_v":3.28,"listen_timer_value_counter":40,"pause_timer_value_counter":1420,"seconds_to_mode_change_counter":14472,"start_time_value_counter":1080,"total_routed_messages_counter":616340,"used_router_slots_counter":96,"is_repeater_listening":"YES","meter_datetime":"2023-11-27 14:18:53","repeater_listening_on_weekdays":"WE","software_version":"0095","status":"POWER_LOW","timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'REPEAT;00035946;POWER_LOW;616340;96;YES;1111-11-11 11:11.11'
+ }
+ }
+}
diff --git a/components/wmbus_common/drivers/src/lansensm.xmq b/components/wmbus_common/drivers/src/lansensm.xmq
new file mode 100644
index 00000000..b96a4b5e
--- /dev/null
+++ b/components/wmbus_common/drivers/src/lansensm.xmq
@@ -0,0 +1,92 @@
+// Copyright (C) 2020-2022 Fredrik Öhrström (gpl-3.0-or-later)
+driver {
+ name = lansensm
+ meter_type = SmokeDetector
+ default_fields = name,id,status,minutes_since_last_manual_test_counter,timestamp
+ detect {
+ mvt = LAS,03,1a
+ }
+ fields {
+ field {
+ name = status
+ quantity = Text
+ info = 'Meter status.'
+ attributes = STATUS,INCLUDE_TPL_STATUS
+ match {
+ measurement_type = Instantaneous
+ vif_range = ErrorFlags
+ add_combinable = StandardConformantDataContent
+ }
+ lookup {
+ name = ERROR_FLAGS
+ map_type = BitToString
+ mask_bits = 0xffff
+ default_message = OK
+ map {
+ name = LOW_BATTERY
+ value = 0x0002
+ test = Set
+ }
+ map {
+ name = SMOKE
+ value = 0x0004
+ test = Set
+ }
+ map {
+ name = MANUAL_TEST
+ value = 0x0008
+ test = Set
+ }
+ map {
+ name = MALFUNCTION
+ value = 0x0010
+ test = Set
+ }
+ map {
+ name = NO_CONNECTION_TO_SMOKE_DETECTOR
+ value = 0x0020
+ test = Set
+ }
+ map {
+ name = SMOKE_SENSOR_END_OF_LIFE
+ value = 0x0100
+ test = Set
+ }
+ }
+ }
+ field {
+ name = async_msg_id
+ quantity = Dimensionless
+ info = 'Unique asynchronous message number.'
+ vif_scaling = None
+ match {
+ measurement_type = Instantaneous
+ vif_range = AccessNumber
+ }
+ }
+ field {
+ name = minutes_since_last_manual_test
+ quantity = Dimensionless
+ info = 'Minutes since last manual test.'
+ vif_scaling = None
+ match {
+ measurement_type = Instantaneous
+ vif_range = Dimensionless
+ }
+ }
+ }
+ tests {
+ test {
+ args = 'SMOKEA lansensm 00010204 NOKEY'
+ telegram = 2E44333004020100031A7AC40020052F2F_02FD971D000004FD084C02000004FD3A467500002F2F2F2F2F2F2F2F2F2F
+ json = '{"_":"telegram","media":"smoke detector","driver":"lansensm","name":"SMOKEA","id":"00010204","status":"OK","async_msg_id_counter":588,"minutes_since_last_manual_test_counter":30022,"timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'SMOKEA;00010204;OK;30022;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'SMOKEA lansensm 00010204 NOKEY'
+ telegram = 2E44333004020100031A7ADE0020052F2F_02FD971D040004FD086502000004FD3A010000002F2F2F2F2F2F2F2F2F2F
+ json = '{"_":"telegram","media":"smoke detector","driver":"lansensm","name":"SMOKEA","id":"00010204","status":"SMOKE","async_msg_id_counter":613,"minutes_since_last_manual_test_counter":1,"timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'SMOKEA;00010204;SMOKE;1;1111-11-11 11:11.11'
+ }
+ }
+}
diff --git a/components/wmbus_common/drivers/src/lansenth.xmq b/components/wmbus_common/drivers/src/lansenth.xmq
new file mode 100644
index 00000000..b2666ff5
--- /dev/null
+++ b/components/wmbus_common/drivers/src/lansenth.xmq
@@ -0,0 +1,102 @@
+// Copyright (C) 2019-2023 Fredrik Öhrström (gpl-3.0-or-later)
+driver {
+ name = lansenth
+ meter_type = TempHygroMeter
+ default_fields = name,id,current_temperature_c,current_relative_humidity_rh,timestamp
+ detect {
+ mvt = LAS,07,1b
+ mvt = LAS,09,1b
+ }
+ library {
+ use = on_time_h
+ }
+ mfct_tpl_status_bits {
+ mask_bits = 0xe0
+ default_message = OK
+ map {
+ name = SABOTAGE_ENCLOSURE
+ value = 0x40
+ test = set
+ }
+ }
+ fields {
+ field {
+ name = status
+ quantity = Text
+ info = 'Status and error flags.'
+ attributes = STATUS,INCLUDE_TPL_STATUS
+ }
+ field {
+ name = current_temperature
+ quantity = Temperature
+ info = 'The current temperature.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = ExternalTemperature
+ }
+ }
+ field {
+ name = current_relative_humidity
+ quantity = RelativeHumidity
+ info = 'The current relative humidity.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = RelativeHumidity
+ }
+ }
+ field {
+ name = average_temperature_1h
+ quantity = Temperature
+ info = 'The average temperature over the last hour.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = ExternalTemperature
+ storage_nr = 1
+ }
+ }
+ field {
+ name = average_relative_humidity_1h
+ quantity = RelativeHumidity
+ info = 'The average relative humidity over the last hour.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = RelativeHumidity
+ storage_nr = 1
+ }
+ }
+ field {
+ name = average_temperature_24h
+ quantity = Temperature
+ info = 'The average temperature over the last day.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = ExternalTemperature
+ storage_nr = 2
+ }
+ }
+ field {
+ name = average_relative_humidity_24h
+ quantity = RelativeHumidity
+ info = 'The average relative humidity over the last day.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = RelativeHumidity
+ storage_nr = 2
+ }
+ }
+ }
+ tests {
+ test {
+ args = 'Tempoo lansenth 00010203 NOKEY'
+ telegram = 2e44333003020100071b7a634820252f2f0265840842658308820165950802fb1aae0142fb1aae018201fb1aa9012f
+ json = '{"_":"telegram","media":"room sensor","driver":"lansenth","name":"Tempoo","id":"00010203","status":"PERMANENT_ERROR SABOTAGE_ENCLOSURE","current_temperature_c":21.8,"current_relative_humidity_rh":43,"average_temperature_1h_c":21.79,"average_relative_humidity_1h_rh":43,"average_temperature_24h_c":21.97,"average_relative_humidity_24h_rh":42.5,"timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'Tempoo;00010203;21.8;43;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'T2 lansenth 00060041 NOKEY'
+ telegram = 2E44333041000600091B7AA70020252F2F_0265DBF94265FC04820165610901FB1B2C41FB1B238101FB1B290223BB00
+ json = '{"_":"telegram","media":"room sensor","driver":"lansenth","name":"T2","id":"00060041","status":"OK","current_temperature_c":-15.73,"current_relative_humidity_rh":44,"average_temperature_1h_c":12.76,"average_relative_humidity_1h_rh":35,"average_temperature_24h_c":24.01,"average_relative_humidity_24h_rh":41,"on_time_h":4488,"timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'T2;00060041;-15.73;44;1111-11-11 11:11.11'
+ }
+ }
+}
diff --git a/components/wmbus_common/drivers/src/lse_07_17.xmq b/components/wmbus_common/drivers/src/lse_07_17.xmq
new file mode 100644
index 00000000..13cfd404
--- /dev/null
+++ b/components/wmbus_common/drivers/src/lse_07_17.xmq
@@ -0,0 +1,128 @@
+// Copyright (C) 2021-2024 Fredrik Öhrström (gpl-3.0-or-later)
+driver {
+ name = lse_07_17
+ meter_type = WaterMeter
+ default_fields = name,id,total_m3,due_m3,due_date,error_code,error_date,device_datetime,timestamp
+ detect {
+ mvt = LSE,18,06
+ mvt = LSE,18,07
+ mvt = LSE,16,07
+ mvt = LSE,17,07
+ mvt = LSE,d8,07
+ }
+ library {
+ use = model_version
+ }
+ fields {
+ field {
+ name = total
+ quantity = Volume
+ info = 'The total water consumption recorded by this meter.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = Volume
+ }
+ }
+ field {
+ name = due
+ quantity = Volume
+ info = 'The water consumption at the due date.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = Volume
+ storage_nr = 1
+ }
+ }
+ field {
+ name = due
+ quantity = PointInTime
+ info = 'The due date configured on the meter.'
+ display_unit = date
+ match {
+ measurement_type = Instantaneous
+ vif_range = Date
+ storage_nr = 1
+ }
+ }
+ field {
+ name = consumption_at_set_date_8
+ quantity = Volume
+ info = 'The water consumption at billing period date 8.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = Volume
+ storage_nr = 8
+ }
+ }
+ field {
+ name = set_date_8
+ quantity = Text
+ info = 'Billing period date 8.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = Date
+ storage_nr = 8
+ }
+ }
+ field {
+ name = error_code
+ quantity = Text
+ info = 'Error code of the Meter, 0 means no error.'
+ match {
+ difvifkey = 02BB56
+ }
+ lookup {
+ name = ERROR_FLAGS
+ map_type = BitToString
+ mask_bits = 0xffff
+ default_message = OK
+ }
+ }
+ field {
+ name = error
+ quantity = PointInTime
+ info = 'The date the error occurred at. If no error, reads 2127-15-31 (FFFF).'
+ display_unit = date
+ match {
+ measurement_type = AtError
+ vif_range = Date
+ }
+ }
+ field {
+ name = device
+ quantity = PointInTime
+ info = 'Date when measurement was recorded.'
+ display_unit = datetime
+ match {
+ measurement_type = Instantaneous
+ vif_range = DateTime
+ }
+ }
+ }
+ tests {
+ test {
+ args = 'Water lse_07_17 13963399 NOKEY'
+ telegram = 244465329933961318067AE1000000_8C04130070000082046CBE2B01FD0C11046D010CA22C
+ json = '{"_":"telegram","media":"warm water","driver":"lse_07_17","name":"Water","id":"13963399","consumption_at_set_date_8_m3":7,"set_date_8":"2021-11-30","device_datetime":"2021-12-02 12:01","model_version":"11","timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'Water;13963399;null;null;null;null;null;2021-12-02 12:01;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'Water lse_07_17 13963399 NOKEY'
+ telegram = 2A4465329933961318067AD8800000_8C04130070000082046CBE2B01FD0C11046D1800A12C02FDAC7E9B2E
+ json = '{"_":"telegram","media":"warm water","driver":"lse_07_17","name":"Water","id":"13963399","consumption_at_set_date_8_m3":7,"set_date_8":"2021-11-30","device_datetime":"2021-12-01 00:24","model_version":"11","timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'Water;13963399;null;null;null;null;null;2021-12-01 00:24;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'Water lse_07_17 13963399 NOKEY'
+ telegram = 2D4465329933961318067ADA000000_0C13567100004C1300000000426CFFFF02BB560000326CFFFF046D2307A12C
+ json = '{"_":"telegram","media":"warm water","driver":"lse_07_17","name":"Water","id":"13963399","total_m3":7.156,"due_m3":0,"due_date":null,"error_code":"OK","error_date":null,"device_datetime":"2021-12-01 07:35","timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'Water;13963399;7.156;0;null;OK;null;2021-12-01 07:35;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'Water2 lse_07_17 09993623 NOKEY'
+ telegram = 2d44653223369909d8077a80000000046d130aed2B0c13233332004c1351762700426cdf2c326cffff02BB560000
+ json = '{"_":"telegram","device_datetime":"2023-11-13 10:19","due_date":"2022-12-31","due_m3":277.651,"error_code":"OK","error_date":null,"id":"09993623","media":"water","driver":"lse_07_17","name":"Water2","timestamp":"1111-11-11T11:11:11Z","total_m3":323.323}'
+ fields = 'Water2;09993623;323.323;277.651;2022-12-31;OK;null;2023-11-13 10:19;1111-11-11 11:11.11'
+ }
+ }
+}
diff --git a/components/wmbus_common/drivers/src/lse_08.xmq b/components/wmbus_common/drivers/src/lse_08.xmq
new file mode 100644
index 00000000..fb9d30e6
--- /dev/null
+++ b/components/wmbus_common/drivers/src/lse_08.xmq
@@ -0,0 +1,93 @@
+// Copyright (C) 2019-2022 Fredrik Öhrström (gpl-3.0-or-later)
+driver {
+ name = lse_08
+ meter_type = HeatCostAllocationMeter
+ default_fields = name,id,set_date,consumption_at_set_date_hca,timestamp
+ detect {
+ mvt = LSE,01,08
+ }
+ fields {
+ field {
+ name = status
+ quantity = Text
+ info = 'Meter status from tpl status field.'
+ attributes = STATUS,INCLUDE_TPL_STATUS
+ match {
+ difvifkey = 01FD73
+ }
+ lookup {
+ name = ERROR_FLAGS
+ map_type = BitToString
+ mask_bits = 0xff
+ default_message = OK
+ }
+ }
+ field {
+ name = current_consumption
+ quantity = HCA
+ info = 'The current heat cost allocation.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = HeatCostAllocation
+ }
+ }
+ field {
+ name = set
+ quantity = PointInTime
+ info = 'The most recent billing period date.'
+ display_unit = date
+ match {
+ measurement_type = Instantaneous
+ vif_range = Date
+ storage_nr = 8
+ }
+ }
+ field {
+ name = consumption_at_set_date
+ quantity = HCA
+ info = 'Heat cost allocation at the most recent billing period date.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = HeatCostAllocation
+ storage_nr = 8
+ }
+ }
+ field {
+ name = device
+ quantity = PointInTime
+ info = 'Date and time when the meter sent the telegram.'
+ display_unit = datetime
+ match {
+ measurement_type = Instantaneous
+ vif_range = DateTime
+ }
+ }
+ field {
+ name = duration_since_readout
+ quantity = Time
+ info = 'Duration since last measurement.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = DurationSinceReadout
+ add_combinable = FutureValue
+ }
+ }
+ field {
+ name = model_version
+ quantity = Text
+ info = 'model version.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = ModelVersion
+ }
+ }
+ }
+ tests {
+ test {
+ args = 'HCA2 lse_08 04998541 NOKEY'
+ telegram = 294465324185990401087a0080000082046c7f018b046e210300046d1a0e6f0202fdac7e012301fd0c01
+ json = '{"_":"telegram","media":"heat cost allocation","driver":"lse_08","name":"HCA2","id":"04998541","status":"UNKNOWN_80","set_date":"2003-01-31","consumption_at_set_date_hca":321,"device_datetime":"2003-02-15 14:26","duration_since_readout_h":2.489167,"model_version":"01","timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'HCA2;04998541;2003-01-31;321;1111-11-11 11:11.11'
+ }
+ }
+}
diff --git a/components/wmbus_common/drivers/src/maddalena.xmq b/components/wmbus_common/drivers/src/maddalena.xmq
index cbb35f64..23bb683f 100644
--- a/components/wmbus_common/drivers/src/maddalena.xmq
+++ b/components/wmbus_common/drivers/src/maddalena.xmq
@@ -1,4 +1,5 @@
-// Copyright (C) 2024 the wmbusmeters project (CC0-1.0)
+/* Copyright (C) 2024 Marton Czwick (CC0-1.0)
+ Copyright (C) 2024 Fredrik Öhrström (CC0-1.0) */
driver {
name = maddalena
meter_type = WaterMeter
@@ -12,9 +13,9 @@ driver {
}
fields {
field {
- name = status
- quantity = Text
- info = 'Status and error flags.'
+ name = status
+ quantity = Text
+ info = 'Status and error flags.'
attributes = INCLUDE_TPL_STATUS
match {
measurement_type = Instantaneous
@@ -32,8 +33,8 @@ driver {
test {
args = 'mywater maddalena 24018699 NOKEY'
telegram = 4E4424349986012401077AF2000020_2F2F0413A7000000046D0E0C163B04FD17000000000E789986012401FF441300000000426C01018401134A00000082016C1F3AD3013B470500C4016D1B14153B
- json = '{"_":"telegram","media":"water","meter":"maddalena","name":"mywater","id":"24018699","target_m3":0,"target_date":"2000-01-01","total_m3":0.167,"status":"OK","fabrication_no":"FF0124018699","meter_datetime":"2024-11-22 12:14","timestamp":"1111-11-11T11:11:11Z"}'
+ json = '{"_":"telegram","media":"water","driver":"maddalena","name":"mywater","id":"24018699","target_m3":0,"target_date":"2000-01-01","total_m3":0.167,"status":"OK","fabrication_no":"FF0124018699","meter_datetime":"2024-11-22 12:14","timestamp":"1111-11-11T11:11:11Z"}'
fields = 'mywater;24018699;OK;0.167;1111-11-11 11:11.11'
}
}
-}
\ No newline at end of file
+}
diff --git a/components/wmbus_common/drivers/src/metronahca.xmq b/components/wmbus_common/drivers/src/metronahca.xmq
new file mode 100644
index 00000000..6fecf7aa
--- /dev/null
+++ b/components/wmbus_common/drivers/src/metronahca.xmq
@@ -0,0 +1,24 @@
+// Copyright (C) 2026 Fredrik Öhrström (gpl-3.0-or-later)
+driver {
+ name = metronahca
+ meter_type = HeatCostAllocationMeter
+ default_fields = name,id,status,consumption_hca,timestamp
+ detect {
+ mvt = MWU,02,08
+ }
+ library {
+ use = status-tpl-only
+ use = consumption_hca
+ use = meter_date
+ use = target_hca
+ use = target_date
+ }
+ tests {
+ test {
+ args = 'MO metronahca 00381428 NOKEY'
+ telegram = 3E44F5362814380002087A430020252F2F_046E3C030000026C5C38426C3F3C446E080300002F2F2F2F2F2F2F2F2F2F0F800C000109003DBF0100209A1E0603
+ json = '{"_": "telegram","status":"OK","consumption_hca": 828,"driver": "metronahca","id": "00381428","media": "heat cost allocation","meter_date": "2026-08-28","name": "MO","target_date": "2025-12-31","target_hca": 776,"timestamp": "1111-11-11T11:11:11Z"}'
+ fields = 'MO;00381428;OK;828;1111-11-11 11:11.11'
+ }
+ }
+}
diff --git a/components/wmbus_common/drivers/src/microclima.xmq b/components/wmbus_common/drivers/src/microclima.xmq
new file mode 100644
index 00000000..20ad5cc3
--- /dev/null
+++ b/components/wmbus_common/drivers/src/microclima.xmq
@@ -0,0 +1,138 @@
+// Copyright (C) 2022 Fredrik Öhrström (gpl-3.0-or-later)
+driver {
+ name = microclima
+ meter_type = HeatMeter
+ default_fields = name,id,status,total_energy_consumption_kwh,total_volume_m3,timestamp
+ detect {
+ mvt = MAD,00,04
+ }
+ library {
+ use = meter_datetime,model_version,parameter_set
+ use = flow_temperature_c,return_temperature_c
+ }
+ fields {
+ field {
+ name = status
+ quantity = Text
+ info = 'Meter status from error flags and tpl status field.'
+ attributes = STATUS,INCLUDE_TPL_STATUS
+ match {
+ measurement_type = Instantaneous
+ vif_range = ErrorFlags
+ }
+ lookup {
+ name = ERROR_FLAGS
+ map_type = BitToString
+ mask_bits = 0xffff
+ default_message = OK
+ }
+ }
+ field {
+ name = total_energy_consumption
+ quantity = Energy
+ info = 'The total heat energy consumption recorded by this meter.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyEnergyVIF
+ }
+ }
+ field {
+ name = total_volume
+ quantity = Volume
+ info = 'The total heating media volume recorded by this meter.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ measurement_type = Instantaneous
+ vif_range = Volume
+ }
+ }
+ field {
+ name = volume_flow
+ quantity = Flow
+ info = 'The current heat media volume flow.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ measurement_type = Instantaneous
+ vif_range = VolumeFlow
+ }
+ }
+ field {
+ name = power
+ quantity = Power
+ info = 'The current power consumption.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ measurement_type = Instantaneous
+ vif_range = PowerW
+ }
+ }
+ field {
+ name = temperature_difference
+ quantity = Temperature
+ info = 'The difference between flow and return media temperatures.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ measurement_type = Instantaneous
+ vif_range = TemperatureDifference
+ }
+ }
+ field {
+ name = set
+ quantity = PointInTime
+ info = 'The most recent billing period date.'
+ attributes = HIDE
+ vif_scaling = Auto
+ dif_signedness = Signed
+ display_unit = date
+ match {
+ measurement_type = Instantaneous
+ vif_range = Date
+ storage_nr = 1
+ }
+ }
+ field {
+ name = 'consumption_at_set_date_{storage_counter}'
+ quantity = Energy
+ info = 'The total water consumption at the historic date.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyEnergyVIF
+ storage_nr = 1,31
+ }
+ }
+ field {
+ name = 'set_date_{storage_counter}'
+ quantity = PointInTime
+ info = 'Unclear! What is the date really?'
+ display_unit = date
+ calculate = 'set_date - ((storage_counter-1counter) * 1 month)'
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyEnergyVIF
+ storage_nr = 1,31
+ }
+ }
+ }
+ tests {
+ test {
+ args = 'Heat microclima 93572431 NOKEY'
+ telegram = 494424343124579300047a5a0000202f2f046d2720b62c04060d07000001fd170004130a8c0400043b00000000042b00000000025b1500025f15000261d0ff03fd0c05000002fd0b1011
+ json = '{"_":"telegram","flow_temperature_c":21,"id":"93572431","media":"heat","driver":"microclima","meter_datetime":"2021-12-22 00:39","model_version":"000005","name":"Heat","parameter_set":"1110","power_kw":0,"return_temperature_c":21,"status":"OK","temperature_difference_c":-0.48,"timestamp":"1111-11-11T11:11:11Z","total_energy_consumption_kwh":1805,"total_volume_m3":297.994,"volume_flow_m3h":0}'
+ fields = 'Heat;93572431;OK;1805;297.994;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'Heat microclima 93573086 NOKEY'
+ telegram = A44424348630579300047ADD0000202F2F046D0721B62C04064708000004135DB0030001FD1700426C9F2C4406C6040000C40106C0070000C4020637070000C4030611070000C404060B070000C405060B070000C406060B070000C407060B070000C40806A5060000C40906F7050000C40A067A050000C40B060F050000C40C06C6040000C40D063F040000C40E06BB030000C40F06A502000003FD0C05000002FD0B1111
+ json = '{"_":"telegram","consumption_at_set_date_11_kwh":1803,"consumption_at_set_date_13_kwh":1803,"consumption_at_set_date_15_kwh":1803,"consumption_at_set_date_17_kwh":1701,"consumption_at_set_date_19_kwh":1527,"consumption_at_set_date_1_kwh":1222,"consumption_at_set_date_21_kwh":1402,"consumption_at_set_date_23_kwh":1295,"consumption_at_set_date_25_kwh":1222,"consumption_at_set_date_27_kwh":1087,"consumption_at_set_date_29_kwh":955,"consumption_at_set_date_31_kwh":677,"consumption_at_set_date_3_kwh":1984,"consumption_at_set_date_5_kwh":1847,"consumption_at_set_date_7_kwh":1809,"consumption_at_set_date_9_kwh":1803,"id":"93573086","media":"heat","driver":"microclima","meter_datetime":"2021-12-22 01:07","model_version":"000005","name":"Heat","parameter_set":"1111","set_date_11_date":"2020-02-29","set_date_13_date":"2019-12-31","set_date_15_date":"2019-10-31","set_date_17_date":"2019-08-31","set_date_19_date":"2019-06-30","set_date_1_date":"2020-12-31","set_date_21_date":"2019-04-30","set_date_23_date":"2019-02-28","set_date_25_date":"2018-12-31","set_date_27_date":"2018-10-31","set_date_29_date":"2018-08-31","set_date_31_date":"2018-06-30","set_date_3_date":"2020-10-31","set_date_5_date":"2020-08-31","set_date_7_date":"2020-06-30","set_date_9_date":"2020-04-30","status":"OK","timestamp":"1111-11-11T11:11:11Z","total_energy_consumption_kwh":2119,"total_volume_m3":241.757}'
+ fields = 'Heat;93573086;OK;2119;241.757;1111-11-11 11:11.11'
+ }
+ }
+}
diff --git a/components/wmbus_common/drivers/src/minomess.xmq b/components/wmbus_common/drivers/src/minomess.xmq
new file mode 100644
index 00000000..eec71511
--- /dev/null
+++ b/components/wmbus_common/drivers/src/minomess.xmq
@@ -0,0 +1,209 @@
+/* Copyright (C) 2021 Olli Salonen (gpl-3.0-or-later)
+ Copyright (C) 2022-2026 Fredrik Öhrström (gpl-3.0-or-later)
+ Copyright (C) 2026 Hynek Moravec (gpl-3.0-or-later) */
+driver {
+ name = minomess
+ meter_type = WaterMeter
+ default_fields = name,id,status,total_m3,target_m3,timestamp
+ detect {
+ mvt = ZRI,00,07
+ mvt = ZRI,01,16
+ mvt = ZRI,01,06
+ }
+ mfct_tpl_status_bits {
+ mask_bits = 0xf8
+ default_message = OK
+ map {
+ name = PERMANENT_ERROR
+ value = 0x08
+ test = Set
+ }
+ map {
+ name = TEMPORARY_ERROR
+ value = 0x10
+ test = Set
+ }
+ map {
+ name = WAS_LEAKING
+ value = 0x20
+ test = Set
+ }
+ map {
+ name = WAS_TAMPERED
+ value = 0x40
+ test = Set
+ }
+ map {
+ name = WAS_REMOVED
+ value = 0x80
+ test = Set
+ }
+ }
+ library {
+ use = total_m3
+ use = meter_date
+ use = meter_datetime
+ use = fabrication_no
+ use = operating_time_h
+ use = on_time_h
+ use = on_time_at_error_h
+ use = total_backward_m3
+ use = volume_flow_m3h
+ }
+ fields {
+ field {
+ name = target
+ quantity = Volume
+ info = 'The total water consumption recorded at the beginning of this month.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = Volume
+ storage_nr = 8
+ }
+ }
+ field {
+ name = target
+ quantity = PointInTime
+ info = 'Date when target water consumption was recorded.'
+ display_unit = date
+ match {
+ measurement_type = Instantaneous
+ vif_range = Date
+ storage_nr = 8
+ }
+ }
+ field {
+ name = target
+ quantity = Volume
+ info = 'The total water consumption recorded at the beginning of this month.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = Volume
+ storage_nr = 1
+ }
+ }
+ field {
+ name = target
+ quantity = PointInTime
+ info = 'Date when target water consumption was recorded.'
+ display_unit = date
+ match {
+ measurement_type = Instantaneous
+ vif_range = Date
+ storage_nr = 1
+ }
+ }
+ field {
+ name = status
+ quantity = Text
+ info = 'Status and error flags.'
+ attributes = STATUS,INCLUDE_TPL_STATUS
+ match {
+ difvifkey = 02FD17
+ }
+ lookup {
+ name = ERROR_FLAGS
+ map_type = BitToString
+ mask_bits = 0xffff
+ default_message = OK
+ // Status byte B (high byte)
+ map {
+ name = BURSTING
+ value = 0x8000
+ test = Set
+ }
+ map {
+ name = REMOVED
+ value = 0x4000
+ test = Set
+ }
+ map {
+ name = LEAKING
+ value = 0x2000
+ test = Set
+ }
+ map {
+ name = WAS_BACKFLOWING
+ value = 0x1000
+ test = Set
+ }
+ map {
+ name = BACKFLOWING
+ value = 0x0800
+ test = Set
+ }
+ map {
+ name = WAS_BLOCKED
+ value = 0x0400
+ test = Set
+ }
+ map {
+ name = UNDERSIZED
+ value = 0x0200
+ test = Set
+ }
+ map {
+ name = OVERSIZED
+ value = 0x0100
+ test = Set
+ }
+ // Status byte A (low byte)
+ map {
+ name = WAS_REMOVED
+ value = 0x0080
+ test = Set
+ }
+ map {
+ name = WAS_TAMPERED
+ value = 0x0040
+ test = Set
+ }
+ map {
+ name = WAS_LEAKING
+ value = 0x0020
+ test = Set
+ }
+ map {
+ name = TEMPORARY_ERROR
+ value = 0x0010
+ test = Set
+ }
+ map {
+ name = PERMANENT_ERROR
+ value = 0x0008
+ test = Set
+ }
+ map {
+ name = BATTERY_EOL
+ value = 0x0004
+ test = Set
+ }
+ map {
+ name = ABNORMAL_CONDITION
+ value = 0x0002
+ test = Set
+ }
+ }
+ }
+ }
+ tests {
+ test {
+ args = 'Mino minomess 15503451 NOKEY'
+ telegram = 6644496A1064035514377251345015496A0007EE0050052F2F_0C1359000000026CBE2B82046CA12B8C0413FFFFFFFF8D0493132CFBFEFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF02FD1700002F2F
+ json = '{"_":"telegram","media":"water","driver":"minomess","name":"Mino","id":"15503451","meter_date":"2021-11-30","total_m3":0.059,"target_date":"2021-11-01","status":"OK","timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'Mino;15503451;OK;0.059;null;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'Zenner_cold minomess 21314151 NOKEY'
+ telegram = 6644496A4425155518377251413121496A0116360050052F2F_0C1355000000026CEC2182046CE1218C0413000000808D0493132C33FE00008000008000008000008000008000008000008000008000008000008000008000008000008000008002FD1700002F2F
+ json = '{"_":"telegram","media":"cold water","driver":"minomess","name":"Zenner_cold","id":"21314151","meter_date":"2023-01-12","total_m3":0.055,"target_m3":80000,"target_date":"2023-01-01","status":"OK","timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'Zenner_cold;21314151;OK;0.055;80000;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'Zenner_warm minomess 51413121 NOKEY'
+ telegram = 6644496A8753155518377221314151496A0106300050052F2F_0C1357000000026CEC2182046CE1218C0413000000808D0493132C33FE00008000008000008000008000008000008000008000008000008000008000008000008000008000008002FD1700002F2F
+ json = '{"_":"telegram","media":"warm water","driver":"minomess","name":"Zenner_warm","id":"51413121","meter_date":"2023-01-12","total_m3":0.057,"target_m3":80000,"target_date":"2023-01-01","status":"OK","timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'Zenner_warm;51413121;OK;0.057;80000;1111-11-11 11:11.11'
+ }
+ }
+}
diff --git a/components/wmbus_common/drivers/src/mkradio3.xmq b/components/wmbus_common/drivers/src/mkradio3.xmq
new file mode 100644
index 00000000..9b4f00e6
--- /dev/null
+++ b/components/wmbus_common/drivers/src/mkradio3.xmq
@@ -0,0 +1,166 @@
+// Copyright (C) 2026 Fredrik Öhrström (gpl-3.0-or-later)
+driver {
+ name = mkradio3
+ meter_type = WaterMeter
+ default_fields = name,id,total_m3,target_m3,current_date,prev_date,timestamp
+ manufacturer = Techem
+ detect {
+ mvt = TCH,74,62
+ mvt = TCH,74,72
+ }
+ fields {
+ field {
+ name = target
+ quantity = Volume
+ info = 'The total water consumption recorded at the beginning of this month.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = Volume
+ storage_nr = 1
+ }
+ }
+ field {
+ name = current
+ quantity = Volume
+ attributes = HIDE
+ match {
+ measurement_type = Instantaneous
+ vif_range = Volume
+ }
+ }
+ field {
+ name = total
+ quantity = Volume
+ info = 'The total water consumption recorded by this meter.'
+ calculate = target_m3+current_m3
+ }
+ field {
+ name = curr_date_raw
+ quantity = Dimensionless
+ attributes = HIDE
+ match {
+ difvifkey = 42FD3A
+ }
+ }
+ field {
+ name = prev_date_raw
+ quantity = Dimensionless
+ attributes = HIDE
+ match {
+ difvifkey = 02FD3A
+ }
+ }
+ field {
+ name = prev_day
+ quantity = Dimensionless
+ attributes = HIDE
+ calculate = 'prev_date_raw_counter % 32counter'
+ }
+ field {
+ name = prev_month
+ quantity = Dimensionless
+ attributes = HIDE
+ calculate = '(prev_date_raw_counter >> 5counter) % 16counter'
+ }
+ field {
+ name = prev_year
+ quantity = Dimensionless
+ attributes = HIDE
+ calculate = 'prev_date_raw_counter >> 9counter'
+ }
+ field {
+ name = curr_day
+ quantity = Dimensionless
+ attributes = HIDE
+ calculate = '(curr_date_raw_counter >> 4counter) % 32counter'
+ }
+ field {
+ name = curr_month
+ quantity = Dimensionless
+ attributes = HIDE
+ calculate = '(curr_date_raw_counter >> 9counter) % 16counter'
+ }
+ field {
+ name = curr_year
+ quantity = Dimensionless
+ attributes = HIDE
+ info = 'Current billing year is previous year unless month/day rolled over.'
+ calculate = 'prev_year_counter + ((curr_month_counter < prev_month_counter) || ((curr_month_counter == prev_month_counter) && (curr_day_counter <= prev_day_counter)))'
+ }
+ field {
+ name = current
+ quantity = PointInTime
+ display_unit = date
+ info = 'Date of current billing period.'
+ calculate = "'2000-01-01' +
+ (((curr_year_counter * 12counter) + curr_month_counter - 1counter) * 1month) +
+ ((curr_day_counter - 1counter) * 24h)"
+ }
+ field {
+ name = prev
+ quantity = PointInTime
+ display_unit = date
+ info = 'Date of previous billing period.'
+ calculate = "'2000-01-01' +
+ (((prev_year_counter * 12counter) + prev_month_counter - 1counter) * 1month) +
+ ((prev_day_counter - 1counter) * 24h)"
+ }
+ field {
+ name = mfct_specific_data
+ quantity = Text
+ attributes = HIDE
+ match_entire_payload = true
+ ixml = "decode = frame.
+ frame = byte,
+ PrevDate,
+ PrevVolume,
+ CurrDate,
+ CurrVolume,
+ byte*.
+
+ PrevDate = word, @DV_prev_date_raw.
+ PrevVolume = word, @DV_target.
+ CurrDate = word, @DV_current_date_raw.
+ CurrVolume = word, @DV_current.
+
+ -hex = ['A'-'F';'0'-'9'].
+ -byte = hex, hex.
+ -word = byte, byte.
+
+ DV_prev_date_raw>dvk = +'02FD3A'.
+ DV_target>dvk = +'4215'.
+ DV_current_date_raw>dvk = +'42FD3A'.
+ DV_current>dvk = +'0215'."
+ }
+ }
+ tests {
+ test {
+ comment = 'Version 0x74 legacy payload'
+ args = 'Duschen mkradio3 34333231 NOKEY'
+ telegram = 2F446850313233347462A2_069F255900B029310000000306060906030609070606050509050505050407040605070500
+ json = '{"_":"telegram","media":"warm water","driver":"mkradio3","name":"Duschen","id":"34333231","total_m3":13.8,"target_m3":8.9,"current_date":"2019-04-27","prev_date":"2018-12-31","timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'Duschen;34333231;13.8;8.9;2019-04-27;2018-12-31;1111-11-11 11:11.11'
+ }
+ test {
+ comment = 'Version 0x74 payload with prev_date 2018-03-31'
+ args = 'Duschen mkradio3 34333231 NOKEY'
+ telegram = 2F446850313233347462A2_067F245900B029310000000306060906030609070606050509050505050407040605070500
+ json = '{"_":"telegram","media":"warm water","driver":"mkradio3","name":"Duschen","id":"34333231","total_m3":13.8,"target_m3":8.9,"current_date":"2018-04-27","prev_date":"2018-03-31","timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'Duschen;34333231;13.8;8.9;2018-04-27;2018-03-31;1111-11-11 11:11.11'
+ }
+ test {
+ comment = 'Version 0x74 payload with prev_date 2018-03-31 and current_date 2019-03-31'
+ args = 'Duschen mkradio3 34333231 NOKEY'
+ telegram = 2F446850313233347462A2_067F245900F007310000000306060906030609070606050509050505050407040605070500
+ json = '{"_":"telegram","media":"warm water","driver":"mkradio3","name":"Duschen","id":"34333231","total_m3":13.8,"target_m3":8.9,"current_date":"2019-03-31","prev_date":"2018-03-31","timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'Duschen;34333231;13.8;8.9;2019-03-31;2018-03-31;1111-11-11 11:11.11'
+ }
+ test {
+ comment = 'Version 0x74 payload with prev_date 2018-03-31 and current_date 2018-04-01'
+ args = 'Duschen mkradio3 34333231 NOKEY'
+ telegram = 2F446850313233347462A2_067F2459001008310000000306060906030609070606050509050505050407040605070500
+ json = '{"_":"telegram","media":"warm water","driver":"mkradio3","name":"Duschen","id":"34333231","total_m3":13.8,"target_m3":8.9,"current_date":"2018-04-01","prev_date":"2018-03-31","timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'Duschen;34333231;13.8;8.9;2018-04-01;2018-03-31;1111-11-11 11:11.11'
+ }
+ }
+}
diff --git a/components/wmbus_common/drivers/src/mkradio3a.xmq b/components/wmbus_common/drivers/src/mkradio3a.xmq
new file mode 100644
index 00000000..320450b6
--- /dev/null
+++ b/components/wmbus_common/drivers/src/mkradio3a.xmq
@@ -0,0 +1,543 @@
+// Copyright (C) 2026 Fredrik Öhrström (gpl-3.0-or-later)
+driver {
+ name = mkradio3a
+ meter_type = WaterMeter
+ default_fields = name,id,total_m3,target_m3,timestamp
+ manufacturer = Techem
+ detect {
+ mvt = TCH,50,72
+ }
+ fields {
+ field {
+ name = curr_date_raw
+ quantity = Dimensionless
+ attributes = HIDE
+ match {
+ difvifkey = 02FD3A
+ }
+ }
+ field {
+ name = total_raw
+ quantity = Dimensionless
+ attributes = HIDE
+ match {
+ difvifkey = 03FD3A
+ }
+ }
+ field {
+ name = hist0_raw
+ quantity = Dimensionless
+ attributes = HIDE
+ match {
+ difvifkey = 8200FD3A
+ }
+ }
+ field {
+ name = hist1_raw
+ quantity = Dimensionless
+ attributes = HIDE
+ match {
+ difvifkey = 8201FD3A
+ }
+ }
+ field {
+ name = hist2_raw
+ quantity = Dimensionless
+ attributes = HIDE
+ match {
+ difvifkey = 8202FD3A
+ }
+ }
+ field {
+ name = hist3_raw
+ quantity = Dimensionless
+ attributes = HIDE
+ match {
+ difvifkey = 8203FD3A
+ }
+ }
+ field {
+ name = hist4_raw
+ quantity = Dimensionless
+ attributes = HIDE
+ match {
+ difvifkey = 8204FD3A
+ }
+ }
+ field {
+ name = hist5_raw
+ quantity = Dimensionless
+ attributes = HIDE
+ match {
+ difvifkey = 8205FD3A
+ }
+ }
+ field {
+ name = hist6_raw
+ quantity = Dimensionless
+ attributes = HIDE
+ match {
+ difvifkey = 8206FD3A
+ }
+ }
+ field {
+ name = hist7_raw
+ quantity = Dimensionless
+ attributes = HIDE
+ match {
+ difvifkey = 8207FD3A
+ }
+ }
+ field {
+ name = hist8_raw
+ quantity = Dimensionless
+ attributes = HIDE
+ match {
+ difvifkey = 8208FD3A
+ }
+ }
+ field {
+ name = hist9_raw
+ quantity = Dimensionless
+ attributes = HIDE
+ match {
+ difvifkey = 8209FD3A
+ }
+ }
+ field {
+ name = hist10_raw
+ quantity = Dimensionless
+ attributes = HIDE
+ match {
+ difvifkey = 820AFD3A
+ }
+ }
+ field {
+ name = hist11_raw
+ quantity = Dimensionless
+ attributes = HIDE
+ match {
+ difvifkey = 820BFD3A
+ }
+ }
+ field {
+ name = curr_day
+ quantity = Dimensionless
+ attributes = HIDE
+ calculate = 'curr_date_raw_counter % 32counter'
+ }
+ field {
+ name = curr_month
+ quantity = Dimensionless
+ attributes = HIDE
+ calculate = '(curr_date_raw_counter >> 5counter) % 16counter'
+ }
+ field {
+ name = curr_year
+ quantity = Dimensionless
+ attributes = HIDE
+ calculate = 'curr_date_raw_counter >> 9counter'
+ }
+ field {
+ name = is_early
+ quantity = Dimensionless
+ attributes = HIDE
+ calculate = 'curr_day_counter <= 15counter'
+ }
+ field {
+ name = is_late
+ quantity = Dimensionless
+ attributes = HIDE
+ calculate = '1counter - is_early_counter'
+ }
+ field {
+ name = hist0_lo
+ quantity = Dimensionless
+ attributes = HIDE
+ calculate = 'hist0_raw_counter % 256counter'
+ }
+ field {
+ name = hist1_lo
+ quantity = Dimensionless
+ attributes = HIDE
+ calculate = 'hist1_raw_counter % 256counter'
+ }
+ field {
+ name = hist2_lo
+ quantity = Dimensionless
+ attributes = HIDE
+ calculate = 'hist2_raw_counter % 256counter'
+ }
+ field {
+ name = hist3_lo
+ quantity = Dimensionless
+ attributes = HIDE
+ calculate = 'hist3_raw_counter % 256counter'
+ }
+ field {
+ name = hist4_lo
+ quantity = Dimensionless
+ attributes = HIDE
+ calculate = 'hist4_raw_counter % 256counter'
+ }
+ field {
+ name = hist5_lo
+ quantity = Dimensionless
+ attributes = HIDE
+ calculate = 'hist5_raw_counter % 256counter'
+ }
+ field {
+ name = hist6_lo
+ quantity = Dimensionless
+ attributes = HIDE
+ calculate = 'hist6_raw_counter % 256counter'
+ }
+ field {
+ name = hist7_lo
+ quantity = Dimensionless
+ attributes = HIDE
+ calculate = 'hist7_raw_counter % 256counter'
+ }
+ field {
+ name = hist8_lo
+ quantity = Dimensionless
+ attributes = HIDE
+ calculate = 'hist8_raw_counter % 256counter'
+ }
+ field {
+ name = hist9_lo
+ quantity = Dimensionless
+ attributes = HIDE
+ calculate = 'hist9_raw_counter % 256counter'
+ }
+ field {
+ name = hist10_lo
+ quantity = Dimensionless
+ attributes = HIDE
+ calculate = 'hist10_raw_counter % 256counter'
+ }
+ field {
+ name = hist11_lo
+ quantity = Dimensionless
+ attributes = HIDE
+ calculate = 'hist11_raw_counter % 256counter'
+ }
+ field {
+ name = hist0_hi
+ quantity = Dimensionless
+ attributes = HIDE
+ calculate = 'hist0_raw_counter >> 8counter'
+ }
+ field {
+ name = hist1_hi
+ quantity = Dimensionless
+ attributes = HIDE
+ calculate = 'hist1_raw_counter >> 8counter'
+ }
+ field {
+ name = hist2_hi
+ quantity = Dimensionless
+ attributes = HIDE
+ calculate = 'hist2_raw_counter >> 8counter'
+ }
+ field {
+ name = hist3_hi
+ quantity = Dimensionless
+ attributes = HIDE
+ calculate = 'hist3_raw_counter >> 8counter'
+ }
+ field {
+ name = hist4_hi
+ quantity = Dimensionless
+ attributes = HIDE
+ calculate = 'hist4_raw_counter >> 8counter'
+ }
+ field {
+ name = hist5_hi
+ quantity = Dimensionless
+ attributes = HIDE
+ calculate = 'hist5_raw_counter >> 8counter'
+ }
+ field {
+ name = hist6_hi
+ quantity = Dimensionless
+ attributes = HIDE
+ calculate = 'hist6_raw_counter >> 8counter'
+ }
+ field {
+ name = hist7_hi
+ quantity = Dimensionless
+ attributes = HIDE
+ calculate = 'hist7_raw_counter >> 8counter'
+ }
+ field {
+ name = hist8_hi
+ quantity = Dimensionless
+ attributes = HIDE
+ calculate = 'hist8_raw_counter >> 8counter'
+ }
+ field {
+ name = hist9_hi
+ quantity = Dimensionless
+ attributes = HIDE
+ calculate = 'hist9_raw_counter >> 8counter'
+ }
+ field {
+ name = hist10_hi
+ quantity = Dimensionless
+ attributes = HIDE
+ calculate = 'hist10_raw_counter >> 8counter'
+ }
+ field {
+ name = hist11_hi
+ quantity = Dimensionless
+ attributes = HIDE
+ calculate = 'hist11_raw_counter >> 8counter'
+ }
+ field {
+ name = rel0
+ quantity = Volume
+ attributes = HIDE
+ calculate = '((is_early_counter * hist0_lo_counter) + (is_late_counter * (hist0_lo_counter + hist0_hi_counter))) * 0.1m3'
+ }
+ field {
+ name = rel1
+ quantity = Volume
+ attributes = HIDE
+ calculate = '((is_early_counter * (hist0_hi_counter + hist1_lo_counter)) + (is_late_counter * (hist1_lo_counter + hist1_hi_counter))) * 0.1m3'
+ }
+ field {
+ name = rel2
+ quantity = Volume
+ attributes = HIDE
+ calculate = '((is_early_counter * (hist1_hi_counter + hist2_lo_counter)) + (is_late_counter * (hist2_lo_counter + hist2_hi_counter))) * 0.1m3'
+ }
+ field {
+ name = rel3
+ quantity = Volume
+ attributes = HIDE
+ calculate = '((is_early_counter * (hist2_hi_counter + hist3_lo_counter)) + (is_late_counter * (hist3_lo_counter + hist3_hi_counter))) * 0.1m3'
+ }
+ field {
+ name = rel4
+ quantity = Volume
+ attributes = HIDE
+ calculate = '((is_early_counter * (hist3_hi_counter + hist4_lo_counter)) + (is_late_counter * (hist4_lo_counter + hist4_hi_counter))) * 0.1m3'
+ }
+ field {
+ name = rel5
+ quantity = Volume
+ attributes = HIDE
+ calculate = '((is_early_counter * (hist4_hi_counter + hist5_lo_counter)) + (is_late_counter * (hist5_lo_counter + hist5_hi_counter))) * 0.1m3'
+ }
+ field {
+ name = rel6
+ quantity = Volume
+ attributes = HIDE
+ calculate = '((is_early_counter * (hist5_hi_counter + hist6_lo_counter)) + (is_late_counter * (hist6_lo_counter + hist6_hi_counter))) * 0.1m3'
+ }
+ field {
+ name = rel7
+ quantity = Volume
+ attributes = HIDE
+ calculate = '((is_early_counter * (hist6_hi_counter + hist7_lo_counter)) + (is_late_counter * (hist7_lo_counter + hist7_hi_counter))) * 0.1m3'
+ }
+ field {
+ name = rel8
+ quantity = Volume
+ attributes = HIDE
+ calculate = '((is_early_counter * (hist7_hi_counter + hist8_lo_counter)) + (is_late_counter * (hist8_lo_counter + hist8_hi_counter))) * 0.1m3'
+ }
+ field {
+ name = rel9
+ quantity = Volume
+ attributes = HIDE
+ calculate = '((is_early_counter * (hist8_hi_counter + hist9_lo_counter)) + (is_late_counter * (hist9_lo_counter + hist9_hi_counter))) * 0.1m3'
+ }
+ field {
+ name = rel10
+ quantity = Volume
+ attributes = HIDE
+ calculate = '((is_early_counter * (hist9_hi_counter + hist10_lo_counter)) + (is_late_counter * (hist10_lo_counter + hist10_hi_counter))) * 0.1m3'
+ }
+ field {
+ name = rel11
+ quantity = Volume
+ attributes = HIDE
+ calculate = '((is_early_counter * (hist10_hi_counter + hist11_lo_counter)) + (is_late_counter * (hist11_lo_counter + hist11_hi_counter))) * 0.1m3'
+ }
+ field {
+ name = total
+ quantity = Volume
+ info = 'The total water consumption recorded by this meter.'
+ calculate = 'total_raw_counter * 0.1m3'
+ }
+ field {
+ name = target
+ quantity = Volume
+ info = 'The total water consumption recorded at the beginning of this month.'
+ calculate = rel0_m3
+ }
+ field {
+ name = target
+ quantity = PointInTime
+ display_unit = date
+ info = 'Date of current billing period.'
+ calculate = "'2000-01-01 00:00:00' +
+ (((curr_year_counter * 12counter) + curr_month_counter - 1counter) * 1month) +
+ ((curr_day_counter - 1counter) * 24h)"
+ }
+ field {
+ name = last_jan
+ quantity = Volume
+ info = 'The total water consumption recorded at the beginning of January.'
+ calculate = '((curr_month_counter == 1counter) * rel0_m3) + ((curr_month_counter == 2counter) * rel1_m3) + ((curr_month_counter == 3counter) * rel2_m3) + ((curr_month_counter == 4counter) * rel3_m3) + ((curr_month_counter == 5counter) * rel4_m3) + ((curr_month_counter == 6counter) * rel5_m3) + ((curr_month_counter == 7counter) * rel6_m3) + ((curr_month_counter == 8counter) * rel7_m3) + ((curr_month_counter == 9counter) * rel8_m3) + ((curr_month_counter == 10counter) * rel9_m3) + ((curr_month_counter == 11counter) * rel10_m3) + ((curr_month_counter == 12counter) * rel11_m3)'
+ }
+ field {
+ name = last_feb
+ quantity = Volume
+ info = 'The total water consumption recorded at the beginning of February.'
+ calculate = '((curr_month_counter == 1counter) * rel11_m3) + ((curr_month_counter == 2counter) * rel0_m3) + ((curr_month_counter == 3counter) * rel1_m3) + ((curr_month_counter == 4counter) * rel2_m3) + ((curr_month_counter == 5counter) * rel3_m3) + ((curr_month_counter == 6counter) * rel4_m3) + ((curr_month_counter == 7counter) * rel5_m3) + ((curr_month_counter == 8counter) * rel6_m3) + ((curr_month_counter == 9counter) * rel7_m3) + ((curr_month_counter == 10counter) * rel8_m3) + ((curr_month_counter == 11counter) * rel9_m3) + ((curr_month_counter == 12counter) * rel10_m3)'
+ }
+ field {
+ name = last_mar
+ quantity = Volume
+ info = 'The total water consumption recorded at the beginning of March.'
+ calculate = '((curr_month_counter == 1counter) * rel10_m3) + ((curr_month_counter == 2counter) * rel11_m3) + ((curr_month_counter == 3counter) * rel0_m3) + ((curr_month_counter == 4counter) * rel1_m3) + ((curr_month_counter == 5counter) * rel2_m3) + ((curr_month_counter == 6counter) * rel3_m3) + ((curr_month_counter == 7counter) * rel4_m3) + ((curr_month_counter == 8counter) * rel5_m3) + ((curr_month_counter == 9counter) * rel6_m3) + ((curr_month_counter == 10counter) * rel7_m3) + ((curr_month_counter == 11counter) * rel8_m3) + ((curr_month_counter == 12counter) * rel9_m3)'
+ }
+ field {
+ name = last_apr
+ quantity = Volume
+ info = 'The total water consumption recorded at the beginning of April.'
+ calculate = '((curr_month_counter == 1counter) * rel9_m3) + ((curr_month_counter == 2counter) * rel10_m3) + ((curr_month_counter == 3counter) * rel11_m3) + ((curr_month_counter == 4counter) * rel0_m3) + ((curr_month_counter == 5counter) * rel1_m3) + ((curr_month_counter == 6counter) * rel2_m3) + ((curr_month_counter == 7counter) * rel3_m3) + ((curr_month_counter == 8counter) * rel4_m3) + ((curr_month_counter == 9counter) * rel5_m3) + ((curr_month_counter == 10counter) * rel6_m3) + ((curr_month_counter == 11counter) * rel7_m3) + ((curr_month_counter == 12counter) * rel8_m3)'
+ }
+ field {
+ name = last_may
+ quantity = Volume
+ info = 'The total water consumption recorded at the beginning of May.'
+ calculate = '((curr_month_counter == 1counter) * rel8_m3) + ((curr_month_counter == 2counter) * rel9_m3) + ((curr_month_counter == 3counter) * rel10_m3) + ((curr_month_counter == 4counter) * rel11_m3) + ((curr_month_counter == 5counter) * rel0_m3) + ((curr_month_counter == 6counter) * rel1_m3) + ((curr_month_counter == 7counter) * rel2_m3) + ((curr_month_counter == 8counter) * rel3_m3) + ((curr_month_counter == 9counter) * rel4_m3) + ((curr_month_counter == 10counter) * rel5_m3) + ((curr_month_counter == 11counter) * rel6_m3) + ((curr_month_counter == 12counter) * rel7_m3)'
+ }
+ field {
+ name = last_jun
+ quantity = Volume
+ info = 'The total water consumption recorded at the beginning of June.'
+ calculate = '((curr_month_counter == 1counter) * rel7_m3) + ((curr_month_counter == 2counter) * rel8_m3) + ((curr_month_counter == 3counter) * rel9_m3) + ((curr_month_counter == 4counter) * rel10_m3) + ((curr_month_counter == 5counter) * rel11_m3) + ((curr_month_counter == 6counter) * rel0_m3) + ((curr_month_counter == 7counter) * rel1_m3) + ((curr_month_counter == 8counter) * rel2_m3) + ((curr_month_counter == 9counter) * rel3_m3) + ((curr_month_counter == 10counter) * rel4_m3) + ((curr_month_counter == 11counter) * rel5_m3) + ((curr_month_counter == 12counter) * rel6_m3)'
+ }
+ field {
+ name = last_jul
+ quantity = Volume
+ info = 'The total water consumption recorded at the beginning of July.'
+ calculate = '((curr_month_counter == 1counter) * rel6_m3) + ((curr_month_counter == 2counter) * rel7_m3) + ((curr_month_counter == 3counter) * rel8_m3) + ((curr_month_counter == 4counter) * rel9_m3) + ((curr_month_counter == 5counter) * rel10_m3) + ((curr_month_counter == 6counter) * rel11_m3) + ((curr_month_counter == 7counter) * rel0_m3) + ((curr_month_counter == 8counter) * rel1_m3) + ((curr_month_counter == 9counter) * rel2_m3) + ((curr_month_counter == 10counter) * rel3_m3) + ((curr_month_counter == 11counter) * rel4_m3) + ((curr_month_counter == 12counter) * rel5_m3)'
+ }
+ field {
+ name = last_aug
+ quantity = Volume
+ info = 'The total water consumption recorded at the beginning of August.'
+ calculate = '((curr_month_counter == 1counter) * rel5_m3) + ((curr_month_counter == 2counter) * rel6_m3) + ((curr_month_counter == 3counter) * rel7_m3) + ((curr_month_counter == 4counter) * rel8_m3) + ((curr_month_counter == 5counter) * rel9_m3) + ((curr_month_counter == 6counter) * rel10_m3) + ((curr_month_counter == 7counter) * rel11_m3) + ((curr_month_counter == 8counter) * rel0_m3) + ((curr_month_counter == 9counter) * rel1_m3) + ((curr_month_counter == 10counter) * rel2_m3) + ((curr_month_counter == 11counter) * rel3_m3) + ((curr_month_counter == 12counter) * rel4_m3)'
+ }
+ field {
+ name = last_sep
+ quantity = Volume
+ info = 'The total water consumption recorded at the beginning of September.'
+ calculate = '((curr_month_counter == 1counter) * rel4_m3) + ((curr_month_counter == 2counter) * rel5_m3) + ((curr_month_counter == 3counter) * rel6_m3) + ((curr_month_counter == 4counter) * rel7_m3) + ((curr_month_counter == 5counter) * rel8_m3) + ((curr_month_counter == 6counter) * rel9_m3) + ((curr_month_counter == 7counter) * rel10_m3) + ((curr_month_counter == 8counter) * rel11_m3) + ((curr_month_counter == 9counter) * rel0_m3) + ((curr_month_counter == 10counter) * rel1_m3) + ((curr_month_counter == 11counter) * rel2_m3) + ((curr_month_counter == 12counter) * rel3_m3)'
+ }
+ field {
+ name = last_oct
+ quantity = Volume
+ info = 'The total water consumption recorded at the beginning of October.'
+ calculate = '((curr_month_counter == 1counter) * rel3_m3) + ((curr_month_counter == 2counter) * rel4_m3) + ((curr_month_counter == 3counter) * rel5_m3) + ((curr_month_counter == 4counter) * rel6_m3) + ((curr_month_counter == 5counter) * rel7_m3) + ((curr_month_counter == 6counter) * rel8_m3) + ((curr_month_counter == 7counter) * rel9_m3) + ((curr_month_counter == 8counter) * rel10_m3) + ((curr_month_counter == 9counter) * rel11_m3) + ((curr_month_counter == 10counter) * rel0_m3) + ((curr_month_counter == 11counter) * rel1_m3) + ((curr_month_counter == 12counter) * rel2_m3)'
+ }
+ field {
+ name = last_nov
+ quantity = Volume
+ info = 'The total water consumption recorded at the beginning of November.'
+ calculate = '((curr_month_counter == 1counter) * rel2_m3) + ((curr_month_counter == 2counter) * rel3_m3) + ((curr_month_counter == 3counter) * rel4_m3) + ((curr_month_counter == 4counter) * rel5_m3) + ((curr_month_counter == 5counter) * rel6_m3) + ((curr_month_counter == 6counter) * rel7_m3) + ((curr_month_counter == 7counter) * rel8_m3) + ((curr_month_counter == 8counter) * rel9_m3) + ((curr_month_counter == 9counter) * rel10_m3) + ((curr_month_counter == 10counter) * rel11_m3) + ((curr_month_counter == 11counter) * rel0_m3) + ((curr_month_counter == 12counter) * rel1_m3)'
+ }
+ field {
+ name = last_dec
+ quantity = Volume
+ info = 'The total water consumption recorded at the beginning of December.'
+ calculate = '((curr_month_counter == 1counter) * rel1_m3) + ((curr_month_counter == 2counter) * rel2_m3) + ((curr_month_counter == 3counter) * rel3_m3) + ((curr_month_counter == 4counter) * rel4_m3) + ((curr_month_counter == 5counter) * rel5_m3) + ((curr_month_counter == 6counter) * rel6_m3) + ((curr_month_counter == 7counter) * rel7_m3) + ((curr_month_counter == 8counter) * rel8_m3) + ((curr_month_counter == 9counter) * rel9_m3) + ((curr_month_counter == 10counter) * rel10_m3) + ((curr_month_counter == 11counter) * rel11_m3) + ((curr_month_counter == 12counter) * rel0_m3)'
+ }
+ field {
+ name = mfct_specific_data
+ quantity = Text
+ attributes = HIDE
+ match_entire_payload = true
+ ixml = "decode = frame.
+ frame = byte,
+ byte,
+ CurrDate,
+ Total,
+ byte,
+ Hist0,
+ byte,
+ Hist1,
+ byte,
+ Hist2,
+ byte,
+ Hist3,
+ byte,
+ Hist4,
+ byte,
+ Hist5,
+ byte,
+ Hist6,
+ byte,
+ Hist7,
+ byte,
+ Hist8,
+ byte,
+ Hist9,
+ byte,
+ Hist10,
+ byte,
+ Hist11,
+ byte.
+
+ CurrDate = word, @DV_curr_date_raw.
+ Total = triplet, @DV_total_raw.
+ Hist0 = word, @DV_hist0_raw.
+ Hist1 = word, @DV_hist1_raw.
+ Hist2 = word, @DV_hist2_raw.
+ Hist3 = word, @DV_hist3_raw.
+ Hist4 = word, @DV_hist4_raw.
+ Hist5 = word, @DV_hist5_raw.
+ Hist6 = word, @DV_hist6_raw.
+ Hist7 = word, @DV_hist7_raw.
+ Hist8 = word, @DV_hist8_raw.
+ Hist9 = word, @DV_hist9_raw.
+ Hist10 = word, @DV_hist10_raw.
+ Hist11 = word, @DV_hist11_raw.
+
+ -hex = ['A'-'F';'0'-'9'].
+ -byte = hex, hex.
+ -word = byte, byte.
+ -triplet = byte, byte, byte.
+
+ DV_curr_date_raw>dvk = +'02FD3A'.
+ DV_total_raw>dvk = +'03FD3A'.
+ DV_hist0_raw>dvk = +'8200FD3A'.
+ DV_hist1_raw>dvk = +'8201FD3A'.
+ DV_hist2_raw>dvk = +'8202FD3A'.
+ DV_hist3_raw>dvk = +'8203FD3A'.
+ DV_hist4_raw>dvk = +'8204FD3A'.
+ DV_hist5_raw>dvk = +'8205FD3A'.
+ DV_hist6_raw>dvk = +'8206FD3A'.
+ DV_hist7_raw>dvk = +'8207FD3A'.
+ DV_hist8_raw>dvk = +'8208FD3A'.
+ DV_hist9_raw>dvk = +'8209FD3A'.
+ DV_hist10_raw>dvk = +'820AFD3A'.
+ DV_hist11_raw>dvk = +'820BFD3A'."
+ }
+ }
+ tests {
+ test {
+ comment = 'Legacy payload'
+ args = 'TCH mkradio3a 62560642 NOKEY'
+ telegram = 36446850420656625072A2_0C007C3110250000293400373A002E38000E15002F37003A39003835002F24003930001D2500312500162900
+ json = '{"_":"telegram","media":"cold water","driver":"mkradio3a","name":"TCH","id":"62560642","target_m3":9.3,"last_apr_m3":8.3,"last_aug_m3":3.5,"last_dec_m3":6.3,"last_feb_m3":6.6,"last_jan_m3":8.6,"last_jul_m3":10.2,"last_jun_m3":11.5,"last_mar_m3":10.5,"last_may_m3":10.9,"last_nov_m3":9.3,"last_oct_m3":11.3,"last_sep_m3":10.2,"total_m3":948.8,"target_date":"2024-11-28","timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'TCH;62560642;948.8;9.3;1111-11-11 11:11.11'
+ }
+ }
+}
diff --git a/components/wmbus_common/drivers/src/mkradio4.xmq b/components/wmbus_common/drivers/src/mkradio4.xmq
new file mode 100644
index 00000000..58661b94
--- /dev/null
+++ b/components/wmbus_common/drivers/src/mkradio4.xmq
@@ -0,0 +1,89 @@
+// Copyright (C) 2026 Fredrik Öhrström (gpl-3.0-or-later)
+driver {
+ name = mkradio4
+ meter_type = WaterMeter
+ default_fields = name,id,total_m3,target_m3,timestamp
+ manufacturer = Techem
+ detect {
+ mvt = TCH,95,62
+ mvt = TCH,70,62
+ mvt = TCH,95,72
+ mvt = TCH,70,72
+ }
+ fields {
+ field {
+ name = prev_raw
+ quantity = Dimensionless
+ attributes = HIDE
+ match {
+ difvifkey = 4215
+ }
+ }
+ field {
+ name = curr_raw
+ quantity = Dimensionless
+ attributes = HIDE
+ match {
+ difvifkey = 0215
+ }
+ }
+ field {
+ name = prev
+ quantity = Volume
+ attributes = HIDE
+ calculate = 'prev_raw_counter * 1m3'
+ }
+ field {
+ name = curr
+ quantity = Volume
+ attributes = HIDE
+ calculate = 'curr_raw_counter * 1m3'
+ }
+ field {
+ name = target
+ quantity = Volume
+ info = 'The total water consumption recorded at the beginning of this month.'
+ calculate = prev_m3
+ }
+ field {
+ name = total
+ quantity = Volume
+ info = 'The total water consumption recorded by this meter.'
+ calculate = 'prev_m3 + curr_m3'
+ }
+ field {
+ name = mfct_specific_data
+ quantity = Text
+ attributes = HIDE
+ match_entire_payload = true
+ ixml = "decode = frame.
+ frame = byte,
+ byte,
+ byte,
+ Prev,
+ byte,
+ byte,
+ Curr,
+ byte*.
+
+ Prev = word, @DV_prev.
+ Curr = word, @DV_curr.
+
+ -hex = ['A'-'F';'0'-'9'].
+ -byte = hex, hex.
+ -word = byte, byte.
+
+ DV_prev>dvk = +'4215'.
+ DV_curr>dvk = +'0215'."
+ }
+ }
+ tests {
+ test {
+ comment = 'Legacy payload'
+ args = 'Duschagain mkradio4 02410120 NOKEY'
+ telegram = 2F446850200141029562A2_06702901006017030004000300000000000000000000000000000000000000000000000000
+ json = '{"_":"telegram","media":"warm water","driver":"mkradio4","name":"Duschagain","id":"02410120","total_m3":0.4,"target_m3":0.1,"timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'Duschagain;02410120;0.4;0.1;1111-11-11 11:11.11'
+ }
+ }
+}
diff --git a/components/wmbus_common/drivers/src/mkradio4a.xmq b/components/wmbus_common/drivers/src/mkradio4a.xmq
new file mode 100644
index 00000000..65e1db29
--- /dev/null
+++ b/components/wmbus_common/drivers/src/mkradio4a.xmq
@@ -0,0 +1,54 @@
+// Copyright (C) 2017-2022 Fredrik Öhrström (gpl-3.0-or-later)
+// Diehl MK Radio 4a water meter.
+driver {
+ name = mkradio4a
+ meter_type = WaterMeter
+ default_fields = name,id,target_m3,target_date,timestamp
+ detect {
+ mvt = HYD,fe,06
+ mvt = TCH,95,37
+ }
+ fields {
+ field {
+ name = target
+ quantity = Volume
+ info = 'The total water consumption recorded at the end of previous year.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ measurement_type = Instantaneous
+ vif_range = Volume
+ storage_nr = 1
+ }
+ }
+ field {
+ name = target
+ quantity = PointInTime
+ info = 'Date when previous year ended.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ display_unit = DateLT
+ match {
+ measurement_type = Instantaneous
+ vif_range = Date
+ storage_nr = 1
+ }
+ }
+ }
+ tests {
+ test {
+ args = 'FOO mkradio4a 66953825 NOKEY'
+ comment = 'Warm water'
+ telegram = 4B44685036494600953772253895662423FE064E0030052F2F_4315A10000426CBF2C0F542CF2DD8BEC869511B2DB8301C3ABA390FB4FDB6F1144DA1F3897DD55F2AD0D194F68510FF8FADFB9
+ json = '{"_":"telegram","media":"warm water","driver":"mkradio4a","name":"FOO","id":"66953825","target_m3":16.1,"target_date":"2021-12-31","timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'FOO;66953825;16.1;2021-12-31;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'BAR mkradio4a 01770002 NOKEY'
+ comment = 'Cold water'
+ telegram = 4B4468508644710095377202007701A85CFE078A0030052F2F_4315F00200426CBF2C0FEE456BF6F802216503E25EB73E9377D54F672681B76C469696E4C7BCCC9072CC79F712360FC3F57D85
+ json = '{"_":"telegram","media":"water","driver":"mkradio4a","name":"BAR","id":"01770002","target_m3":75.2,"target_date":"2021-12-31","timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'BAR;01770002;75.2;2021-12-31;1111-11-11 11:11.11'
+ }
+ }
+}
diff --git a/components/wmbus_common/drivers/src/munia.xmq b/components/wmbus_common/drivers/src/munia.xmq
new file mode 100644
index 00000000..d0e3d5ac
--- /dev/null
+++ b/components/wmbus_common/drivers/src/munia.xmq
@@ -0,0 +1,61 @@
+// Copyright (C) 2019-2022 Fredrik Öhrström (gpl-3.0-or-later)
+driver {
+ name = munia
+ meter_type = TempHygroMeter
+ default_fields = name,id,current_temperature_c,current_relative_humidity_rh,timestamp
+ detect {
+ mvt = WEP,02,1b
+ mvt = WEP,04,1b
+ }
+ fields {
+ field {
+ name = status
+ quantity = Text
+ info = 'Meter status. Reports OK if neither tpl sts nor error flags have bits set.'
+ attributes = STATUS,INCLUDE_TPL_STATUS
+ match {
+ difvifkey = 02FD971D
+ }
+ lookup {
+ name = ERROR_FLAGS
+ map_type = BitToString
+ mask_bits = 0xffff
+ default_message = OK
+ }
+ }
+ field {
+ name = current_temperature
+ quantity = Temperature
+ info = 'The current temperature.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = ExternalTemperature
+ }
+ }
+ field {
+ name = current_relative_humidity
+ quantity = RelativeHumidity
+ info = 'The current relative humidity.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = RelativeHumidity
+ }
+ }
+ }
+ tests {
+ test {
+ args = 'TempoHygro munia 00013482 NOKEY'
+ comment = 'Munia version 02.'
+ telegram = 2E44B05C82340100021B7A460000002F2F0A6601020AFB1A570602FD971D00002F2F2F2F2F2F2F2F2F2F2F2F2F2F2F
+ json = '{"_":"telegram","media":"room sensor","driver":"munia","name":"TempoHygro","id":"00013482","status":"OK","current_temperature_c":20.1,"current_relative_humidity_rh":65.7,"timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'TempoHygro;00013482;20.1;65.7;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'TempoHygro munia 00220111 NOKEY'
+ comment = 'Robin version 04.'
+ telegram = 2E44B05C11012200041B7A2B0000002F2F0A6617020AFB1A100602FD971D00002F2F2F2F2F2F2F2F2F2F2F2F2F2F2F
+ json = '{"_":"telegram","media":"room sensor","driver":"munia","name":"TempoHygro","id":"00220111","status":"OK","current_temperature_c":21.7,"current_relative_humidity_rh":61,"timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'TempoHygro;00220111;21.7;61;1111-11-11 11:11.11'
+ }
+ }
+}
diff --git a/components/wmbus_common/drivers/src/nemo.xmq b/components/wmbus_common/drivers/src/nemo.xmq
new file mode 100644
index 00000000..97e1f241
--- /dev/null
+++ b/components/wmbus_common/drivers/src/nemo.xmq
@@ -0,0 +1,375 @@
+// Copyright (C) 2023 Fredrik Öhrström (gpl-3.0-or-later)
+driver {
+ name = nemo
+ meter_type = ElectricityMeter
+ default_fields = name,id,status,total_active_positive_3phase_kwh,active_positive_3phase_kw,timestamp
+ link_mode = MBUS
+ detect {
+ mvt = IME,1d,02
+ }
+ fields {
+ // Telegram 1 fields
+ field {
+ name = total_active_positive_3phase
+ quantity = Energy
+ info = 'Et+ the total 3-phase active positive energy.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyEnergyVIF
+ storage_nr = 0
+ tariff_nr = 1
+ subunit_nr = 1
+ }
+ }
+ field {
+ name = active_positive_3phase
+ quantity = Power
+ info = 'P+ the 3-phase active positive power.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyPowerVIF
+ storage_nr = 0
+ tariff_nr = 1
+ subunit_nr = 1
+ }
+ }
+ field {
+ name = total_reactive_positive_3phase
+ quantity = Energy
+ info = 'Er+ the total 3-phase reactive positive energy.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyEnergyVIF
+ storage_nr = 0
+ tariff_nr = 1
+ subunit_nr = 2
+ }
+ }
+ field {
+ name = reactive_positive_3phase
+ quantity = Power
+ info = 'Q+ the 3-phase reactive positive power.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyPowerVIF
+ storage_nr = 0
+ tariff_nr = 1
+ subunit_nr = 2
+ }
+ }
+ field {
+ name = total_active_partial_3phase
+ quantity = Energy
+ info = 'Part Et+ the total 3-phase active partial energy.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyEnergyVIF
+ storage_nr = 0
+ tariff_nr = 2
+ subunit_nr = 1
+ }
+ }
+ field {
+ name = active_negative_3phase
+ quantity = Power
+ info = 'P- the 3-phase active negative power.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyPowerVIF
+ storage_nr = 0
+ tariff_nr = 2
+ subunit_nr = 1
+ }
+ }
+ field {
+ name = total_reactive_partial_3phase
+ quantity = Energy
+ info = 'Part Er+ the total 3-phase reactive partial energy.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyEnergyVIF
+ storage_nr = 0
+ tariff_nr = 2
+ subunit_nr = 2
+ }
+ }
+ field {
+ name = reactive_negative_3phase
+ quantity = Power
+ info = 'Q- the 3-phase reactive negative power.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyPowerVIF
+ storage_nr = 0
+ tariff_nr = 2
+ subunit_nr = 2
+ }
+ }
+ field {
+ name = power
+ quantity = Dimensionless
+ info = 'PF the power factor.'
+ display_unit = factor
+ match {
+ measurement_type = Instantaneous
+ vif_range = Dimensionless
+ }
+ }
+ field {
+ name = status
+ quantity = Text
+ info = 'Status. OK if no error flags are set.'
+ attributes = STATUS,INCLUDE_TPL_STATUS
+ match {
+ measurement_type = Instantaneous
+ vif_range = ErrorFlags
+ }
+ lookup {
+ name = ERROR_FLAGS
+ map_type = BitToString
+ mask_bits = 0xff
+ default_message = OK
+ }
+ }
+ // Telegram 2 fields
+ field {
+ name = current_at_phase_1
+ quantity = Amperage
+ info = 'I1 Amperage for L1 phase.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = Amperage
+ add_combinable = Mfct01
+ }
+ }
+ field {
+ name = current_at_phase_2
+ quantity = Amperage
+ info = 'I2 Amperage for L2 phase.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = Amperage
+ add_combinable = Mfct02
+ }
+ }
+ field {
+ name = current_at_phase_3
+ quantity = Amperage
+ info = 'I3 Amperage for L3 phase.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = Amperage
+ add_combinable = Mfct03
+ }
+ }
+ field {
+ name = voltage_at_phase_1
+ quantity = Voltage
+ info = 'L1-N Voltage for L1 phase.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = Voltage
+ add_combinable = Mfct01
+ }
+ }
+ field {
+ name = voltage_at_phase_2
+ quantity = Voltage
+ info = 'L2-N Voltage for L2 phase.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = Voltage
+ add_combinable = Mfct02
+ }
+ }
+ field {
+ name = voltage_at_phase_3
+ quantity = Voltage
+ info = 'L3-N Voltage for L3 phase.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = Voltage
+ add_combinable = Mfct03
+ }
+ }
+ // Telegram 3 fields
+ field {
+ name = active_power_at_phase_1
+ quantity = Power
+ info = 'P1 Power for L1 phase.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyPowerVIF
+ subunit_nr = 1
+ add_combinable = Mfct01
+ }
+ }
+ field {
+ name = active_power_at_phase_2
+ quantity = Power
+ info = 'P2 Power for L2 phase.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyPowerVIF
+ subunit_nr = 1
+ add_combinable = Mfct02
+ }
+ }
+ field {
+ name = active_power_at_phase_3
+ quantity = Power
+ info = 'P3 Power for L3 phase.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyPowerVIF
+ subunit_nr = 1
+ add_combinable = Mfct03
+ }
+ }
+ field {
+ name = reactive_power_at_phase_1
+ quantity = Power
+ info = 'Q1 Power for L1 phase.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyPowerVIF
+ subunit_nr = 2
+ add_combinable = Mfct01
+ }
+ }
+ field {
+ name = reactive_power_at_phase_2
+ quantity = Power
+ info = 'Q2 Power for L2 phase.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyPowerVIF
+ subunit_nr = 2
+ add_combinable = Mfct02
+ }
+ }
+ field {
+ name = reactive_power_at_phase_3
+ quantity = Power
+ info = 'Q3 Power for L3 phase.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyPowerVIF
+ subunit_nr = 2
+ add_combinable = Mfct03
+ }
+ }
+ field {
+ name = at_phase_1_power
+ quantity = Dimensionless
+ info = 'PF1 the power factor for L1 phase.'
+ display_unit = factor
+ match {
+ measurement_type = Instantaneous
+ vif_range = Dimensionless
+ add_combinable = Mfct01
+ }
+ }
+ field {
+ name = at_phase_2_power
+ quantity = Dimensionless
+ info = 'PF2 the power factor for L2 phase.'
+ display_unit = factor
+ match {
+ measurement_type = Instantaneous
+ vif_range = Dimensionless
+ add_combinable = Mfct02
+ }
+ }
+ field {
+ name = at_phase_3_power
+ quantity = Dimensionless
+ info = 'PF3 the power factor for L3 phase.'
+ display_unit = factor
+ match {
+ measurement_type = Instantaneous
+ vif_range = Dimensionless
+ add_combinable = Mfct03
+ }
+ }
+ field {
+ name = voltage_l1_l2
+ quantity = Voltage
+ info = 'L1-L2 Voltage between phases.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = Voltage
+ add_combinable = Mfct04
+ }
+ }
+ field {
+ name = voltage_l2_l3
+ quantity = Voltage
+ info = 'L2-L3 Voltage between phases.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = Voltage
+ add_combinable = Mfct05
+ }
+ }
+ field {
+ name = voltage_l3_l1
+ quantity = Voltage
+ info = 'L3-L1 Voltage between phases.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = Voltage
+ add_combinable = Mfct06
+ }
+ }
+ field {
+ name = current_in_neutral
+ quantity = Amperage
+ info = 'I Neutral amperage.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = Amperage
+ add_combinable = Mfct04
+ }
+ }
+ field {
+ name = raw_frequency
+ quantity = Frequency
+ info = 'Frequency in 0.1 Hz'
+ attributes = HIDE
+ vif_scaling = None
+ match {
+ difvifkey = 05FF5A
+ }
+ }
+ field {
+ name = frequency
+ quantity = Frequency
+ info = 'Frequency of AC.'
+ calculate = 'raw_frequency_hz / 10 counter'
+ }
+ }
+ tests {
+ test {
+ args = 'Elen nemo 00067609 NOKEY'
+ comment = 'Telegram 1'
+ telegram = 6864646808657209760600A5251D02000000008E500400355873060085502B0013BF478E9040040029452501008590402B00F800468E600400000000000085602B000000008EA0400400890000000085A0402B0000000005FD3ADCF97E3F01FD17001F00000000009116
+ json = '{"_":"telegram","media":"electricity","driver":"nemo","name":"Elen","id":"00067609","total_active_positive_3phase_kwh":6735835,"active_positive_3phase_kw":97.83,"total_reactive_positive_3phase_kwh":1254529,"reactive_positive_3phase_kw":8.254,"total_active_partial_3phase_kwh":0,"active_negative_3phase_kw":0,"total_reactive_partial_3phase_kwh":89,"reactive_negative_3phase_kw":0,"power_factor":0.996,"status":"OK","frequency_hz":null,"timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'Elen;00067609;OK;6735835;97.83;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'Elen nemo 00067609 NOKEY'
+ comment = 'Telegram 2, this is decoded by itself in this test so the default fields become null because they are sent in the previous telegram.'
+ telegram = 684B4B6808657209760600A5251D020100000005FDD9FF0100F4174805FDD9FF0200B8084805FDD9FF0300B0014805FDC8FF0100C0104505FDC8FF020040114505FDC8FF03005011451F00000000000716
+ json = '{"_":"telegram","media":"electricity","driver":"nemo","name":"Elen","id":"00067609","status":"OK","current_at_phase_1_a":155.6,"current_at_phase_2_a":140,"current_at_phase_3_a":132.8,"voltage_at_phase_1_v":231.6,"voltage_at_phase_2_v":232.4,"voltage_at_phase_3_v":232.5,"frequency_hz":null,"timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'Elen;00067609;OK;null;null;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'Elen nemo 00067609 NOKEY'
+ comment = 'Telegram 3, this is also decoded by itself in this test, thus the null default field values.'
+ telegram = 689E9E6808657209760600A5251D02020000008540ABFF0100360B478540ABFF02002CFA468540ABFF030074ED46858040ABFF0100C0E244858040ABFF0200405A45858040ABFF030060364505FDBAFF0178BE7F3F05FDBAFF0240357E3F05FDBAFF0353B87E3F05FDC8FF0400907A4505FDC8FF0500707B4505FDC8FF0600807B4505FDD9FF0400502A4705FF5A0000FA4302FD3AC80002FD3A0A000F00000000008B16
+ json = '{"_":"telegram","media":"electricity","driver":"nemo","name":"Elen","id":"00067609","power_factor":200,"status":"OK","active_power_at_phase_1_kw":35.638,"active_power_at_phase_2_kw":32.022,"active_power_at_phase_3_kw":30.394,"reactive_power_at_phase_1_kw":1.814,"reactive_power_at_phase_2_kw":3.492,"reactive_power_at_phase_3_kw":2.918,"at_phase_1_power_factor":0.999,"at_phase_2_power_factor":0.993,"at_phase_3_power_factor":0.995,"voltage_l1_l2_v":400.9,"voltage_l2_l3_v":402.3,"voltage_l3_l1_v":402.4,"current_in_neutral_a":43.6,"frequency_hz":50,"timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'Elen;00067609;OK;null;null;1111-11-11 11:11.11'
+ }
+ }
+}
diff --git a/components/wmbus_common/drivers/src/nzr.xmq b/components/wmbus_common/drivers/src/nzr.xmq
index 86a7a25f..f3d05e1a 100644
--- a/components/wmbus_common/drivers/src/nzr.xmq
+++ b/components/wmbus_common/drivers/src/nzr.xmq
@@ -5,13 +5,12 @@ driver {
meter_type = ElectricityMeter
detect {
mvt = NZR,00,02
- mvt = EMH,00,02
}
fields {
field {
- name = status
- quantity = Text
- info = status_and_error_flags
+ name = status
+ quantity = Text
+ info = status_and_error_flags
attributes = STATUS,INCLUDE_TPL_STATUS
match {
measurement_type = Instantaneous
@@ -179,19 +178,19 @@ driver {
test {
args = 'NZR1 nzr 17911459 NOKEY'
telegram = 684f4f68080B7259149117523B00029200000084002ccB00000084012962fB0000840229e85f010084032905Bf00008201fd49ed008202fd49e9008203fd49ea008201fd5a21018202fd5a8d018203fd5ad700e216
- json = '{"_": "telegram","current_at_phase_1_a": 2.89,"current_at_phase_2_a": 3.97,"current_at_phase_3_a": 2.15,"current_power_consumption_1_kw": 0.64354,"current_power_consumption_2_kw": 0.90088,"current_power_consumption_3_kw": 0.48901,"current_power_consumption_kw": 2.03,"id": "17911459","media": "electricity","meter": "nzr","name": "NZR1","status": "OK","timestamp": "1111-11-11T11:11:11Z","voltage_at_phase_1_v": 237,"voltage_at_phase_2_v": 233,"voltage_at_phase_3_v": 234}'
+ json = '{"_": "telegram","current_at_phase_1_a": 2.89,"current_at_phase_2_a": 3.97,"current_at_phase_3_a": 2.15,"current_power_consumption_1_kw": 0.64354,"current_power_consumption_2_kw": 0.90088,"current_power_consumption_3_kw": 0.48901,"current_power_consumption_kw": 2.03,"id": "17911459","media": "electricity","driver": "nzr","name": "NZR1","status": "OK","timestamp": "1111-11-11T11:11:11Z","voltage_at_phase_1_v": 237,"voltage_at_phase_2_v": 233,"voltage_at_phase_3_v": 234}'
fields = 'NZR1;17911459;OK;null;1111-11-11 11:11.11'
}
test {
args = 'NZR nzr 07911459 NOKEY'
telegram = 685a5a68080B7259149107523B0002580000008c10058576490001fd170084002ccB00000084012962fB0000840229e85f010084032905Bf00008201fd49ed008202fd49e9008203fd49ea008201fd5a21018202fd5a8d018203fd5ad7009216
- json = '{"_":"telegram","media":"electricity","meter":"nzr","name":"NZR","id":"07911459","current_at_phase_1_a":2.89,"current_at_phase_2_a":3.97,"current_at_phase_3_a":2.15,"current_power_consumption_kw":2.03,"current_power_consumption_1_kw":0.64354,"current_power_consumption_2_kw":0.90088,"current_power_consumption_3_kw":0.48901,"total_energy_consumption_kwh":49768.5,"voltage_at_phase_1_v":237,"voltage_at_phase_2_v":233,"voltage_at_phase_3_v":234,"status":"OK","timestamp":"1111-11-11T11:11:11Z"}'
+ json = '{"_":"telegram","media":"electricity","driver":"nzr","name":"NZR","id":"07911459","current_at_phase_1_a":2.89,"current_at_phase_2_a":3.97,"current_at_phase_3_a":2.15,"current_power_consumption_kw":2.03,"current_power_consumption_1_kw":0.64354,"current_power_consumption_2_kw":0.90088,"current_power_consumption_3_kw":0.48901,"total_energy_consumption_kwh":49768.5,"voltage_at_phase_1_v":237,"voltage_at_phase_2_v":233,"voltage_at_phase_3_v":234,"status":"OK","timestamp":"1111-11-11T11:11:11Z"}'
fields = 'NZR;07911459;OK;49768.5;1111-11-11 11:11.11'
}
test {
args = 'NZR nzr 07911459 NOKEY'
telegram = 68222268080B7259149107523B0002580000008c1005857649008400292f6a010001fd17001fd116
- json = '{"_": "telegram","current_power_consumption_kw": 0.92719,"id": "07911459","media": "electricity","meter": "nzr","name": "NZR","status": "OK","timestamp": "1111-11-11T11:11:11Z","total_energy_consumption_kwh": 49768.5}'
+ json = '{"_": "telegram","current_power_consumption_kw": 0.92719,"id": "07911459","media": "electricity","driver": "nzr","name": "NZR","status": "OK","timestamp": "1111-11-11T11:11:11Z","total_energy_consumption_kwh": 49768.5}'
fields = 'NZR;07911459;OK;49768.5;1111-11-11 11:11.11'
}
}
diff --git a/components/wmbus_common/drivers/src/omnipower.xmq b/components/wmbus_common/drivers/src/omnipower.xmq
new file mode 100644
index 00000000..30bdadd8
--- /dev/null
+++ b/components/wmbus_common/drivers/src/omnipower.xmq
@@ -0,0 +1,87 @@
+/* Copyright (C) 2018-2022 Fredrik Öhrström (gpl-3.0-or-later)
+ Copyright (C) 2020 Janus Bo Andersen (gpl-3.0-or-later)
+
+ Implemented October 2020 Janus Bo Andersen:
+ Implements Kamstrup OmniPower, energy meter.
+ This C1 WM-Bus meter broadcasts:
+ - Accumulated energy consumption (A+, kWh)
+ - Accumulated energy production (A-, kWh)
+ - Current power consumption (P+, kW)
+ - Current power production (P-, kW)
+
+ According to Kamstrup doc. 58101496_C1_GB_05.2018
+ (Wireless M-Bus Module for OMNIPOWER), the single-phase,
+ three-phase and CT meters send the same datagram.
+
+ Meter version. Implementation tested against meter:
+ Kamstrup one-phase with firmware version 0x30.
+
+ Meter uses AES-128 in CTR mode, which is the only mode supported by
+ the extended link layer (wm-bus), see EN 13757-4:2019. */
+driver {
+ name = omnipower
+ meter_type = ElectricityMeter
+ default_fields = name,id,total_energy_consumption_kwh,total_energy_production_kwh,current_power_consumption_kw,current_power_production_kw,timestamp
+ detect {
+ mvt = KAM,30,02
+ }
+ compact_frame_formats {
+ difvif = 040404843C042B04AB3C
+ }
+ fields {
+ field {
+ name = total_energy_consumption
+ info = 'The total energy consumption recorded by this meter.'
+ quantity = Energy
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyEnergyVIF
+ }
+ }
+ field {
+ name = total_energy_production
+ info = 'The total energy backward (production) recorded by this meter.'
+ quantity = Energy
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyEnergyVIF
+ add_combinable = BackwardFlow
+ }
+ }
+ field {
+ name = current_power_consumption
+ info = 'The current power consumption.'
+ quantity = Power
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyPowerVIF
+ }
+ }
+ field {
+ name = current_power_production
+ info = 'The current power production.'
+ quantity = Power
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyPowerVIF
+ add_combinable = BackwardFlow
+ }
+ }
+ }
+ tests {
+ test {
+ comment = 'Long Frame'
+ args = 'myomnipower omnipower 32666857 NOKEY'
+ telegram = 2D442D2C5768663230028D20E4E2C81C20878C78_04041A03000004843C00000000042B0300000004AB3C00000000
+ json = '{"_":"telegram","media":"electricity","driver":"omnipower","name":"myomnipower","id":"32666857","total_energy_consumption_kwh":7.94,"total_energy_production_kwh":0,"current_power_consumption_kw":0.003,"current_power_production_kw":0,"timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'myomnipower;32666857;7.94;0;0.003;0;1111-11-11 11:11.11'
+ }
+ test {
+ comment = 'Compact Frame'
+ args = 'myomnipower omnipower 32666857 NOKEY'
+ telegram = 27442D2C5768663230028D20E900C91C2011BA79138CCCFB_1A030000000000000300000000000000
+ json = '{"_":"telegram","media":"electricity","driver":"omnipower","name":"myomnipower","id":"32666857","total_energy_consumption_kwh":7.94,"total_energy_production_kwh":0,"current_power_consumption_kw":0.003,"current_power_production_kw":0,"timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'myomnipower;32666857;7.94;0;0.003;0;1111-11-11 11:11.11'
+ }
+ }
+}
diff --git a/components/wmbus_common/drivers/src/op041a.xmq b/components/wmbus_common/drivers/src/op041a.xmq
new file mode 100644
index 00000000..5f4f1aa4
--- /dev/null
+++ b/components/wmbus_common/drivers/src/op041a.xmq
@@ -0,0 +1,24 @@
+// Copyright (C) 2026 Fredrik Öhrström (gpl-3.0-or-later)
+driver {
+ name = op041a
+ meter_type = WaterMeter
+ default_fields = name,id,total_m3,meter_datetime,timestamp
+ manufacturer = Apator
+ model = op041a
+ detect {
+ mvt = APA,1a,07
+ }
+ library {
+ use = total_m3
+ use = meter_datetime
+ }
+ tests {
+ test {
+ args = 'W op041a 11337834 NOKEY'
+ comment = ''
+ telegram = 41440106347833111A078CC0A27AAB0030852F2F_04132F010000046D332E46320F60000000000869970743F13B375F3124000000240000002F2F2F2F2F2F2F2F2F2F
+ json = '{"_":"telegram","media":"water","driver":"op041a","name":"W","id":"11337834","total_m3":0.303,"meter_datetime":"2026-02-06 14:51","timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'W;11337834;0.303;2026-02-06 14:51;1111-11-11 11:11.11'
+ }
+ }
+}
diff --git a/components/wmbus_common/drivers/src/picoflux.xmq b/components/wmbus_common/drivers/src/picoflux.xmq
new file mode 100644
index 00000000..5eaeef04
--- /dev/null
+++ b/components/wmbus_common/drivers/src/picoflux.xmq
@@ -0,0 +1,80 @@
+// Copyright (C) 2026 Fredrik Öhrström (gpl-3.0-or-later)
+driver {
+ name = picoflux
+ meter_type = WaterMeter
+ default_fields = name,id,status,total_m3,timestamp
+ manufacturer = Ecomess
+ model = 'Picoflux AiR'
+ detect {
+ mvt = ECM,05,07
+ }
+ // Water-leak alarm is carried in the unencrypted TPL status byte
+ // (per the Ecomess/Kaden wM-Bus OMS spec, shared by the Kaden water meters):
+ // bits 0+1 (value 3) -> permanent water-leak alarm (decoded as ALARM by the
+ // standard OMS bits)
+ // bit 5 (0x20) -> during the leak the flow stayed below 100 l/h
+ // bit 6 (0x40) -> during the leak the flow exceeded 100 l/h
+ // (bits 5 and 6 may both be set if the flow varied across the limit)
+ mfct_tpl_status_bits {
+ mask_bits = 0xe0
+ default_message = OK
+ map {
+ name = LEAK_FLOW_LOW
+ value = 0x20
+ test = set
+ }
+ map {
+ name = LEAK_FLOW_HIGH
+ value = 0x40
+ test = set
+ }
+ }
+ library {
+ use = total_m3
+ use = total_backward_m3
+ use = meter_datetime
+ use = actuality_duration_s
+ }
+ fields {
+ field {
+ name = status
+ quantity = Text
+ info = 'Status and error flags.'
+ attributes = STATUS,INCLUDE_TPL_STATUS
+ match {
+ measurement_type = Instantaneous
+ vif_range = ErrorFlags
+ }
+ lookup {
+ name = ERROR_FLAGS
+ map_type = BitToString
+ mask_bits = 0xffff
+ default_message = OK
+ // Help needed. What are the meaning of these bits?
+ }
+ }
+ }
+ tests {
+ test {
+ args = 'MyWater picoflux 56544919 9F5213BC13841410BB1410141515E4D5'
+ comment = 'Ecomess Picoflux AiR, medium consumption (4.492 m3)'
+ telegram = 6e446d141949545605077adf0060055ad21c5cafec3a71c0720754f2ca777e238d0318d35a2de0f8fe592e9a0d0cacbd61dfd2815c21a7fa743d93b5e543847e3e67b5d80816a253db655d4354b1a06c1fb7bae99e0fa322bff37a05fd651c5e3c09968ae4df0d8d911fee33ef31b9
+ json = '{"_":"telegram","media":"water","driver":"picoflux","name":"MyWater","id":"56544919","actuality_duration_s":0,"total_backward_m3":0,"total_m3":4.492,"meter_datetime":"2026-02-09 08:57","status":"OK","timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'MyWater;56544919;OK;4.492;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'MyWater picoflux 57530510 9F5213BC13841410BB1410141515E4D5'
+ comment = 'Ecomess Picoflux AiR, high consumption (10.617 m3)'
+ telegram = 6e446d141005535705077aa900600544f4d6e4edb113cdb888981db36355614215a48c813a08dea801b46bd5612fa0bea01763ceed5fa0e0f44b8b7f983f08f6cc6694d63c56a6edc6498978373f1c71430167bc360144f26cfa8d06fe41f05cbc1d2b463dfa696f7a36a85e964f4f
+ json = '{"_":"telegram","media":"water","driver":"picoflux","name":"MyWater","id":"57530510","actuality_duration_s":0,"total_backward_m3":0,"total_m3":10.617,"meter_datetime":"2026-02-09 08:57","status":"OK","timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'MyWater;57530510;OK;10.617;1111-11-11 11:11.11'
+ }
+ test {
+ comment = 'Water-leak alarm: TPL status byte set to 0x63 (synthetic, from the 4.492 m3 telegram) = leak with flow both below and above the 100 l/h limit'
+ args = 'MyWater picoflux 56544919 9F5213BC13841410BB1410141515E4D5'
+ telegram = 6e446d141949545605077adf6360055ad21c5cafec3a71c0720754f2ca777e238d0318d35a2de0f8fe592e9a0d0cacbd61dfd2815c21a7fa743d93b5e543847e3e67b5d80816a253db655d4354b1a06c1fb7bae99e0fa322bff37a05fd651c5e3c09968ae4df0d8d911fee33ef31b9
+ json = '{"_":"telegram","media":"water","driver":"picoflux","name":"MyWater","id":"56544919","actuality_duration_s":0,"total_backward_m3":0,"total_m3":4.492,"meter_datetime":"2026-02-09 08:57","status":"ALARM LEAK_FLOW_HIGH LEAK_FLOW_LOW","timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'MyWater;56544919;ALARM LEAK_FLOW_HIGH LEAK_FLOW_LOW;4.492;1111-11-11 11:11.11'
+ }
+ }
+}
diff --git a/components/wmbus_common/drivers/src/piigth.xmq b/components/wmbus_common/drivers/src/piigth.xmq
new file mode 100644
index 00000000..767e3b4a
--- /dev/null
+++ b/components/wmbus_common/drivers/src/piigth.xmq
@@ -0,0 +1,120 @@
+// Copyright (C) 2021-2022 Fredrik Öhrström (gpl-3.0-or-later)
+driver {
+ name = piigth
+ meter_type = TempHygroMeter
+ default_fields = name,id,status,temperature_c,relative_humidity_rh,timestamp
+ detect {
+ mvt = PII,01,1b
+ }
+ library {
+ use = fabrication_no,software_version
+ }
+ fields {
+ field {
+ name = status
+ quantity = Text
+ info = 'Meter status from tpl status field.'
+ attributes = STATUS,INCLUDE_TPL_STATUS
+ match {
+ measurement_type = Instantaneous
+ vif_range = ErrorFlags
+ }
+ lookup {
+ name = ERROR_FLAGS
+ map_type = BitToString
+ mask_bits = 0xffffffff
+ default_message = OK
+ }
+ }
+ field {
+ name = temperature
+ quantity = Temperature
+ info = 'The current temperature.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ attributes = REQUIRED
+ match {
+ measurement_type = Instantaneous
+ vif_range = ExternalTemperature
+ }
+ }
+ field {
+ name = average_temperature_1h
+ quantity = Temperature
+ info = 'The average temperature over the last hour.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ attributes = REQUIRED
+ match {
+ measurement_type = Instantaneous
+ vif_range = ExternalTemperature
+ storage_nr = 1
+ }
+ }
+ field {
+ name = average_temperature_24h
+ quantity = Temperature
+ info = 'The average temperature over the last 24 hours.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ attributes = REQUIRED
+ match {
+ measurement_type = Instantaneous
+ vif_range = ExternalTemperature
+ storage_nr = 2
+ }
+ }
+ field {
+ name = relative_humidity
+ quantity = RelativeHumidity
+ info = 'The current relative humidity.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ attributes = REQUIRED
+ match {
+ measurement_type = Instantaneous
+ vif_range = RelativeHumidity
+ }
+ }
+ field {
+ name = relative_humidity_1h
+ quantity = RelativeHumidity
+ info = 'The average relative humidity over the last hour.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ attributes = REQUIRED
+ match {
+ measurement_type = Instantaneous
+ vif_range = RelativeHumidity
+ storage_nr = 1
+ }
+ }
+ field {
+ name = relative_humidity_24h
+ quantity = RelativeHumidity
+ info = 'The average relative humidity over the last 24 hours.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ attributes = REQUIRED
+ match {
+ measurement_type = Instantaneous
+ vif_range = RelativeHumidity
+ storage_nr = 2
+ }
+ }
+ }
+ tests {
+ test {
+ args = 'Tempo piigth 10000284 NOKEY'
+ telegram = 68383868080072840200102941011B04000000_0265C0094265A509B20165000002FB1A900142FB1A6901B201FB1A00000C788402001002FD0F21000FC016
+ json = '{"_":"telegram","media":"room sensor","driver":"piigth","name":"Tempo","id":"10000284","fabrication_no":"10000284","software_version":"0021","status":"OK","temperature_c":24.96,"average_temperature_1h_c":24.69,"relative_humidity_rh":40,"relative_humidity_1h_rh":36.1,"timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'Tempo;10000284;OK;24.96;40;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'Tempo piigth 10000284 NOKEY'
+ telegram = 68383868080072840200102941011B06000000_02653F0A4265000A820165CA0902FB1A4F0142FB1A53018201FB1A5E010C788402001002FD0F21000F1916
+ json = '{"_":"telegram","media":"room sensor","driver":"piigth","name":"Tempo","id":"10000284","fabrication_no":"10000284","software_version":"0021","status":"OK","temperature_c":26.23,"average_temperature_1h_c":25.6,"average_temperature_24h_c":25.06,"relative_humidity_rh":33.5,"relative_humidity_1h_rh":33.9,"relative_humidity_24h_rh":35,"timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'Tempo;10000284;OK;26.23;33.5;1111-11-11 11:11.11'
+ }
+ }
+}
diff --git a/components/wmbus_common/drivers/src/pollucomf.xmq b/components/wmbus_common/drivers/src/pollucomf.xmq
new file mode 100644
index 00000000..abdee6c0
--- /dev/null
+++ b/components/wmbus_common/drivers/src/pollucomf.xmq
@@ -0,0 +1,168 @@
+// Copyright (C) 2022 Fredrik Öhrström (gpl-3.0-or-later)
+driver {
+ name = pollucomf
+ meter_type = HeatMeter
+ default_fields = name,id,status,total_kwh,total_m3,target_kwh,target_m3,timestamp
+ detect {
+ mvt = SEN,1d,04
+ }
+ library {
+ use = operating_time_h
+ use = on_time_h
+ use = on_time_at_error_h
+ use = meter_datetime
+ }
+ fields {
+ field {
+ name = status
+ quantity = Text
+ info = 'Meter status.'
+ attributes = STATUS,INCLUDE_TPL_STATUS
+ }
+ field {
+ name = total
+ quantity = Energy
+ info = 'The total energy consumption recorded by this meter.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyEnergyVIF
+ }
+ }
+ field {
+ name = total
+ quantity = Volume
+ info = 'The total amount of water that has passed through this meter.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ measurement_type = Instantaneous
+ vif_range = Volume
+ }
+ }
+ field {
+ name = power
+ quantity = Power
+ info = 'The active power consumption.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyPowerVIF
+ }
+ }
+ field {
+ name = flow
+ quantity = Flow
+ info = 'The flow of water.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ measurement_type = Instantaneous
+ vif_range = VolumeFlow
+ }
+ }
+ field {
+ name = forward
+ quantity = Temperature
+ info = 'The forward temperature of the water.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ measurement_type = Instantaneous
+ vif_range = FlowTemperature
+ }
+ }
+ field {
+ name = return
+ quantity = Temperature
+ info = 'The return temperature of the water.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ measurement_type = Instantaneous
+ vif_range = ReturnTemperature
+ }
+ }
+ field {
+ name = target
+ quantity = PointInTime
+ info = 'The most recent billing period date.'
+ display_unit = DateLT
+ match {
+ measurement_type = Instantaneous
+ vif_range = Date
+ storage_nr = 1
+ }
+ }
+ field {
+ name = target
+ quantity = Energy
+ info = 'The energy consumption recorded by this meter at the set date.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyEnergyVIF
+ storage_nr = 1
+ }
+ }
+ field {
+ name = target
+ quantity = Volume
+ info = 'The amount of water that had passed through this meter at the set date.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ measurement_type = Instantaneous
+ vif_range = Volume
+ storage_nr = 1
+ }
+ }
+ field {
+ name = forward_max
+ quantity = Temperature
+ info = 'The maximum forward temperature of the water.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ measurement_type = Maximum
+ vif_range = FlowTemperature
+ storage_nr = 1
+ }
+ }
+ field {
+ name = return_max
+ quantity = Temperature
+ info = 'The maximum return temperature of the water.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ measurement_type = Maximum
+ vif_range = ReturnTemperature
+ storage_nr = 1
+ }
+ }
+ field {
+ name = flow_max
+ quantity = Flow
+ info = 'The maximum forward flow of water through this meter.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ measurement_type = Maximum
+ vif_range = VolumeFlow
+ storage_nr = 1
+ }
+ }
+ }
+ tests {
+ test {
+ args = 'Heat pollucomf 14175439 NOKEY'
+ telegram = 5e44ae4c395417141d047a9f0050252f2f046d2d32d92c0223B701040600000000041331000000032B000000033B000000025a3201025ef2003222000002fd170000426cBf2c440600000000441301000000525a0000525e0000533B000000
+ json = '{"_":"telegram","media":"heat","driver":"pollucomf","name":"Heat","id":"14175439","status":"OK","total_kwh":0,"total_m3":0.049,"power_kw":0,"flow_m3h":0,"forward_c":30.6,"return_c":24.2,"target_date":"2021-12-31","target_kwh":0,"target_m3":0.001,"forward_max_c":0,"return_max_c":0,"flow_max_m3h":0,"on_time_h":10536,"on_time_at_error_h":0,"meter_datetime":"2022-12-25 18:45","timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'Heat;14175439;OK;0;0.049;0;0.001;1111-11-11 11:11.11'
+ }
+ }
+}
diff --git a/components/wmbus_common/drivers/src/q400.xmq b/components/wmbus_common/drivers/src/q400.xmq
new file mode 100644
index 00000000..763b3839
--- /dev/null
+++ b/components/wmbus_common/drivers/src/q400.xmq
@@ -0,0 +1,133 @@
+// Copyright (C) 2020-2022 Fredrik Öhrström (gpl-3.0-or-later)
+driver {
+ name = q400
+ meter_type = WaterMeter
+ default_fields = name,id,total_m3,timestamp
+ detect {
+ mvt = AXI,01,07
+ mvt = AXI,10,07
+ mvt = AXI,20,07
+ }
+ library {
+ use = meter_datetime
+ use = on_time_h
+ use = total_m3
+ use = total_forward_m3
+ use = total_backward_m3
+ use = flow_temperature_c
+ use = volume_flow_m3h
+ }
+ fields {
+ field {
+ name = status
+ quantity = Text
+ info = 'Status and error flags.'
+ attributes = STATUS,INCLUDE_TPL_STATUS
+ }
+ field {
+ name = set
+ quantity = PointInTime
+ info = 'Date and time when the previous billing period ended.'
+ display_unit = datetime
+ match {
+ measurement_type = Instantaneous
+ vif_range = DateTime
+ storage_nr = 1
+ }
+ }
+ field {
+ name = consumption_at_set_date
+ quantity = Volume
+ info = 'The total water consumption at the end of the previous billing period.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = Volume
+ storage_nr = 1
+ }
+ }
+ field {
+ name = forward_at_set_date
+ quantity = Volume
+ info = 'The total media volume flowing forward at the end of previous billing period.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = Volume
+ storage_nr = 1
+ add_combinable = ForwardFlow
+ }
+ }
+ field {
+ name = backward_at_set_date
+ quantity = Volume
+ info = 'The total media volume flowing backward at the end of the previous billing period.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = Volume
+ storage_nr = 1
+ add_combinable = BackwardFlow
+ }
+ }
+ field {
+ name = error_free_time
+ quantity = Time
+ info = 'How long the meter has been running without recording an error.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = OperatingTime
+ }
+ }
+ field {
+ name = error_code
+ quantity = Dimensionless
+ info = 'Instantaneous error code, as documented by Axioma. Zero means no error.'
+ vif_scaling = None
+ match {
+ difvifkey = 34FD17
+ }
+ }
+ field {
+ name = battery
+ quantity = Dimensionless
+ info = 'Percentage of battery remaining.'
+ vif_scaling = None
+ display_unit = pct
+ match {
+ difvifkey = 01FD74
+ }
+ }
+ }
+ tests {
+ test {
+ args = 'Q400Water q400 72727272 NOKEY'
+ telegram = 2E4409077272727210077AD71020052F2F_046D040D742C041377000000446D0000612C4413000000002F2F2F2F2F2F
+ json = '{"_":"telegram","media":"water","driver":"q400","name":"Q400Water","id":"72727272","meter_datetime":"2019-12-20 13:04","total_m3":0.119,"status":"TEMPORARY_ERROR","set_datetime":"2019-12-01 00:00","consumption_at_set_date_m3":0,"timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'Q400Water;72727272;0.119;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'AxiomaWater q400 72727273 NOKEY'
+ comment = 'Test Axioma W1 telegram with additional fields compared to the older q400 meter.'
+ telegram = 5E4409077372727210077A710050052F2F_046D0110A92704130022000004933B0000000004933C00000000023B000002592A0A446D0000A12744130000000044933B0000000044933C0000000001FD74622F2F2F2F2F2F2F2F2F2F2F2F2F2F
+ json = '{"_":"telegram","media":"water","driver":"q400","name":"AxiomaWater","id":"72727273","meter_datetime":"2021-07-09 16:01","total_m3":8.704,"total_forward_m3":0,"total_backward_m3":0,"flow_temperature_c":26.02,"volume_flow_m3h":0,"status":"OK","set_datetime":"2021-07-01 00:00","consumption_at_set_date_m3":0,"forward_at_set_date_m3":0,"backward_at_set_date_m3":0,"battery_pct":98,"timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'AxiomaWater;72727273;8.704;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'Mm q400 05829163 NOKEY'
+ telegram = 544409076391820510077ABF100000046D2A0DC62C0420E80F430104130000000004933B0000000004933C00000000023B00000259F0D8446D0000C12C44130000000044933B0000000044933C0000000001FD7461
+ json = '{"_":"telegram","backward_at_set_date_m3":0,"battery_pct":97,"consumption_at_set_date_m3":0,"flow_temperature_c":-100,"forward_at_set_date_m3":0,"id":"05829163","media":"water","driver":"q400","meter_datetime":"2022-12-06 13:42","name":"Mm","on_time_h":5881.166667,"set_datetime":"2022-12-01 00:00","status":"TEMPORARY_ERROR","timestamp":"1111-11-11T11:11:11Z","total_backward_m3":0,"total_forward_m3":0,"total_m3":0,"volume_flow_m3h":0}'
+ fields = 'Mm;05829163;0;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'AxiomaW1v20 q400 10221910 NOKEY'
+ comment = 'Test Axioma W1 telegram with dll-version 0x20, dry/empty spool status.'
+ telegram = 8C4409071019221020077A56100000046D3008423804209081A20004130000000004933B0000000004933C00000000023B00000259F0D8446D0000413844130000000044933B0000000044933C00000000C401130000000034FD17010000000424407DA20001FD74630F0200000000000000000000000041380000000000000000000000000000000000000000
+ json = '{"_":"telegram","media":"water","driver":"q400","name":"AxiomaW1v20","id":"10221910","backward_at_set_date_m3":0,"battery_pct":99,"consumption_at_set_date_m3":0,"error_code_counter":1,"error_free_time_h":2958.026667,"flow_temperature_c":-100,"forward_at_set_date_m3":0,"on_time_h":2958.333333,"set_datetime":"2026-08-01 00:00","total_m3":0,"total_backward_m3":0,"total_forward_m3":0,"volume_flow_m3h":0,"meter_datetime":"2026-08-02 08:48","status":"TEMPORARY_ERROR","timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'AxiomaW1v20;10221910;0;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'MM q400 06289748 NOKEY'
+ telegram = 614409074897280601077AD1000000046D2C173B370420580193040413FB94000004933BFB94000004933C01000000023B02000259E306446D000021374413C08A000044933BC08A000044933C0100000034FD17000000000424BA01930401FD745B
+ json = '{"_":"telegram","backward_at_set_date_m3":0.001,"battery_pct":91,"consumption_at_set_date_m3":35.52,"error_code_counter":0,"error_free_time_h":21317.527222,"flow_temperature_c":17.63,"forward_at_set_date_m3":35.52,"id":"06289748","media":"water","driver":"q400","meter_datetime":"2025-07-27 23:44","name":"MM","on_time_h":21317.5,"set_datetime":"2025-07-01 00:00","status":"OK","timestamp":"1111-11-11T11:11:11Z","total_backward_m3":0.001,"total_forward_m3":38.139,"total_m3":38.139,"volume_flow_m3h":0.002}'
+ fields = 'MM;06289748;38.139;1111-11-11 11:11.11'
+ }
+ }
+}
diff --git a/components/wmbus_common/drivers/src/qcaloric.xmq b/components/wmbus_common/drivers/src/qcaloric.xmq
new file mode 100644
index 00000000..67f31609
--- /dev/null
+++ b/components/wmbus_common/drivers/src/qcaloric.xmq
@@ -0,0 +1,247 @@
+// Copyright (C) 2019-2023 Fredrik Öhrström (gpl-3.0-or-later)
+driver {
+ name = qcaloric
+ aliases = whe5x,whe46x
+ meter_type = HeatCostAllocationMeter
+ default_fields = name,id,current_consumption_hca,set_date,consumption_at_set_date_hca,timestamp
+ detect {
+ mvt = LSE,34,08
+ mvt = LSE,35,08
+ mvt = QDS,35,08
+ mvt = QDS,34,08
+ mvt = QDS,36,08
+ mvt = LSE,18,08
+ mvt = ZRI,fd,08
+ }
+ library {
+ use = model_version
+ }
+ fields {
+ field {
+ name = status
+ quantity = Text
+ info = 'Meter status from tpl status field.'
+ attributes = STATUS,INCLUDE_TPL_STATUS
+ match {
+ difvifkey = 01FD73
+ }
+ lookup {
+ name = ERROR_FLAGS
+ map_type = BitToString
+ mask_bits = 0xff
+ default_message = OK
+ }
+ }
+ field {
+ name = current_consumption
+ quantity = HCA
+ info = 'The current heat cost allocation.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ measurement_type = Instantaneous
+ vif_range = HeatCostAllocation
+ }
+ }
+ field {
+ name = set
+ quantity = PointInTime
+ info = 'The most recent billing period date.'
+ display_unit = DateLT
+ match {
+ measurement_type = Instantaneous
+ vif_range = Date
+ storage_nr = 1
+ }
+ }
+ field {
+ name = consumption_at_set_date
+ quantity = HCA
+ info = 'Heat cost allocation at the most recent billing period date.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ measurement_type = Instantaneous
+ vif_range = HeatCostAllocation
+ storage_nr = 1
+ }
+ }
+ field {
+ name = set_date_1
+ quantity = Text
+ info = 'The most recent billing period date.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = Date
+ storage_nr = 1
+ }
+ }
+ field {
+ name = consumption_at_set_date_1
+ quantity = HCA
+ info = 'Heat cost allocation at the most recent billing period date.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ measurement_type = Instantaneous
+ vif_range = HeatCostAllocation
+ storage_nr = 1
+ }
+ }
+ field {
+ name = 'set_date_{storage_counter}'
+ quantity = Text
+ info = 'The nth billing period date.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = Date
+ storage_nr = 2,17
+ }
+ }
+ field {
+ name = 'consumption_at_set_date_{storage_counter}'
+ quantity = HCA
+ info = 'Heat cost allocation at the nth billing period date.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ measurement_type = Instantaneous
+ vif_range = HeatCostAllocation
+ storage_nr = 2,17
+ }
+ }
+ field {
+ name = error_date
+ quantity = Text
+ info = 'Date when the meter entered an error state.'
+ match {
+ measurement_type = AtError
+ vif_range = Date
+ }
+ }
+ field {
+ name = mfct_wbds
+ quantity = Text
+ attributes = HIDE
+ match {
+ difvifkey = 0DFF5F
+ }
+ // Siemens/Qundis WalkByDataSet block: DIF=0D (variable-len), VIF=FF (mfct), VIFE=5F, 53 bytes.
+ // 9-byte proprietary header (skipped as quad, quad, byte):
+ // [0] 0x00 reserved
+ // [1] 0x82 wbdsId: Type 2 WalkByDataSet
+ // [2] counter rolling transmission counter (increments each send)
+ // [3] 0x00 reserved
+ // [4] 0x00 reserved
+ // [5] flags device-specific status/flags (format unknown)
+ // [6] 0x00/01 extra flag
+ // [7] 0x07 constant (possibly number of data records)
+ // [8] 0xB0 meter type: 0xB0=HCA, 0xC0=heat, 0xC1=warm/cold water
+ // Byte [9] is the VIF (0x6E = HCA), which is matched below.
+ ixml = "decode = quad, quad, byte, hca.
+
+ hca = '6E', error_date, current_hca, cutoff_date, cutoff_hca, last_date, last_hca, byte*.
+
+ error_date = word, @DV_426C.
+ current_hca = quad, @DV_0C6E.
+ cutoff_date = word, @DV_426C.
+ cutoff_hca = quad, @DV_4C6E.
+ last_date = word, @DV_C2086C.
+ last_hca = quad, @DV_CC086E.
+
+ -hex = ['A'-'F';'0'-'9'].
+ -byte = hex, hex.
+ -word = byte, byte.
+ -quad = byte, byte, byte, byte.
+
+ DV_426C>dvk = +'426C'.
+ DV_0C6E>dvk = +'0C6E'.
+ DV_4C6E>dvk = +'4C6E'.
+ DV_C2086C>dvk = +'C2086C'.
+ DV_CC086E>dvk = +'CC086E'."
+ }
+ field {
+ name = device
+ quantity = PointInTime
+ info = 'Date and time when the meter sent the telegram.'
+ display_unit = datetime
+ match {
+ measurement_type = Instantaneous
+ vif_range = DateTime
+ }
+ }
+ field {
+ name = flow_temperature
+ quantity = Temperature
+ info = 'Forward media temperature.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ measurement_type = Instantaneous
+ vif_range = FlowTemperature
+ }
+ }
+ }
+ tests {
+ test {
+ args = 'MyElement1 qcaloric 78563412 NOKEY'
+ telegram = 314493441234567835087a740000200b6e2701004b6e450100426c5f2ccb086e790000c2086c7f21326cffff046d200b7422
+ json = '{"_":"telegram","media":"heat cost allocation","driver":"qcaloric","name":"MyElement1","id":"78563412","status":"OK","current_consumption_hca":127,"set_date":"2018-12-31","consumption_at_set_date_hca":145,"set_date_1":"2018-12-31","consumption_at_set_date_1_hca":145,"set_date_17":"2019-01-31","consumption_at_set_date_17_hca":79,"error_date":"","device_datetime":"2019-02-20 11:32","timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'MyElement1;78563412;127;2018-12-31;145;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'MyElement2 qcaloric 90919293 NOKEY'
+ comment = 'Walkby telegram with WalkByDataSet manufacturer block (Type2, Bcd6, VIF=HCA).'
+ telegram = 49449344939291903408780DFF5F350082180000800007B06EFFFF970000009F2C70020000BE26970000000000010018002E001F002E0023FF210008000500020000002F046D220FA227
+ json = '{"_":"telegram","media":"heat cost allocation","driver":"qcaloric","name":"MyElement2","id":"90919293","status":"OK","current_consumption_hca":97,"set_date":"2020-12-31","consumption_at_set_date_hca":270,"set_date_1":"2020-12-31","consumption_at_set_date_1_hca":270,"set_date_17":"2021-06-30","consumption_at_set_date_17_hca":97,"device_datetime":"2021-07-02 15:34","timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'MyElement2;90919293;97;2020-12-31;270;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'zenner_heat qcaloric 25932395 NOKEY'
+ telegram = 5E44496A95239325FD087A2CC050052F2F_0B6E030100426CDF2C4B6EFFFFFF82046CE1228B046E6200008D04EE132C3BFEFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF2F2F2F2F
+ json = '{"_":"telegram","media":"heat cost allocation","driver":"qcaloric","name":"zenner_heat","id":"25932395","status":"UNKNOWN_C0","current_consumption_hca":103,"set_date":"2022-12-31","set_date_1":"2022-12-31","set_date_8":"2023-02-01","consumption_at_set_date_8_hca":62,"timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'zenner_heat;25932395;103;2022-12-31;null;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'MyElement3 qcaloric 90919293 NOKEY'
+ comment = 'Normal telegram that fills in values.'
+ telegram = 314493449392919034087a520000200b6e9700004b6e700200426c9f2ccb086e970000c2086cbe26326cffff046d2d16a227
+ json = '{"_":"telegram","media":"heat cost allocation","driver":"qcaloric","name":"MyElement3","id":"90919293","status":"OK","current_consumption_hca":97,"set_date":"2020-12-31","consumption_at_set_date_hca":270,"set_date_1":"2020-12-31","consumption_at_set_date_1_hca":270,"set_date_17":"2021-06-30","consumption_at_set_date_17_hca":97,"error_date":"","device_datetime":"2021-07-02 22:45","timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'MyElement3;90919293;97;2020-12-31;270;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'MyElement4 qcaloric 90919293 NOKEY'
+ comment = 'Another mostly empty telegram, but values are now valid.'
+ telegram = 49449344939291903408780DFF5F350082180000800007B06EFFFF970000009F2C70020000BE26970000000000010018002E001F002E0023FF210008000500020000002F046D220FA228
+ json = '{"_":"telegram","media":"heat cost allocation","driver":"qcaloric","name":"MyElement4","id":"90919293","status":"OK","current_consumption_hca":97,"set_date":"2020-12-31","consumption_at_set_date_hca":270,"set_date_1":"2020-12-31","consumption_at_set_date_1_hca":270,"set_date_17":"2021-06-30","consumption_at_set_date_17_hca":97,"device_datetime":"2021-08-02 15:34","timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'MyElement4;90919293;97;2020-12-31;270;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'HCA1 qcaloric 91835132 NOKEY'
+ comment = 'Another version of the heat cost allocator. Was known as whe5x, so a name alias exist that maps to qcaloric.'
+ telegram = 284465323251839134087a4f0000000b6e0403004b6e660300426c9e29326cffff046d1416b921dd2f
+ json = '{"_":"telegram","media":"heat cost allocation","driver":"qcaloric","name":"HCA1","id":"91835132","consumption_at_set_date_hca":366,"consumption_at_set_date_1_hca":366,"current_consumption_hca":304,"device_datetime":"2021-01-25 22:20","error_date":"","set_date":"2020-09-30","set_date_1":"2020-09-30","status":"OK","timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'HCA1;91835132;304;2020-09-30;366;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'HCA2 qcaloric 60366655 NOKEY'
+ comment = 'Another version of the heat cost allocator. Was known as whe46x, which now is a name alias mapped to qcaloric.'
+ telegram = 344465325566366018087A90040000046D1311962C01FD0C03326CFFFF01FD7300025AC2000DFF5F0C0008003030810613080BFFFC
+ json = '{"_":"telegram","media":"heat cost allocation","driver":"qcaloric","name":"HCA2","id":"60366655","status":"POWER_LOW","error_date":"","device_datetime":"2020-12-22 17:19","model_version":"03","flow_temperature_c":19.4,"timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'HCA2;60366655;null;null;null;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'HCA3 qcaloric 60366655 NOKEY'
+ telegram = 2a4465325566366018087ac3040000046d1617Ba210B6e890000426c9f2c4B6e520600326cffff01fd7300
+ json = '{"_":"telegram","media":"heat cost allocation","driver":"qcaloric","name":"HCA3","id":"60366655","status":"POWER_LOW","current_consumption_hca":89,"set_date":"2020-12-31","consumption_at_set_date_hca":652,"set_date_1":"2020-12-31","consumption_at_set_date_1_hca":652,"error_date":"","device_datetime":"2021-01-26 23:22","timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'HCA3;60366655;89;2020-12-31;652;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'HCA4 qcaloric 30535282 NOKEY'
+ comment = 'Mostly mfct specific data. Not yet decoded.'
+ telegram = 49449344825253303608780DFF5F350082430035E3DFC4EAC97A58B8610713D93549E2601258D617D267E7515C764B002A88CD341A9F9DF3C6034DE5B6D1FAB3619CBA9F046D250DDA2C
+ json = '{"_":"telegram","media":"heat cost allocation","driver":"qcaloric","name":"HCA4","id":"30535282","status":"OK","device_datetime":"2022-12-26 13:37","timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'HCA4;30535282;null;null;null;1111-11-11 11:11.11'
+ }
+ }
+}
diff --git a/components/wmbus_common/drivers/src/qheat_55_us.xmq b/components/wmbus_common/drivers/src/qheat_55_us.xmq
new file mode 100644
index 00000000..5e7e44e6
--- /dev/null
+++ b/components/wmbus_common/drivers/src/qheat_55_us.xmq
@@ -0,0 +1,126 @@
+// Copyright (C) 2022 Fredrik Öhrström (gpl-3.0-or-later)
+driver {
+ name = qheat_55_us
+ meter_type = HeatMeter
+ default_fields = name,id,total_energy_consumption_kwh,key_datetime_kwh,timestamp
+ detect {
+ mvt = LUG,07,04
+ mvt = LUG,0a,04
+ }
+ fields {
+ field {
+ name = device
+ quantity = PointInTime
+ info = 'Device date time.'
+ display_unit = datetime
+ match {
+ measurement_type = Instantaneous
+ vif_range = DateTime
+ }
+ }
+ field {
+ name = total_energy_consumption
+ quantity = Energy
+ info = 'The total energy consumption recorded by this meter.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyEnergyVIF
+ index_nr = 1
+ storage_nr = 0
+ tariff_nr = 0
+ }
+ }
+ field {
+ name = key
+ quantity = PointInTime
+ info = 'The key (billing) date'
+ display_unit = datetime
+ match {
+ measurement_type = Instantaneous
+ vif_range = DateTime
+ index_nr = 1
+ storage_nr = 1
+ tariff_nr = 0
+ }
+ }
+ field {
+ name = key_datetime
+ quantity = Energy
+ info = 'The total energy consumption recorded at key (billing) date'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyEnergyVIF
+ index_nr = 1
+ storage_nr = 1
+ tariff_nr = 0
+ }
+ }
+ field {
+ name = prev_month
+ quantity = PointInTime
+ info = 'The date of end of last month.'
+ display_unit = datetime
+ match {
+ measurement_type = Instantaneous
+ vif_range = DateTime
+ index_nr = 1
+ storage_nr = 2
+ tariff_nr = 0
+ }
+ }
+ field {
+ name = 'prev_{storage_counter-1counter}_month'
+ quantity = Energy
+ info = 'Previous month energy.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyEnergyVIF
+ index_nr = 1
+ storage_nr = 2,14
+ tariff_nr = 0
+ }
+ }
+ field {
+ name = actuality_duration
+ quantity = Time
+ info = 'The time between the measurement and the sending of this telegram.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ measurement_type = Instantaneous
+ vif_range = ActualityDuration
+ }
+ }
+ field {
+ name = time_without_measurement
+ quantity = Time
+ info = 'How long the meter has been in an error state and unable to measure values, while powered up.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ measurement_type = AtError
+ vif_range = OnTime
+ }
+ }
+ }
+ tests {
+ test {
+ args = 'Heat qheat_55_us 70835484 NOKEY'
+ telegram = 9644a732845483700a047ae70000200274fc00046d230Bd3250c0605920000446d3B17Bf2c4c068251000084016d3B17de248c010605900000cc0106988500008c020629770000cc0206226600008c030682510000cc0306933600008c040602260000cc0406691800008c050633140000cc0506900900008c060618020000cc0606000000008c0706000000003c22000000000f001000
+ json = '{"_":"telegram","media":"heat","driver":"qheat_55_us","name":"Heat","id":"70835484","device_datetime":"2022-05-19 11:35","total_energy_consumption_kwh":9205,"key_datetime":"2021-12-31 23:59","key_datetime_kwh":5182,"prev_month_datetime":"2022-04-30 23:59","prev_1_month_kwh":9005,"prev_2_month_kwh":8598,"prev_3_month_kwh":7729,"prev_4_month_kwh":6622,"prev_5_month_kwh":5182,"prev_6_month_kwh":3693,"prev_7_month_kwh":2602,"prev_8_month_kwh":1869,"prev_9_month_kwh":1433,"prev_10_month_kwh":990,"prev_11_month_kwh":218,"prev_12_month_kwh":0,"prev_13_month_kwh":0,"actuality_duration_h":0.07,"time_without_measurement_h":0,"timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'Heat;70835484;9205;5182;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'Heat qheat_55_us 70639638 NOKEY'
+ telegram = 9644a7323896637007047a5B00002002740400046d2a0fe2210c0600080600446d3B17df2c4c069106060084016d3B17df2c8c010691060600cc0106227005008c020667430500cc0206122505008c030612180500cc0306821305008c040636090500cc0406080505008c050660960400cc0506597304008c060600410400cc0606790504008c0706936303003c22000000000f001000
+ json = '{"_":"telegram","media":"heat","driver":"qheat_55_us","name":"Heat","id":"70639638","device_datetime":"2023-01-02 15:42","total_energy_consumption_kwh":60800,"key_datetime":"2022-12-31 23:59","key_datetime_kwh":60691,"prev_month_datetime":"2022-12-31 23:59","prev_1_month_kwh":60691,"prev_2_month_kwh":57022,"prev_3_month_kwh":54367,"prev_4_month_kwh":52512,"prev_5_month_kwh":51812,"prev_6_month_kwh":51382,"prev_7_month_kwh":50936,"prev_8_month_kwh":50508,"prev_9_month_kwh":49660,"prev_10_month_kwh":47359,"prev_11_month_kwh":44100,"prev_12_month_kwh":40579,"prev_13_month_kwh":36393,"actuality_duration_h":0.001111,"time_without_measurement_h":0,"timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'Heat;70639638;60800;60691;1111-11-11 11:11.11'
+ }
+ }
+}
diff --git a/components/wmbus_common/drivers/src/qheatv2.xmq b/components/wmbus_common/drivers/src/qheatv2.xmq
new file mode 100644
index 00000000..e1ee5896
--- /dev/null
+++ b/components/wmbus_common/drivers/src/qheatv2.xmq
@@ -0,0 +1,441 @@
+// Copyright (C) 2026 Fredrik Öhrström (gpl-3.0-or-later)
+driver {
+ name = qheatv2
+ meter_type = HeatMeter
+ default_fields = name,id,status,total_kwh,timestamp
+ manufacturer = Qundis
+ transform_payload = try_qundis_decode
+ detect {
+ mvt = QDS,23,04
+ mvt = QDS,23,37
+ mvt = QDS,3c,04
+ mvt = QDS,3c,37
+ mvt = QDS,3e,04
+ mvt = QDS,3e,37
+ mvt = QDS,46,04
+ mvt = QDS,46,37
+ mvt = QDS,47,04
+ mvt = QDS,47,37
+ mvt = QDS,48,04
+ mvt = QDS,48,37
+ mvt = QDS,3f,37
+ mvt = LSE,23,04
+ mvt = LSE,23,37
+ mvt = LSE,3e,04
+ mvt = LSE,3e,37
+ mvt = LSE,46,04
+ mvt = LSE,46,37
+ mvt = LSE,47,04
+ mvt = LSE,47,37
+ mvt = LSE,48,04
+ mvt = LSE,48,37
+ }
+ library {
+ use = meter_datetime
+ use = enhanced_id
+ use = fabrication_no
+ }
+ fields {
+ field {
+ name = status
+ quantity = Text
+ info = 'Status and error flags'
+ attributes = STATUS,INCLUDE_TPL_STATUS
+ match {
+ measurement_type = Instantaneous
+ vif_range = ErrorFlags
+ }
+ lookup {
+ /* !!! The status bits are uncertain !!!
+ The documentation links to some intranet site, Qundis Error Codes Specification v1.7
+ at https://base/svn/sys/Meter/Gen/G5-5/PUBL/Gen55_SysSpec_Error-Codes_EN_v1.6_any.pdf
+ Here is a table that might apply:
+ https://www.manualslib.com/manual/2046543/Qundis-Q-Heat-5-5-Us.html?page=5 */
+ name = ERROR_FLAGS
+ map_type = BitToString
+ mask_bits = 0xffff
+ default_message = OK
+ map {
+ name = NO_FLOW
+ // F0
+ value = 0x0001
+ test = Set
+ }
+ map {
+ name = SUPPLY_SENSOR_INTERRUPTED
+ // F1
+ value = 0x0002
+ test = Set
+ }
+ map {
+ name = RETURN_SENSOR_INTERRUPTED
+ // F2
+ value = 0x0004
+ test = Set
+ }
+ map {
+ name = TEMPERATURE_ELECTRONICS_ERROR
+ // F3
+ value = 0x0008
+ test = Set
+ }
+ map {
+ name = BATTERY_VOLTAGE_ERROR
+ // F4
+ value = 0x0010
+ test = Set
+ }
+ map {
+ name = SHORT_CIRCUIT_SUPPLY_SENSOR
+ // F5
+ value = 0x0020
+ test = Set
+ }
+ map {
+ name = SHORT_CIRCUIT_RETURN_SENSOR
+ // F6
+ value = 0x0040
+ test = Set
+ }
+ map {
+ name = MEMORY_ERROR
+ // F7
+ value = 0x0080
+ test = Set
+ }
+ map {
+ name = SABOTAGE
+ // F8 - F1,2,3,5,6 longer than 8 hours, latching error, no more measurements performed.
+ value = 0x0100
+ test = Set
+ }
+ map {
+ name = ELECTRONICS_ERROR
+ // F9
+ value = 0x0200
+ test = Set
+ }
+ }
+ }
+ field {
+ name = total
+ quantity = Energy
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyEnergyVIF
+ }
+ }
+ field {
+ name = target
+ quantity = Energy
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyEnergyVIF
+ storage_nr = 17
+ }
+ }
+ field {
+ name = target
+ quantity = PointInTime
+ display_unit = date
+ match {
+ measurement_type = Instantaneous
+ vif_range = Date
+ storage_nr = 17
+ }
+ }
+ field {
+ name = target_year
+ quantity = Energy
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyEnergyVIF
+ storage_nr = 1
+ }
+ }
+ field {
+ name = target_year
+ quantity = PointInTime
+ display_unit = date
+ match {
+ measurement_type = Instantaneous
+ vif_range = Date
+ storage_nr = 1
+ }
+ }
+ field {
+ name = device_error
+ quantity = PointInTime
+ display_unit = date
+ match {
+ measurement_type = AtError
+ vif_range = Date
+ }
+ }
+ field {
+ name = mfct_specific_data
+ quantity = Text
+ attributes = HIDE
+ match {
+ difvifkey = 0DFF5F
+ }
+ // Siemens/Qundis WalkByDataSet block: DIF=0D (variable-len), VIF=FF (mfct), VIFE=5F, 53 bytes.
+ // 9-byte proprietary header (skipped as quad, quad, byte):
+ // [0] 0x00 reserved
+ // [1] 0x82 wbdsId: Type 2 WalkByDataSet
+ // [2] counter rolling transmission counter (increments each send)
+ // [3] 0x00 reserved
+ // [4] 0x00 reserved
+ // [5] flags device-specific status/flags (format unknown)
+ // [6] 0x00/01 extra flag
+ // [7] 0x07 constant (possibly number of data records)
+ // [8] 0xC0 meter type: 0xB0=HCA, 0xC0=heat, 0xC1=warm/cold water
+ // Byte [9] is the VIF (0x05/06/0D/0E = energy variants), which is matched below.
+ // The 9-byte header is validated against its fixed bytes ([0]=00, [1]=82,
+ // [3]=00, [4]=00, [7]=07, [8]=C0 heat) so that a block with an
+ // unexpected layout (e.g. an encrypted/non-walk-by payload that merely
+ // happens to carry the 0DFF5F difvifkey) fails closed instead of having
+ // total_*/target_*/energy_delta_* invented from the wrong offsets.
+ // Encrypted 2026 5.5-series blocks ([4]=0x35) are AES-128-CBC decrypted in
+ // the C++ layer before this grammar runs; see issue #1916 and #2025.
+ ixml = "decode = -'00', -'82', byte, -'00', -'00', byte, byte, -'07', -'C0',
+ (energy_100wh |
+ energy_1kwh |
+ energy_100kj |
+ energy_1mj).
+
+ energy_100wh = '05', error_date, total_100wh, target_year_date, target_year_100wh, target_date, target_100wh, d05_1, d05_2, d05_3, d05_4, d05_5, d05_6, d05_7, d05_8, d05_9, d05_10, d05_11, d05_12, -pad.
+ energy_1kwh = '06', error_date, total_1kwh, target_year_date, target_year_1kwh, target_date, target_1kwh, d06_1, d06_2, d06_3, d06_4, d06_5, d06_6, d06_7, d06_8, d06_9, d06_10, d06_11, d06_12, -pad.
+ energy_100kj = '0D', error_date, total_100kj, target_year_date, target_year_100kj, target_date, target_100kj, d0d_1, d0d_2, d0d_3, d0d_4, d0d_5, d0d_6, d0d_7, d0d_8, d0d_9, d0d_10, d0d_11, d0d_12, -pad.
+ energy_1mj = '0E', error_date, total_1mj, target_year_date, target_year_1mj, target_date, target_1mj, d0e_1, d0e_2, d0e_3, d0e_4, d0e_5, d0e_6, d0e_7, d0e_8, d0e_9, d0e_10, d0e_11, d0e_12, -pad.
+
+ error_date = word, @DV_426C.
+ target_year_date = word, @DV_426C.
+ target_date = word, @DV_C2086C.
+
+ total_100wh = quad, @DV_0C05.
+ total_1kwh = quad, @DV_0C06.
+ total_100kj = quad, @DV_0C0D.
+ total_1mj = quad, @DV_0C0E.
+
+ target_year_100wh = quad, @DV_4C05.
+ target_year_1kwh = quad, @DV_4C06.
+ target_year_100kj = quad, @DV_4C0D.
+ target_year_1mj = quad, @DV_4C0E.
+
+ target_100wh = quad, @DV_CC0805.
+ target_1kwh = quad, @DV_CC0806.
+ target_100kj = quad, @DV_CC080D.
+ target_1mj = quad, @DV_CC080E.
+
+ d05_1 = word, @DV_820105.
+ d05_2 = word, @DV_C20105.
+ d05_3 = word, @DV_820205.
+ d05_4 = word, @DV_C20205.
+ d05_5 = word, @DV_820305.
+ d05_6 = word, @DV_C20305.
+ d05_7 = word, @DV_820405.
+ d05_8 = word, @DV_C20405.
+ d05_9 = word, @DV_820505.
+ d05_10 = word, @DV_C20505.
+ d05_11 = word, @DV_820605.
+ d05_12 = word, @DV_C20605.
+
+ d06_1 = word, @DV_820106.
+ d06_2 = word, @DV_C20106.
+ d06_3 = word, @DV_820206.
+ d06_4 = word, @DV_C20206.
+ d06_5 = word, @DV_820306.
+ d06_6 = word, @DV_C20306.
+ d06_7 = word, @DV_820406.
+ d06_8 = word, @DV_C20406.
+ d06_9 = word, @DV_820506.
+ d06_10 = word, @DV_C20506.
+ d06_11 = word, @DV_820606.
+ d06_12 = word, @DV_C20606.
+
+ d0d_1 = word, @DV_82010D.
+ d0d_2 = word, @DV_C2010D.
+ d0d_3 = word, @DV_82020D.
+ d0d_4 = word, @DV_C2020D.
+ d0d_5 = word, @DV_82030D.
+ d0d_6 = word, @DV_C2030D.
+ d0d_7 = word, @DV_82040D.
+ d0d_8 = word, @DV_C2040D.
+ d0d_9 = word, @DV_82050D.
+ d0d_10 = word, @DV_C2050D.
+ d0d_11 = word, @DV_82060D.
+ d0d_12 = word, @DV_C2060D.
+
+ d0e_1 = word, @DV_82010E.
+ d0e_2 = word, @DV_C2010E.
+ d0e_3 = word, @DV_82020E.
+ d0e_4 = word, @DV_C2020E.
+ d0e_5 = word, @DV_82030E.
+ d0e_6 = word, @DV_C2030E.
+ d0e_7 = word, @DV_82040E.
+ d0e_8 = word, @DV_C2040E.
+ d0e_9 = word, @DV_82050E.
+ d0e_10 = word, @DV_C2050E.
+ d0e_11 = word, @DV_82060E.
+ d0e_12 = word, @DV_C2060E.
+ -pad = byte.
+
+ -hex = ['A'-'F';'0'-'9'].
+ -byte = hex, hex.
+ -word = byte, byte.
+ -triplet = byte, byte, byte.
+ -quad = byte, byte, byte, byte.
+
+ DV_0C05>dvk = +'0C05'.
+ DV_0C06>dvk = +'0C06'.
+ DV_0C0D>dvk = +'0C0D'.
+ DV_0C0E>dvk = +'0C0E'.
+
+ DV_4C05>dvk = +'4C05'.
+ DV_4C06>dvk = +'4C06'.
+ DV_4C0D>dvk = +'4C0D'.
+ DV_4C0E>dvk = +'4C0E'.
+
+ DV_CC0805>dvk = +'CC0805'.
+ DV_CC0806>dvk = +'CC0806'.
+ DV_CC080D>dvk = +'CC080D'.
+ DV_CC080E>dvk = +'CC080E'.
+
+ DV_426C>dvk = +'426C'.
+ DV_C2086C>dvk = +'C2086C'.
+
+ DV_820105>dvk = +'820105'.
+ DV_C20105>dvk = +'C20105'.
+ DV_820205>dvk = +'820205'.
+ DV_C20205>dvk = +'C20205'.
+ DV_820305>dvk = +'820305'.
+ DV_C20305>dvk = +'C20305'.
+ DV_820405>dvk = +'820405'.
+ DV_C20405>dvk = +'C20405'.
+ DV_820505>dvk = +'820505'.
+ DV_C20505>dvk = +'C20505'.
+ DV_820605>dvk = +'820605'.
+ DV_C20605>dvk = +'C20605'.
+
+ DV_820106>dvk = +'820106'.
+ DV_C20106>dvk = +'C20106'.
+ DV_820206>dvk = +'820206'.
+ DV_C20206>dvk = +'C20206'.
+ DV_820306>dvk = +'820306'.
+ DV_C20306>dvk = +'C20306'.
+ DV_820406>dvk = +'820406'.
+ DV_C20406>dvk = +'C20406'.
+ DV_820506>dvk = +'820506'.
+ DV_C20506>dvk = +'C20506'.
+ DV_820606>dvk = +'820606'.
+ DV_C20606>dvk = +'C20606'.
+
+ DV_82010D>dvk = +'82010D'.
+ DV_C2010D>dvk = +'C2010D'.
+ DV_82020D>dvk = +'82020D'.
+ DV_C2020D>dvk = +'C2020D'.
+ DV_82030D>dvk = +'82030D'.
+ DV_C2030D>dvk = +'C2030D'.
+ DV_82040D>dvk = +'82040D'.
+ DV_C2040D>dvk = +'C2040D'.
+ DV_82050D>dvk = +'82050D'.
+ DV_C2050D>dvk = +'C2050D'.
+ DV_82060D>dvk = +'82060D'.
+ DV_C2060D>dvk = +'C2060D'.
+
+ DV_82010E>dvk = +'82010E'.
+ DV_C2010E>dvk = +'C2010E'.
+ DV_82020E>dvk = +'82020E'.
+ DV_C2020E>dvk = +'C2020E'.
+ DV_82030E>dvk = +'82030E'.
+ DV_C2030E>dvk = +'C2030E'.
+ DV_82040E>dvk = +'82040E'.
+ DV_C2040E>dvk = +'C2040E'.
+ DV_82050E>dvk = +'82050E'.
+ DV_C2050E>dvk = +'C2050E'.
+ DV_82060E>dvk = +'82060E'.
+ DV_C2060E>dvk = +'C2060E'."
+ }
+ field {
+ name = 'energy_delta_{storage_counter}'
+ quantity = Energy
+ info = 'Monthly energy delta.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyEnergyVIF
+ storage_nr = 2,13
+ }
+ }
+ }
+ tests {
+ test {
+ args = 'QHeato qheatv2 67228058 NOKEY'
+ telegram = 3C449344957002372337725880226793442304DC0000200C05043900004C0500000000426C9F2CCC080551070000C2086CBE29326CFFFF046D280DB62A
+ json = '{"_": "telegram","device_error_date":null,"id": "67228058","media": "heat","driver": "qheatv2","meter_datetime": "2021-10-22 13:40","name": "QHeato","status": "OK","target_date": "2021-09-30","target_kwh": 75.1,"target_year_date": "2020-12-31","target_year_kwh": 0,"timestamp": "1111-11-11T11:11:11Z","total_kwh": 390.4}'
+ fields = 'QHeato;67228058;OK;390.4;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'QQ1 qheatv2 68204641 NOKEY'
+ telegram = 58449344414620684737780779414620689344470c0dff5f3500825a00000e0007c00dffff310803001f3c036800003e34310803000080008000800080008000800000931a92293128190c00002f02fd170000046d000f3235
+ json = '{"_": "telegram","energy_delta_10_kwh":295.611111,"energy_delta_11_kwh":285.805556,"energy_delta_12_kwh":86.027778,"energy_delta_13_kwh":0,"energy_delta_2_kwh":-910.222222,"energy_delta_3_kwh":-910.222222,"energy_delta_4_kwh":-910.222222,"energy_delta_5_kwh":-910.222222,"energy_delta_6_kwh":-910.222222,"energy_delta_7_kwh":-910.222222,"energy_delta_8_kwh":0,"energy_delta_9_kwh":188.972222,"enhanced_id": "0C47449368204641","id": "68204641","media": "radio converter (meter side)","driver": "qheatv2","meter_datetime": "2025-05-18 15:00","name": "QQ1","status": "OK","target_date": "2025-04-30","target_kwh":856.416667,"target_year_date": "2024-12-31","target_year_kwh":188.972222,"timestamp": "1111-11-11T11:11:11Z","total_kwh": 856.416667}'
+ fields = 'QQ1;68204641;OK;856.416667;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'QHeat55r qheatv2 68267443 NOKEY'
+ comment = 'Q AMR telegram sent every 7.5 minutes'
+ comment = 'Q Walk by telegram sent every 112 seconds'
+ telegram = 414493444374266848377243742668934448042A000020_0C06140000004C0600000000426CFFFFCC080600000000C2086C3C3202FD170000326CFFFF046D190F3533
+ json = '{"_": "telegram","device_error_date":null,"id": "68267443","media": "heat","driver": "qheatv2","meter_datetime": "2025-03-21 15:25","name": "QHeat55r","status": "OK","target_date": "2025-02-28","target_kwh": 0,"target_year_date":null,"target_year_kwh": 0,"timestamp": "1111-11-11T11:11:11Z","total_kwh": 14}'
+ fields = 'QHeat55r;68267443;OK;14;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'QHeat55r qheatv2 68267443 NOKEY'
+ comment = 'Q Walk by telegram sent every 112 seconds'
+ telegram = 5844934443742668483778_077943742668934448040DFF5F3500822A0000610107C006FFFF14000000FFFF000000003C32000000000080008000800080008000800080008000800080008000002F02FD170000046D1B0F3533
+ json = '{"_": "telegram","energy_delta_10_kwh":-32768,"energy_delta_11_kwh":-32768,"energy_delta_12_kwh":-32768,"energy_delta_13_kwh":0,"energy_delta_2_kwh":-32768,"energy_delta_3_kwh":-32768,"energy_delta_4_kwh":-32768,"energy_delta_5_kwh":-32768,"energy_delta_6_kwh":-32768,"energy_delta_7_kwh":-32768,"energy_delta_8_kwh":-32768,"energy_delta_9_kwh":-32768,"enhanced_id": "0448449368267443","id": "68267443","media": "radio converter (meter side)","driver": "qheatv2","meter_datetime": "2025-03-21 15:27","name": "QHeat55r","status": "OK","target_date": "2025-02-28","target_kwh": 0,"target_year_date":null,"target_year_kwh": 0,"timestamp": "1111-11-11T11:11:11Z","total_kwh": 14}'
+ fields = 'QHeat55r;68267443;OK;14;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'QQA qheatv2 67958979 NOKEY'
+ telegram = 58449344798995673E377807797989956793443E040DFF5F3500827D0000810007C006FFFF531100003F3C490900005F318010000083003C0022000B0000000000000000000A002B00510083002F02FD170000046D16085832
+ json = '{"_": "telegram","energy_delta_10_kwh":10,"energy_delta_11_kwh":43,"energy_delta_12_kwh":81,"energy_delta_13_kwh":131,"energy_delta_2_kwh":131,"energy_delta_3_kwh":60,"energy_delta_4_kwh":34,"energy_delta_5_kwh":11,"energy_delta_6_kwh":0,"energy_delta_7_kwh":0,"energy_delta_8_kwh":0,"energy_delta_9_kwh":0,"enhanced_id": "043E449367958979","id": "67958979","media": "radio converter (meter side)","driver": "qheatv2","meter_datetime": "2026-02-24 08:22","name": "QQA","status": "OK","target_date": "2026-01-31","target_kwh":1080,"target_year_date": "2025-12-31","target_year_kwh":949,"timestamp": "1111-11-11T11:11:11Z","total_kwh": 1153}'
+ fields = 'QQA;67958979;OK;1153;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'QQB qheatv2 67985890 NOKEY'
+ telegram = 41449344905898673e37729058986793443e04120000200c06809300004c0678820000426c3f3ccc080636900000c2086c5f3102fd170000326cffff046d1d095832
+ json = '{"_": "telegram","device_error_date":null,"id": "67985890","media": "heat","driver": "qheatv2","meter_datetime": "2026-02-24 09:29","name": "QQB","status": "OK","target_date": "2026-01-31","target_kwh": 9036,"target_year_date": "2025-12-31","target_year_kwh": 8278,"timestamp": "1111-11-11T11:11:11Z","total_kwh": 9380}'
+ fields = 'QQB;67985890;OK;9380;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'HeatEnc55 qheatv2 67918447 NOKEY'
+ comment = 'Real encrypted Q heat 5.5 walk-by block (header byte[4]=0x35, from issue #1588) without a configured key: the grammar must fail closed and must not invent total_*/target_*/energy_delta_* values from the ciphertext. Only the trailing 046D meter_datetime record is real. See issues #1588/#2025.'
+ telegram = 58449344478491673C377807794784916793443C040DFF5F350082B40035F4E5CF5874D4A45106F8435DDA926DE66FE62E8ECCB25B199CCAE33C6B679AD69AC8629398ECDCB1DDB263EDA12137EA02FD170000046D080F4333
+ json = '{"_": "telegram","enhanced_id": "043C449367918447","id": "67918447","media": "radio converter (meter side)","driver": "qheatv2","meter_datetime": "2026-03-03 15:08","name": "HeatEnc55","status": "MISSING_KEY","timestamp": "1111-11-11T11:11:11Z"}'
+ fields = 'HeatEnc55;67918447;MISSING_KEY;null;1111-11-11 11:11.11'
+ }
+ test {
+ comment = 'Q heat 5.5 behind a radio converter: the walk-by block must be decrypted with the meter identity from the 0779 record (type 04), not with the dll address of the converter (type 37). Telegram and key from issue #2100, the meter display shows 5 kWh.'
+ args = 'QHeat55w qheatv2 68750153 CA7972242CA575C50CEB38FAD70AD169'
+ telegram = 58449344530175683F377807795301756893443F040DFF5F3500822C003555280D40FBA8998C9FE06BD8CD6796ED62F9C8B2CAE1A80D6166007C446865E9502913B1DC1EE820986D102033CE54B902FD170000046D22105639
+ json = '{"_":"telegram","driver":"qheatv2","energy_delta_10_kwh":-32768,"energy_delta_11_kwh":0,"energy_delta_12_kwh":2,"energy_delta_13_kwh":1,"energy_delta_2_kwh":-32768,"energy_delta_3_kwh":-32768,"energy_delta_4_kwh":-32768,"energy_delta_5_kwh":-32768,"energy_delta_6_kwh":-32768,"energy_delta_7_kwh":-32768,"energy_delta_8_kwh":-32768,"energy_delta_9_kwh":-32768,"enhanced_id":"043F449368750153","id":"68750153","media":"radio converter (meter side)","meter_datetime":"2026-09-22 16:34","name":"QHeat55w","status":"OK","target_date":"2026-08-31","target_kwh":3,"target_year_date":null,"target_year_kwh":0,"timestamp":"1111-11-11T11:11:11Z","total_kwh":5}'
+ fields = 'QHeat55w;68750153;OK;5;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'RealKeyHeat qheatv2 68369440 31E7AEDAE4B659172F4D4ED99DD7AA52'
+ comment = 'Real encrypted Q Heat 5.5 walk-by block with the real key that the meter owner published in issue #1497, module version 0x3f: proves the AES path end-to-end with genuine key material. The fail-closed counterpart without a key is covered by HeatEnc55. The expected total was 3052 until the walk-by IV was built from the meter identity instead of the converter address, which changes exactly the low byte of the total.'
+ telegram = 58449344409436683F377807794094366893443F040DFF5F350082110035D3B5EA13344D42345FD6C9B468DC8A7FC0A56DCECD9861481F7C87A367564D55E1576B5A9B00E00CF10466C038D6393902FD170000046D00135832
+ json = '{"_":"telegram","driver":"qheatv2","energy_delta_10_kwh":364,"energy_delta_11_kwh":370,"energy_delta_12_kwh":354,"energy_delta_13_kwh":334,"energy_delta_2_kwh":-32768,"energy_delta_3_kwh":-32768,"energy_delta_4_kwh":-32768,"energy_delta_5_kwh":0,"energy_delta_6_kwh":0,"energy_delta_7_kwh":517,"energy_delta_8_kwh":457,"energy_delta_9_kwh":375,"enhanced_id":"043F449368369440","id":"68369440","media":"radio converter (meter side)","meter_datetime":"2026-02-24 19:00","name":"RealKeyHeat","status":"OK","target_date":"2026-01-31","target_kwh":2771,"target_year_date":"2025-12-31","target_year_kwh":2437,"timestamp":"1111-11-11T11:11:11Z","total_kwh":3061}'
+ fields = 'RealKeyHeat;68369440;OK;3061;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'QH qheatv2 67250945 NOKEY'
+ telegram = 2e4465B24509256723047aa20000000c05035612004c0520131100426c3f3c326cffff046d12014a330c7834980437
+ json = '{"_": "telegram","fabrication_no": "37049834","id": "67250945","media": "heat","driver": "qheatv2","device_error_date": null,"meter_datetime": "2026-03-10 01:18","name": "QH","status": "OK","target_year_date": "2025-12-31","target_year_kwh": 11132,"timestamp": "1111-11-11T11:11:11Z","total_kwh": 12560.3}'
+ fields = 'QH;67250945;OK;12560.3;1111-11-11 11:11.11'
+ }
+ }
+}
diff --git a/components/wmbus_common/drivers/src/qsmoke.xmq b/components/wmbus_common/drivers/src/qsmoke.xmq
new file mode 100644
index 00000000..2cf6bc94
--- /dev/null
+++ b/components/wmbus_common/drivers/src/qsmoke.xmq
@@ -0,0 +1,121 @@
+// Copyright (C) 2021 Fredrik Öhrström (gpl-3.0-or-later)
+driver {
+ name = qsmoke
+ meter_type = SmokeDetector
+ default_fields = name,id,status,last_alarm_date,alarm_counter,timestamp
+ detect {
+ mvt = QDS,21,1a
+ mvt = QDS,23,1a
+ mvt = LSE,21,1a
+ mvt = LSE,23,1a
+ }
+ fields {
+ field {
+ name = status
+ quantity = Text
+ info = 'Meter error flags. IMPORTANT! Smoke alarm is probably NOT reported here! You MUST check last alarm date and counter!'
+ attributes = STATUS,INCLUDE_TPL_STATUS
+ match {
+ measurement_type = Instantaneous
+ vif_range = ErrorFlags
+ }
+ lookup {
+ name = ERROR_FLAGS
+ name = ERROR_FLAGS
+ map_type = BitToString
+ mask_bits = 0xffff
+ default_message = OK
+ // Help wanted! Missing understanding of bits
+ }
+ }
+ field {
+ name = last_alarm
+ quantity = PointInTime
+ info = 'Date when the smoke alarm last triggered.'
+ display_unit = DateLT
+ match {
+ measurement_type = Instantaneous
+ vif_range = Date
+ storage_nr = 6
+ }
+ }
+ field {
+ name = alarm
+ quantity = Dimensionless
+ info = 'Number of times the smoke alarm has triggered.'
+ match {
+ difvifkey = 81037C034C4123
+ }
+ }
+ field {
+ name = message
+ quantity = PointInTime
+ info = 'Device date time.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = DateTime
+ }
+ }
+ field {
+ name = test_button_last
+ quantity = PointInTime
+ info = 'Date when test button was last pressed.'
+ display_unit = DateLT
+ match {
+ measurement_type = Instantaneous
+ vif_range = Date
+ storage_nr = 4
+ }
+ }
+ field {
+ name = test_button
+ quantity = Dimensionless
+ info = 'Number of times the test button has been pressed.'
+ match {
+ difvifkey = 81027C03495523
+ }
+ }
+ field {
+ name = transmission
+ quantity = Dimensionless
+ info = 'Transmission counter?'
+ dif_signedness = Unsigned
+ match {
+ measurement_type = Instantaneous
+ vif_range = AccessNumber
+ }
+ }
+ field {
+ name = at_error
+ quantity = PointInTime
+ info = 'Date when the device entered an error state.'
+ display_unit = DateLT
+ match {
+ measurement_type = AtError
+ vif_range = Date
+ }
+ }
+ field {
+ name = some_sort_of_duration
+ quantity = Time
+ info = 'What does this mean'
+ match {
+ difvifkey = 02FDAC7E
+ }
+ }
+ }
+ tests {
+ test {
+ args = 'QSmokeo qsmoke 45797086 NOKEY'
+ telegram = 3E44934486707945211A7801FD08F081027C034955230082026CFFFF81037C034C41230082036CFFFF03FD17000000326CFFFF046D0F0ABC2B02FDAC7E1100
+ json = '{"_":"telegram","media":"smoke detector","driver":"qsmoke","name":"QSmokeo","id":"45797086","status":"OK","last_alarm_date":null,"alarm_counter":0,"message_datetime":"2021-11-28 10:15","test_button_last_date":null,"test_button_counter":0,"transmission_counter":240,"at_error_date":null,"some_sort_of_duration_h":0.004722,"timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'QSmokeo;45797086;OK;null;0;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'QSmokep qsmoke 48128850 NOKEY'
+ telegram = 3744934450881248231A7A5C00002081027C034955230082026CFFFF81037C034C41230082036CFFFF02FD170000326CFFFF046D2514BC2B
+ json = '{"_":"telegram","media":"smoke detector","driver":"qsmoke","name":"QSmokep","id":"48128850","status":"OK","last_alarm_date":null,"alarm_counter":0,"message_datetime":"2021-11-28 20:37","test_button_last_date":null,"test_button_counter":0,"at_error_date":null,"timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'QSmokep;48128850;OK;null;0;1111-11-11 11:11.11'
+ }
+ }
+}
diff --git a/components/wmbus_common/drivers/src/qualcosonic.xmq b/components/wmbus_common/drivers/src/qualcosonic.xmq
new file mode 100644
index 00000000..ca265842
--- /dev/null
+++ b/components/wmbus_common/drivers/src/qualcosonic.xmq
@@ -0,0 +1,132 @@
+// Copyright (C) 2022-2024 Fredrik Öhrström (gpl-3.0-or-later)
+driver {
+ name = qualcosonic
+ meter_type = HeatCoolingMeter
+ default_fields = name,id,status,total_heat_energy_kwh,total_cooling_energy_kwh,power_kw,target_datetime,target_heat_energy_kwh,target_cooling_energy_kwh,timestamp
+ detect {
+ mvt = AXI,0b,0d
+ mvt = AXI,0c,0d
+ }
+ library {
+ use = fabrication_no
+ use = operating_time_h
+ use = on_time_h
+ use = meter_datetime
+ use = meter_datetime_at_error
+ use = total_m3
+ use = flow_temperature_c
+ use = return_temperature_c
+ use = flow_return_temperature_difference_c
+ use = volume_flow_m3h
+ }
+ fields {
+ field {
+ name = status
+ quantity = Text
+ info = 'Meter status. Includes both meter error field and tpl status field.'
+ attributes = STATUS,INCLUDE_TPL_STATUS
+ match {
+ measurement_type = Instantaneous
+ vif_range = ErrorFlags
+ }
+ lookup {
+ name = ERROR_FLAGS
+ map_type = BitToString
+ mask_bits = 0xffffffff
+ default_message = OK
+ }
+ }
+ field {
+ name = total_heat_energy
+ quantity = Energy
+ info = 'The total heating energy consumption recorded by this meter.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyEnergyVIF
+ add_combinable = ForwardFlow
+ }
+ }
+ field {
+ name = total_cooling_energy
+ quantity = Energy
+ info = 'The total cooling energy consumption recorded by this meter.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyEnergyVIF
+ add_combinable = BackwardFlow
+ }
+ }
+ field {
+ name = power
+ quantity = Power
+ info = 'The current power consumption.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyPowerVIF
+ }
+ }
+ field {
+ name = target
+ quantity = PointInTime
+ info = 'Date and time when the previous billing period ended.'
+ display_unit = datetime
+ match {
+ measurement_type = Instantaneous
+ vif_range = DateTime
+ storage_nr = 16
+ }
+ }
+ field {
+ name = target_heat_energy
+ quantity = Energy
+ info = 'The heating energy consumption recorded at the end of the previous billing period.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyEnergyVIF
+ add_combinable = ForwardFlow
+ storage_nr = 16
+ }
+ }
+ field {
+ name = target_cooling_energy
+ quantity = Energy
+ info = 'The cooling energy consumption recorded at the end of the previous billing period.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyEnergyVIF
+ add_combinable = BackwardFlow
+ storage_nr = 16
+ }
+ }
+ }
+ tests {
+ test {
+ args = 'qualco qualcosonic 03016408 NOKEY'
+ telegram = 76440907086401030B0d7a78000000046d030fB726346d0000010134fd170000000004200f13cf0104240f13cf0104863B0000000004863cdc0000000413B5150600042B86f1ffff043B030200000259c002025d2c05026194fd0c780864010384086d3B17Bf258408863B000000008408863c0B000000
+ json = '{"_":"telegram","media":"heat/cooling load","driver":"qualcosonic","name":"qualco","id":"03016408","fabrication_no":"03016408","operating_time_h":8430.013056,"on_time_h":8430.013056,"meter_datetime":"2021-06-23 15:03","meter_datetime_at_error":"2000-01-01 00:00","total_m3":398.773,"flow_temperature_c":7.04,"return_temperature_c":13.24,"flow_return_temperature_difference_c":-6.2,"volume_flow_m3h":0.515,"status":"OK","total_heat_energy_kwh":0,"total_cooling_energy_kwh":220,"power_kw":-3.706,"target_datetime":"2021-05-31 23:59","target_heat_energy_kwh":0,"target_cooling_energy_kwh":11,"timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'qualco;03016408;OK;0;220;-3.706;2021-05-31 23:59;0;11;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'qualcoe4 qualcosonic 29481002 NOKEY'
+ telegram = 76440907021048290C0D7A89000000_046D2301E122346D0000010134FD17000000000420097468020424DE736802048E3BF1190000048E3C00000000041323860100042B6E050000043B2500000002598016025D000A0261800C0C780210482984086D3B17FF2184088E3BED19000084088E3C00000000
+ json = '{"_":"telegram","media":"heat/cooling load","driver":"qualcosonic","name":"qualcoe4","id":"29481002","fabrication_no":"29481002","operating_time_h":11222.177222,"on_time_h":11222.189167,"meter_datetime":"2023-02-01 01:35","meter_datetime_at_error":"2000-01-01 00:00","total_m3":99.875,"flow_temperature_c":57.6,"return_temperature_c":25.6,"flow_return_temperature_difference_c":32,"volume_flow_m3h":0.037,"status":"OK","total_heat_energy_kwh":1844.722222,"total_cooling_energy_kwh":0,"power_kw":1.39,"target_datetime":"2023-01-31 23:59","target_heat_energy_kwh":1843.611111,"target_cooling_energy_kwh":0,"timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'qualcoe4;29481002;OK;1844.722222;0;1.39;2023-01-31 23:59;1843.611111;0;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'qualcoe4long qualcosonic 98499485 NOKEY'
+ telegram = 9A440907859449980C0D7A8C000000_046D3201E122346D0000010134FD17000000000420BB776802042490776802048E3BF2190000048E3C00000000041329860100042B6A010000043B0B0000000259F615025D1E0B0261D80A0C788594499884086D3B17FF21820859BC1382085D720B840824915D680284088E3BED19000084088E3C00000000840813028601008408BE5800000000027F00AD
+ json = '{"_":"telegram","media":"heat/cooling load","driver":"qualcosonic","name":"qualcoe4long","id":"98499485","fabrication_no":"98499485","operating_time_h":11222.44,"on_time_h":11222.451944,"meter_datetime":"2023-02-01 01:50","meter_datetime_at_error":"2000-01-01 00:00","total_m3":99.881,"flow_temperature_c":56.22,"return_temperature_c":28.46,"flow_return_temperature_difference_c":27.76,"volume_flow_m3h":0.011,"status":"OK","total_heat_energy_kwh":1845,"total_cooling_energy_kwh":0,"power_kw":0.362,"target_datetime":"2023-01-31 23:59","target_heat_energy_kwh":1843.611111,"target_cooling_energy_kwh":0,"timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'qualcoe4long;98499485;OK;1845;0;0.362;2023-01-31 23:59;1843.611111;0;1111-11-11 11:11.11'
+ }
+ }
+}
diff --git a/components/wmbus_common/drivers/src/qwaterv2.xmq b/components/wmbus_common/drivers/src/qwaterv2.xmq
new file mode 100644
index 00000000..9bc92788
--- /dev/null
+++ b/components/wmbus_common/drivers/src/qwaterv2.xmq
@@ -0,0 +1,409 @@
+// Copyright (C) 2026 Fredrik Öhrström (gpl-3.0-or-later)
+driver {
+ name = qwaterv2
+ meter_type = WaterMeter
+ default_fields = name,id,status,total_m3,timestamp
+ manufacturer = Qundis
+ transform_payload = try_qundis_decode
+ detect {
+ mvt = QDS,33,37
+ mvt = QDS,35,06
+ mvt = QDS,35,07
+ mvt = QDS,35,37
+ mvt = QDS,16,06
+ mvt = QDS,16,07
+ mvt = QDS,17,06
+ mvt = QDS,17,07
+ mvt = QDS,18,06
+ mvt = QDS,18,07
+ mvt = QDS,19,06
+ mvt = QDS,19,07
+ mvt = QDS,1A,06
+ mvt = QDS,1A,07
+ mvt = QDS,1D,06
+ mvt = QDS,1D,07
+ mvt = QDS,36,06
+ mvt = QDS,36,07
+ mvt = LSE,19,07
+ mvt = LSE,1A,06
+ mvt = LSE,1A,07
+ mvt = LSE,1D,06
+ mvt = LSE,1D,07
+ mvt = LSE,33,37
+ mvt = LSE,35,37
+ mvt = LSE,36,06
+ mvt = LSE,36,07
+ }
+ library {
+ use = total_m3
+ use = meter_datetime
+ use = enhanced_id
+ }
+ fields {
+ field {
+ name = status
+ quantity = Text
+ info = 'Status and error flags'
+ attributes = STATUS,INCLUDE_TPL_STATUS
+ match {
+ measurement_type = Instantaneous
+ vif_range = ErrorFlags
+ }
+ lookup {
+ name = ERROR_FLAGS
+ map_type = BitToString
+ mask_bits = 0xffff
+ default_message = OK
+ map {
+ name = NO_FLOW
+ // F0
+ value = 0x0001
+ test = Set
+ }
+ map {
+ name = SUPPLY_SENSOR_INTERRUPTED
+ // F1
+ value = 0x0002
+ test = Set
+ }
+ map {
+ name = RETURN_SENSOR_INTERRUPTED
+ // F2
+ value = 0x0004
+ test = Set
+ }
+ map {
+ name = TEMPERATURE_ELECTRONICS_ERROR
+ // F3
+ value = 0x0008
+ test = Set
+ }
+ map {
+ name = BATTERY_VOLTAGE_ERROR
+ // F4
+ value = 0x0010
+ test = Set
+ }
+ map {
+ name = SHORT_CIRCUIT_SUPPLY_SENSOR
+ // F5
+ value = 0x0020
+ test = Set
+ }
+ map {
+ name = SHORT_CIRCUIT_RETURN_SENSOR
+ // F6
+ value = 0x0040
+ test = Set
+ }
+ map {
+ name = MEMORY_ERROR
+ // F7
+ value = 0x0080
+ test = Set
+ }
+ map {
+ name = SABOTAGE
+ // F8 - F1,2,3,5,6 longer than 8 hours, latching error, no more measurements performed.
+ value = 0x0100
+ test = Set
+ }
+ map {
+ name = ELECTRONICS_ERROR
+ // F9
+ value = 0x0200
+ test = Set
+ }
+ }
+ }
+ field {
+ name = target
+ quantity = Volume
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyVolumeVIF
+ storage_nr = 17
+ }
+ }
+ field {
+ name = volume_flow
+ quantity = Flow
+ match {
+ measurement_type = Instantaneous
+ vif_range = VolumeFlow
+ add_combinable = DurationExceedsLowerLast
+ }
+ }
+ field {
+ name = target
+ quantity = PointInTime
+ display_unit = date
+ match {
+ measurement_type = Instantaneous
+ vif_range = Date
+ storage_nr = 17
+ }
+ }
+ field {
+ name = target_year
+ quantity = Volume
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyVolumeVIF
+ storage_nr = 1
+ }
+ }
+ field {
+ name = target_year
+ quantity = PointInTime
+ display_unit = date
+ match {
+ measurement_type = Instantaneous
+ vif_range = Date
+ storage_nr = 1
+ }
+ }
+ field {
+ name = device_error
+ quantity = PointInTime
+ display_unit = date
+ match {
+ measurement_type = AtError
+ vif_range = Date
+ }
+ }
+ field {
+ name = mfct_specific_data
+ quantity = Text
+ attributes = HIDE
+ match {
+ difvifkey = 0DFF5F
+ }
+ // Siemens/Qundis WalkByDataSet block: DIF=0D (variable-len), VIF=FF (mfct), VIFE=5F, 53 bytes.
+ // 9-byte proprietary header (skipped as quad, quad, byte):
+ // [0] 0x00 reserved
+ // [1] 0x82 wbdsId: Type 2 WalkByDataSet
+ // [2] counter rolling transmission counter (increments each send)
+ // [3] 0x00 reserved
+ // [4] 0x00 reserved
+ // [5] flags device-specific status/flags (format unknown)
+ // [6] 0x00/01 extra flag
+ // [7] 0x07 constant (possibly number of data records)
+ // [8] 0xC1 meter type: 0xB0=HCA, 0xC0=heat, 0xC1=warm/cold water
+ // Byte [9] is the VIF (0x13 = volume in liters), which is matched below.
+ // The 9-byte header is validated against its fixed bytes ([0]=00, [1]=82,
+ // [3]=00, [4]=00, [7]=07, [8]=C1 warm/cold water) so that a block with an
+ // unexpected layout (e.g. an encrypted/non-walk-by payload that merely
+ // happens to carry the 0DFF5F difvifkey) fails closed instead of having
+ // total_m3/target_*/volume_delta_* invented from the wrong offsets.
+ // See issue #1916.
+ ixml = "decode = -'00', -'82', byte, -'00', -'00', byte, byte, -'07', -'C1', volume_l.
+
+ volume_l = '13', error_date, total_l, target_year_date, target_year_l, target_date, target_l, delta1, delta2, delta3, delta4, delta5, delta6, delta7, delta8, delta9, delta10, delta11, delta12, -pad.
+
+ error_date = word, @DV_426C.
+ target_year_date = word, @DV_426C.
+ target_date = word, @DV_C2086C.
+
+ total_l = quad, @DV_0C13.
+ target_year_l = quad, @DV_4C13.
+ target_l = quad, @DV_CC0813.
+
+ delta1 = word, @DV_820113.
+ delta2 = word, @DV_C20113.
+ delta3 = word, @DV_820213.
+ delta4 = word, @DV_C20213.
+ delta5 = word, @DV_820313.
+ delta6 = word, @DV_C20313.
+ delta7 = word, @DV_820413.
+ delta8 = word, @DV_C20413.
+ delta9 = word, @DV_820513.
+ delta10 = word, @DV_C20513.
+ delta11 = word, @DV_820613.
+ delta12 = word, @DV_C20613.
+ -pad = byte.
+
+ -hex = ['A'-'F';'0'-'9'].
+ -byte = hex, hex.
+ -word = byte, byte.
+ -triplet = byte, byte, byte.
+ -quad = byte, byte, byte, byte.
+
+ DV_0C13>dvk = +'0C13'.
+ DV_4C13>dvk = +'4C13'.
+
+ DV_CC0813>dvk = +'CC0813'.
+
+ DV_426C>dvk = +'426C'.
+ DV_C2086C>dvk = +'C2086C'.
+
+ DV_820113>dvk = +'820113'.
+ DV_C20113>dvk = +'C20113'.
+ DV_820213>dvk = +'820213'.
+ DV_C20213>dvk = +'C20213'.
+ DV_820313>dvk = +'820313'.
+ DV_C20313>dvk = +'C20313'.
+ DV_820413>dvk = +'820413'.
+ DV_C20413>dvk = +'C20413'.
+ DV_820513>dvk = +'820513'.
+ DV_C20513>dvk = +'C20513'.
+ DV_820613>dvk = +'820613'.
+ DV_C20613>dvk = +'C20613'."
+ }
+ field {
+ name = 'volume_delta_{storage_counter}'
+ quantity = Volume
+ info = 'Monthly volume delta.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyVolumeVIF
+ storage_nr = 2,13
+ }
+ }
+ }
+ tests {
+ test {
+ args = 'MyQWater qwaterv2 12353648 NOKEY'
+ telegram = 3b4493444836351218067ac70000200c13911900004c1391170000426cbf2ccc081391170000c2086cbf2c02bb560000326cffff046d1e02de21fed0
+ json = '{"_": "telegram","device_error_date":null,"id": "12353648","media": "warm water","driver": "qwaterv2","meter_datetime": "2022-01-30 02:30","name": "MyQWater","status": "OK","target_date": "2021-12-31","target_m3": 1.791,"target_year_date": "2021-12-31","target_year_m3": 1.791,"timestamp": "1111-11-11T11:11:11Z","total_m3": 1.991,"volume_flow_m3h": 0}'
+ fields = 'MyQWater;12353648;OK;1.991;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'MyQWater qwaterv2 12353648 NOKEY'
+ comment = 'A walk by telegram with mfct data.'
+ telegram = 4b449344483635121806780dff5f350082da0000600107c113ffff48200000bf2c91170000df2120200000008001000000060019001000160018000d001300350017002f046d370cc422c759
+ json = '{"_": "telegram","id": "12353648","media": "warm water","driver": "qwaterv2","meter_datetime": "2022-02-04 12:55","name": "MyQWater","status": "OK","target_date": "2022-01-31","target_m3":2.02,"target_year_date": "2021-12-31","target_year_m3": 1.791,"timestamp": "1111-11-11T11:11:11Z","total_m3": 2.048,"volume_delta_2_m3":-32.768,"volume_delta_3_m3":0.001,"volume_delta_4_m3":0,"volume_delta_5_m3":0.006,"volume_delta_6_m3":0.025,"volume_delta_7_m3":0.016,"volume_delta_8_m3":0.022,"volume_delta_9_m3":0.024,"volume_delta_10_m3":0.013,"volume_delta_11_m3":0.019,"volume_delta_12_m3":0.053,"volume_delta_13_m3":0.023}'
+ fields = 'MyQWater;12353648;OK;2.048;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'AnotherQWater qwaterv2 66666666 NOKEY'
+ telegram = 3C449344682268363537726666666693443507720000200C13670512004C1361100300426CBF2CCC081344501100C2086CDF28326CFFFF046D0813CF29
+ json = '{"_":"telegram","media":"water","driver":"qwaterv2","name":"AnotherQWater","id":"66666666","device_error_date":null,"target_m3":115.044,"target_date":"2022-08-31","target_year_m3":31.061,"target_year_date":"2021-12-31","total_m3":120.567,"meter_datetime":"2022-09-15 19:08","status":"OK","timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'AnotherQWater;66666666;OK;120.567;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'YetAnoter qwaterv2 33333333 NOKEY'
+ telegram = 3C449344333333333537723333333393443506B8000020_0C13350000004C1300000000426CBF2CCC081300000000C2086CDF25326CFFFF046D0516CE262
+ json = '{"_":"telegram","media":"warm water","driver":"qwaterv2","name":"YetAnoter","id":"33333333","status":"OK","total_m3":0.035,"target_year_m3":0,"target_year_date":"2021-12-31","target_m3":0,"target_date":"2022-05-31","device_error_date":null,"meter_datetime":"2022-06-14 22:05","timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'YetAnoter;33333333;OK;0.035;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'QWater-7-18 qwaterv2 12230094 NOKEY'
+ telegram = 394493449400231218077ad30000200c13536712004c1307920500426cBf2ccc081373621200c2086cde2B02BB560000326cffff046d3714c32c
+ json = '{"_":"telegram","media":"water","driver":"qwaterv2","name":"QWater-7-18","id":"12230094","status":"OK","total_m3":126.753,"target_year_m3":59.207,"target_year_date":"2021-12-31","target_m3":126.273,"target_date":"2022-11-30","device_error_date":null,"volume_flow_m3h":0,"meter_datetime":"2022-12-03 20:55","timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'QWater-7-18;12230094;OK;126.753;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'QWoo qwaterv2 13144514 NOKEY'
+ coment = 'Warm water'
+ telegram = 394493441445141316067A31000020_0C13671605004C1348160500426CDF2CCC081348160500C2086CDF2C02BB560000326CFFFF046D3414E121
+ json = '{"_":"telegram","target_date": "2022-12-31","target_m3": 51.648,"target_year_date": "2022-12-31","target_year_m3": 51.648,"device_error_date":null,"id": "13144514","media": "warm water","driver": "qwaterv2","meter_datetime": "2023-01-01 20:52","name": "QWoo","status": "OK","timestamp": "1111-11-11T11:11:11Z","total_m3": 51.667,"volume_flow_m3h": 0}'
+ fields = 'QWoo;13144514;OK;51.667;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'QWooo qwaterv2 13176890 NOKEY'
+ comment = 'Cold water'
+ telegram = 394493449068171316077A0B000020_0C13358612004C1307851200426CDF2CCC081307851200C2086CDF2C02BB560000326CFFFF046D3014E121
+ json = '{"_":"telegram","target_date": "2022-12-31","target_m3": 128.507,"target_year_date": "2022-12-31","target_year_m3": 128.507,"device_error_date":null,"id": "13176890","media": "water","driver": "qwaterv2","meter_datetime": "2023-01-01 20:48","name": "QWooo","status": "OK","timestamp": "1111-11-11T11:11:11Z","total_m3": 128.635,"volume_flow_m3h": 0}'
+ fields = 'QWooo;13176890;OK;128.635;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'QWooo qwaterv2 78563412 NOKEY'
+ comment = 'Proprietary Q walk-by message'
+ telegram = 49449344123456781606780DFF5F3500824E00007F0007C113FFFF63961300DF2C82731200FE2463811300A400F200D100A900DD00E000E90006011601EA0027010F012F046D0211F225
+ json = '{"_":"telegram","id": "78563412","media": "warm water","driver": "qwaterv2","meter_datetime": "2023-05-18 17:02","name": "QWooo","status": "OK","target_date": "2023-04-30","target_m3": 138.163,"target_year_date": "2022-12-31","target_year_m3": 127.382,"timestamp": "1111-11-11T11:11:11Z","total_m3": 139.663,"volume_delta_2_m3":0.164,"volume_delta_3_m3":0.242,"volume_delta_4_m3":0.209,"volume_delta_5_m3":0.169,"volume_delta_6_m3":0.221,"volume_delta_7_m3":0.224,"volume_delta_8_m3":0.233,"volume_delta_9_m3":0.262,"volume_delta_10_m3":0.278,"volume_delta_11_m3":0.234,"volume_delta_12_m3":0.295,"volume_delta_13_m3":0.271}'
+ fields = 'QWooo;78563412;OK;139.663;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'QWaaa qwaterv2 51220588 NOKEY'
+ telegram = 4944934488052251190778_0DFF5F350082930000810007C113FFFF91670400FF2C265402001E34332204000000EE00F201A501DB01C1015401B70178019701B901C9012F046D06091D35
+ json = '{"_": "telegram","driver": "qwaterv2","id": "51220588","media": "water","meter_datetime": "2024-05-29 09:06","name": "QWaaa","status": "OK","target_date": "2024-04-30","target_m3": 42.233,"target_year_date": "2023-12-31","target_year_m3": 25.426,"timestamp": "1111-11-11T11:11:11Z","total_m3": 46.791,"volume_delta_10_m3": 0.376,"volume_delta_11_m3": 0.407,"volume_delta_12_m3": 0.441,"volume_delta_13_m3": 0.457,"volume_delta_2_m3": 0,"volume_delta_3_m3": 0.238,"volume_delta_4_m3": 0.498,"volume_delta_5_m3": 0.421,"volume_delta_6_m3": 0.475,"volume_delta_7_m3": 0.449,"volume_delta_8_m3": 0.34,"volume_delta_9_m3": 0.439}'
+ fields = 'QWaaa;51220588;OK;46.791;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'QWccc qwaterv2 13492674 NOKEY'
+ telegram = 394493447426491317077ADD0000200C13975710004C1330970700426CFF2CCC081387471000C2086C1E3B02BB560000326CFFFF046D1E0F113C
+ json = '{"_": "telegram","device_error_date": null,"driver": "qwaterv2","id": "13492674","media": "water","meter_datetime": "2024-12-17 15:30","name": "QWccc","status": "OK","target_date": "2024-11-30","target_m3": 104.787,"target_year_date": "2023-12-31","target_year_m3": 79.73,"timestamp": "1111-11-11T11:11:11Z","total_m3": 105.797,"volume_flow_m3h": 0}'
+ fields = 'QWccc;13492674;OK;105.797;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'QWddd qwaterv2 13334995 NOKEY'
+ telegram = 49449344954933131706780DFF5F3500823A0000600107C113FFFF29970300FF2C580303001E3B269703000A006500750073005B0070007D0061005B004200160000002F046D1E0F113C
+ json = '{"_": "telegram","driver": "qwaterv2","id": "13334995","media": "warm water","meter_datetime": "2024-12-17 15:30","name": "QWddd","status": "OK","target_date": "2024-11-30","target_m3": 39.726,"target_year_date": "2023-12-31","target_year_m3": 30.358,"timestamp": "1111-11-11T11:11:11Z","total_m3": 39.729,"volume_delta_10_m3": 0.091,"volume_delta_11_m3": 0.066,"volume_delta_12_m3": 0.022,"volume_delta_13_m3": 0,"volume_delta_2_m3": 0.01,"volume_delta_3_m3": 0.101,"volume_delta_4_m3": 0.117,"volume_delta_5_m3": 0.115,"volume_delta_6_m3": 0.091,"volume_delta_7_m3": 0.112,"volume_delta_8_m3": 0.125,"volume_delta_9_m3": 0.097}'
+ fields = 'QWddd;13334995;OK;39.729;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'QQ1 qwaterv2 37439212 NOKEY'
+ telegram = 5344934412924337353778077912924337934435070DFF5F3500828A0000100007C113FFFF966600001F3C000000003E3419580000008000800080008000800080008000005A0094009C00BB002F046D010F3235
+ json = '{"_": "telegram","driver": "qwaterv2","enhanced_id": "0735449337439212","id": "37439212","media": "radio converter (meter side)","meter_datetime": "2025-05-18 15:01","name": "QQ1","status": "OK","target_date": "2025-04-30","target_m3": 5.819,"target_year_date": "2024-12-31","target_year_m3": 0,"timestamp": "1111-11-11T11:11:11Z","total_m3": 6.696,"volume_delta_10_m3": 0.09,"volume_delta_11_m3": 0.148,"volume_delta_12_m3": 0.156,"volume_delta_13_m3": 0.187,"volume_delta_2_m3": -32.768,"volume_delta_3_m3": -32.768,"volume_delta_4_m3": -32.768,"volume_delta_5_m3": -32.768,"volume_delta_6_m3": -32.768,"volume_delta_7_m3": -32.768,"volume_delta_8_m3": -32.768,"volume_delta_9_m3": 0}'
+ fields = 'QQ1;37439212;OK;6.696;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'QQ2 qwaterv2 37432649 NOKEY'
+ telegram = 5344934449264337353778077949264337934435070DFF5F350082560000110007C113FFFF245300001F3C210400003E348946000000800080008000800080008000002A0066005F00730072002F046D000F3235
+ json = '{"_": "telegram","driver": "qwaterv2","enhanced_id": "0735449337432649","id": "37432649","media": "radio converter (meter side)","meter_datetime": "2025-05-18 15:00","name": "QQ2","status": "OK","target_date": "2025-04-30","target_m3": 4.689,"target_year_date": "2024-12-31","target_year_m3": 0.421,"timestamp": "1111-11-11T11:11:11Z","total_m3": 5.324,"volume_delta_10_m3": 0.102,"volume_delta_11_m3": 0.095,"volume_delta_12_m3": 0.115,"volume_delta_13_m3": 0.114,"volume_delta_2_m3": -32.768,"volume_delta_3_m3": -32.768,"volume_delta_4_m3": -32.768,"volume_delta_5_m3": -32.768,"volume_delta_6_m3": -32.768,"volume_delta_7_m3": -32.768,"volume_delta_8_m3": 0,"volume_delta_9_m3": 0.042}'
+ fields = 'QQ2;37432649;OK;5.324;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'QQ3 qwaterv2 60101441 NOKEY'
+ telegram = 39449344411410601A067ADB000020_0C13780000004C1300000000426CFFFFCC081335000000C2086C3F3502BB560000326CFFFF046D14173136
+ json = '{"_": "telegram","device_error_date": null,"driver": "qwaterv2","id": "60101441","media": "warm water","meter_datetime": "2025-06-17 23:20","name": "QQ3","status": "OK","target_date": "2025-05-31","target_m3": 0.035,"target_year_date": null,"target_year_m3": 0,"timestamp": "1111-11-11T11:11:11Z","total_m3": 0.078,"volume_flow_m3h": 0}'
+ fields = 'QQ3;60101441;OK;0.078;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'QQ4 qwaterv2 60113189 NOKEY'
+ telegram = 39449344893111601a077a580000200c13200300004c1300000000426cffffcc081334000000c2086c3f3502BB560000326cffff046d13173136
+ json = '{"_": "telegram","device_error_date": null,"driver": "qwaterv2","id": "60113189","media": "water","meter_datetime": "2025-06-17 23:19","name": "QQ4","status": "OK","target_date": "2025-05-31","target_m3": 0.034,"target_year_date": null,"target_year_m3": 0,"timestamp": "1111-11-11T11:11:11Z","total_m3": 0.32,"volume_flow_m3h": 0}'
+ fields = 'QQ4;60113189;OK;0.32;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'QQ5 qwaterv2 61193115 NOKEY'
+ comment = 'Qundis Qwater5.5 Model 2026 (warm water)'
+ telegram = 39449344153119611D067A870000200C13370000004C1300000000426CFFFFCC081300000000C2086CFFFF02BB560000326CFFFF046D320D5E31
+ json = '{"_": "telegram","device_error_date": null,"driver": "qwaterv2","id": "61193115","media": "warm water","meter_datetime": "2026-01-30 13:50","name": "QQ5","status": "OK","target_date": null,"target_m3": 0,"target_year_date": null,"target_year_m3": 0,"timestamp": "1111-11-11T11:11:11Z","total_m3": 0.037,"volume_flow_m3h": 0}'
+ fields = 'QQ5;61193115;OK;0.037;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'QQ5 qwaterv2 61193115 NOKEY'
+ telegram = 49449344153119611D06780DFF5F3500826E0000F00007C113FFFF66000000FFFF00000000FFFF000000000080008000800080008000800080008000800080008000802F046D33125F31
+ json = '{"_": "telegram","driver": "qwaterv2","id": "61193115","media": "warm water","meter_datetime": "2026-01-31 18:51","name": "QQ5","status": "OK","target_date": null,"target_m3": 0,"target_year_date": null,"target_year_m3": 0,"timestamp": "1111-11-11T11:11:11Z","total_m3": 0.066,"volume_delta_10_m3": -32.768,"volume_delta_11_m3": -32.768,"volume_delta_12_m3": -32.768,"volume_delta_13_m3": -32.768,"volume_delta_2_m3": -32.768,"volume_delta_3_m3": -32.768,"volume_delta_4_m3": -32.768,"volume_delta_5_m3": -32.768,"volume_delta_6_m3": -32.768,"volume_delta_7_m3": -32.768,"volume_delta_8_m3": -32.768,"volume_delta_9_m3": -32.768}'
+ fields = 'QQ5;61193115;OK;0.066;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'QQ5 qwaterv2 61193115 NOKEY'
+ telegram = 49449344153119611D06780DFF5F350082EC0000F10007C113FFFF69000000FFFF000000005F31680000000080008000800080008000800080008000800080008006002F046D1C0A4132
+ json = '{"_": "telegram","driver": "qwaterv2","id": "61193115","media": "warm water","meter_datetime": "2026-02-01 10:28","name": "QQ5","status": "OK","target_date": "2026-01-31","target_m3": 0.068,"target_year_date": null,"target_year_m3": 0,"timestamp": "1111-11-11T11:11:11Z","total_m3": 0.069,"volume_delta_10_m3": -32.768,"volume_delta_11_m3": -32.768,"volume_delta_12_m3": -32.768,"volume_delta_13_m3": 0.006,"volume_delta_2_m3": -32.768,"volume_delta_3_m3": -32.768,"volume_delta_4_m3": -32.768,"volume_delta_5_m3": -32.768,"volume_delta_6_m3": -32.768,"volume_delta_7_m3": -32.768,"volume_delta_8_m3": -32.768,"volume_delta_9_m3": -32.768}'
+ fields = 'QQ5;61193115;OK;0.069;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'MyQWater19 qwaterv2 51341234 NOKEY'
+ comment = 'Version 19 warm water meter, walk by telegram with mfct data.'
+ telegram = 49449344341234511906780dff5f3500822d0000600107c113ffff748702003f3c215102005f374386020031003600330033003600360031003600320037002e002c002f046d0f114b38
+ json = '{"_":"telegram","media":"warm water","driver":"qwaterv2","name":"MyQWater19","id":"51341234","target_m3":28.643,"target_date":"2026-07-31","target_year_m3":25.121,"target_year_date":"2025-12-31","total_m3":28.774,"volume_delta_2_m3":0.049,"volume_delta_3_m3":0.054,"volume_delta_4_m3":0.051,"volume_delta_5_m3":0.051,"volume_delta_6_m3":0.054,"volume_delta_7_m3":0.054,"volume_delta_8_m3":0.049,"volume_delta_9_m3":0.054,"volume_delta_10_m3":0.05,"volume_delta_11_m3":0.055,"volume_delta_12_m3":0.046,"volume_delta_13_m3":0.044,"meter_datetime":"2026-08-11 17:15","status":"OK","timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'MyQWater19;51341234;OK;28.774;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'AESTest qwaterv2 14356101 000102030405060708090A0B0C0D0E0F'
+ comment = 'Encrypted Q walk-by block (header byte[4]=0x35). AES-128-CBC EN 13757-7 Mode 5 with the configured key, Mode-5 IV (M-field+A-field+ACC*8, ACC=header byte[2]). The body decrypts to the legacy plaintext layout and is parsed by the unchanged ixml grammar. The telegram and key are synthetic (not real Qundis material) so the test is self-contained. See issue #1916/#2025.'
+ telegram = 49449344016135141907780DFF5F350082C100353E44AAFEDB237BDBA569404984A09F6DD12E32C2F03DD335E259DD446711256309CC4E894B4C7D8CC427C7491B94FF05046D160A4F38
+ json = '{"_": "telegram","id": "14356101","media": "water","driver": "qwaterv2","meter_datetime": "2026-08-15 10:22","name": "AESTest","status": "OK","target_date":null,"target_m3": 0.03,"target_year_date":null,"target_year_m3": 0.02,"timestamp": "1111-11-11T11:11:11Z","total_m3": 0.01,"volume_delta_10_m3": 0.009,"volume_delta_11_m3": 0.01,"volume_delta_12_m3": 0.011,"volume_delta_13_m3": 0.012,"volume_delta_2_m3": 0.001,"volume_delta_3_m3": 0.002,"volume_delta_4_m3": 0.003,"volume_delta_5_m3": 0.004,"volume_delta_6_m3": 0.005,"volume_delta_7_m3": 0.006,"volume_delta_8_m3": 0.007,"volume_delta_9_m3": 0.008}'
+ fields = 'AESTest;14356101;OK;0.01;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'Kaltwasser qwaterv2 14356101 NOKEY'
+ comment = 'Q Water 5.5 walk-by block with an unsupported layout: no 13FFFF volume marker after the 9-byte header. The ixml grammar must fail closed and must not invent total_m3/target_*/volume_delta_* values from a block it cannot decode. Only the trailing 046D meter_datetime record is real. See issue #1916.'
+ telegram = 49449344016135141907780DFF5F350082C100352CAA738213487EAF84ED6BFD4DA668870285651771A8D3CFB7DE5ECC22081CDD1A3FFBA6506F901027DB29B4672708D9046D160A4F38
+ json = '{"_": "telegram","id": "14356101","media": "water","driver": "qwaterv2","meter_datetime": "2026-08-15 10:22","name": "Kaltwasser","status": "MISSING_KEY","timestamp": "1111-11-11T11:11:11Z"}'
+ fields = 'Kaltwasser;14356101;MISSING_KEY;null;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'RealKeyWater qwaterv2 60170138 5787C5961E48C039A225D4DB4F8ECF06'
+ comment = 'Real encrypted Q Water 5.5 walk-by block with the real key that the meter owner published in issue #1497: proves the AES path end-to-end with genuine key material (the AESTest above uses a synthetic key). The expected values match what the owner posted in #1497. The fail-closed counterpart without a key is covered by Kaltwasser.'
+ telegram = 49449344380117601A07780DFF5F350082B50035512F79A321C70F8FD90041D03F7633718457556C82E3A6CB5A20617A9523867033C80F9D21054140A90BC438C04F1E9D046D3B125832
+ json = '{"_":"telegram","driver":"qwaterv2","id":"60170138","media":"water","meter_datetime":"2026-02-24 18:59","name":"RealKeyWater","status":"OK","target_date":"2026-01-31","target_m3":24.086,"target_year_date":"2025-12-31","target_year_m3":20.552,"timestamp":"1111-11-11T11:11:11Z","total_m3":26.546,"volume_delta_10_m3":0.338,"volume_delta_11_m3":0.338,"volume_delta_12_m3":0.363,"volume_delta_13_m3":0.353,"volume_delta_2_m3":-32.768,"volume_delta_3_m3":-32.768,"volume_delta_4_m3":-32.768,"volume_delta_5_m3":-32.768,"volume_delta_6_m3":0.003,"volume_delta_7_m3":0.334,"volume_delta_8_m3":0.343,"volume_delta_9_m3":0.336}'
+ fields = 'RealKeyWater;60170138;OK;26.546;1111-11-11 11:11.11'
+ }
+ }
+}
diff --git a/components/wmbus_common/drivers/src/relhca.xmq b/components/wmbus_common/drivers/src/relhca.xmq
index 15e7a616..c92d129a 100644
--- a/components/wmbus_common/drivers/src/relhca.xmq
+++ b/components/wmbus_common/drivers/src/relhca.xmq
@@ -30,7 +30,7 @@ driver {
args = 'MyRelHCA relhca 00000104 NOKEY'
comment = 'Telegram 1'
telegram = 682f2f6808687204010000ac484100b30000000c6e41648401046d9c021732426cff2c4c6e4164840142ec7e1f3c0fc101010c6d16
- json = '{"_":"telegram","consumption_hca": 1846441,"id": "00000104","media": "other","meter": "relhca","meter_datetime": "2024-02-23 02:28","target_hca": 1846441,"name": "MyRelHCA","next_target_date": "2024-12-31","target_date": "2023-12-31","timestamp": "1111-11-11T11:11:11Z"}'
+ json = '{"_":"telegram","consumption_hca": 1846441,"id": "00000104","media": "other","driver": "relhca","meter_datetime": "2024-02-23 02:28","target_hca": 1846441,"name": "MyRelHCA","next_target_date": "2024-12-31","target_date": "2023-12-31","timestamp": "1111-11-11T11:11:11Z"}'
fields = 'MyRelHCA;00000104;1846441;1111-11-11 11:11.11'
}
}
diff --git a/components/wmbus_common/drivers/src/rfmamb.xmq b/components/wmbus_common/drivers/src/rfmamb.xmq
new file mode 100644
index 00000000..dc450e69
--- /dev/null
+++ b/components/wmbus_common/drivers/src/rfmamb.xmq
@@ -0,0 +1,214 @@
+// Copyright (C) 2019-2026 Fredrik Öhrström (gpl-3.0-or-later)
+driver {
+ name = rfmamb
+ meter_type = TempHygroMeter
+ default_fields = name,id,current_temperature_c,current_relative_humidity_rh,timestamp
+ manufacturer = BMeters
+ detect {
+ mvt = BMT,10,1b
+ }
+ mfct_tpl_status_bits {
+ mask_bits = 0x7f
+ default_message = OK
+ map {
+ name = APP_BUSY
+ value = 0x01
+ test = set
+ }
+ map {
+ name = APP_ERROR
+ value = 0x02
+ test = set
+ }
+ map {
+ name = LOW_BATTERY
+ value = 0x04
+ test = set
+ }
+ map {
+ name = MODULE_REMOVED
+ value = 0x08
+ test = set
+ }
+ map {
+ name = SENSOR_READ_ERROR
+ value = 0x10
+ test = set
+ }
+ map {
+ name = TEMP_OUT_OF_RANGE
+ value = 0x20
+ test = set
+ }
+ map {
+ name = RH_OUT_OF_RANGE
+ value = 0x40
+ test = set
+ }
+ }
+ fields {
+ field {
+ name = status
+ quantity = Text
+ info = 'Status and error flags.'
+ attributes = STATUS,INCLUDE_TPL_STATUS
+ }
+ field {
+ name = current_temperature
+ quantity = Temperature
+ info = 'The current temperature.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = ExternalTemperature
+ }
+ }
+ field {
+ name = average_temperature_1h
+ quantity = Temperature
+ info = 'The average temperature over the last hour.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = ExternalTemperature
+ storage_nr = 1
+ }
+ }
+ field {
+ name = average_temperature_24h
+ quantity = Temperature
+ info = 'The average temperature over the last 24 hours.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = ExternalTemperature
+ storage_nr = 2
+ }
+ }
+ field {
+ name = maximum_temperature_1h
+ quantity = Temperature
+ info = 'The maximum temperature over the last hour.'
+ match {
+ measurement_type = Maximum
+ vif_range = ExternalTemperature
+ }
+ }
+ field {
+ name = maximum_temperature_24h
+ quantity = Temperature
+ info = 'The maximum temperature over the last 24 hours.'
+ match {
+ measurement_type = Maximum
+ vif_range = ExternalTemperature
+ storage_nr = 1
+ }
+ }
+ field {
+ name = minimum_temperature_1h
+ quantity = Temperature
+ info = 'The minimum temperature over the last hour.'
+ match {
+ measurement_type = Minimum
+ vif_range = ExternalTemperature
+ }
+ }
+ field {
+ name = minimum_temperature_24h
+ quantity = Temperature
+ info = 'The minimum temperature over the last 24 hours.'
+ match {
+ measurement_type = Minimum
+ vif_range = ExternalTemperature
+ storage_nr = 1
+ }
+ }
+ field {
+ name = current_relative_humidity
+ quantity = RelativeHumidity
+ info = 'The current relative humidity.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = RelativeHumidity
+ }
+ }
+ field {
+ name = average_relative_humidity_1h
+ quantity = RelativeHumidity
+ info = 'The average relative humidity over the last hour.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = RelativeHumidity
+ storage_nr = 1
+ }
+ }
+ field {
+ name = average_relative_humidity_24h
+ quantity = RelativeHumidity
+ info = 'The average relative humidity over the last 24 hours.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = RelativeHumidity
+ storage_nr = 2
+ }
+ }
+ field {
+ name = maximum_relative_humidity_1h
+ quantity = RelativeHumidity
+ info = 'The maximum relative humidity over the last hour.'
+ match {
+ measurement_type = Maximum
+ vif_range = RelativeHumidity
+ }
+ }
+ field {
+ name = maximum_relative_humidity_24h
+ quantity = RelativeHumidity
+ info = 'The maximum relative humidity over the last 24 hours.'
+ match {
+ measurement_type = Maximum
+ vif_range = RelativeHumidity
+ storage_nr = 1
+ }
+ }
+ field {
+ name = minimum_relative_humidity_1h
+ quantity = RelativeHumidity
+ info = 'The minimum relative humidity over the last hour.'
+ match {
+ measurement_type = Minimum
+ vif_range = RelativeHumidity
+ }
+ }
+ field {
+ name = minimum_relative_humidity_24h
+ quantity = RelativeHumidity
+ info = 'The minimum relative humidity over the last 24 hours.'
+ match {
+ measurement_type = Minimum
+ vif_range = RelativeHumidity
+ storage_nr = 1
+ }
+ }
+ field {
+ name = device
+ quantity = PointInTime
+ info = 'The meter date time.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = DateTime
+ }
+ }
+ }
+ tests {
+ test {
+ args = 'Rummet rfmamb 11772288 NOKEY'
+ telegram = 5744b40988227711101b7ab20800000265a00842658f088201659f08226589081265a0086265510852652b0902fb1aba0142fb1ab0018201fb1abd0122fb1aa90112fb1aba0162fb1aa60152fb1af501066d3b3bb36b2a00
+ json = '{"_":"telegram","media":"room sensor","driver":"rfmamb","name":"Rummet","id":"11772288","status":"MODULE_REMOVED","current_temperature_c":22.08,"average_temperature_1h_c":21.91,"average_temperature_24h_c":22.07,"maximum_temperature_1h_c":22.08,"minimum_temperature_1h_c":21.85,"maximum_temperature_24h_c":23.47,"minimum_temperature_24h_c":21.29,"current_relative_humidity_rh":44.2,"average_relative_humidity_1h_rh":43.2,"average_relative_humidity_24h_rh":44.5,"minimum_relative_humidity_1h_rh":42.5,"maximum_relative_humidity_1h_rh":44.2,"maximum_relative_humidity_24h_rh":50.1,"minimum_relative_humidity_24h_rh":42.2,"device_datetime":"2019-10-11 19:59","timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'Rummet;11772288;22.08;44.2;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'Pokojak rfmamb 23699558 6C649F296476D737CACB75A2D639CE14'
+ telegram = 5e44b40958956923101b7a7f085005816afb5f6f40ec742610b93c109973edd8c098f505d5dfcfd53dfd72708178e4b81436cb753c6dff5094c48c26607c66419628d424ce41f38f9bd927757d82fa43396aa59a77c694acadab776460d472
+ json = '{"_":"telegram","media":"room sensor","driver":"rfmamb","name":"Pokojak","id":"23699558","status":"MODULE_REMOVED","current_temperature_c":20.91,"average_temperature_1h_c":20.95,"average_temperature_24h_c":19.5,"maximum_temperature_1h_c":20.98,"minimum_temperature_1h_c":20.91,"maximum_temperature_24h_c":19.88,"minimum_temperature_24h_c":19.07,"current_relative_humidity_rh":35.8,"average_relative_humidity_1h_rh":36.1,"average_relative_humidity_24h_rh":36.9,"minimum_relative_humidity_1h_rh":35.8,"maximum_relative_humidity_1h_rh":36.5,"maximum_relative_humidity_24h_rh":37,"minimum_relative_humidity_24h_rh":36.7,"device_datetime":"2026-02-09 08:59","timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'Pokojak;23699558;20.91;35.8;1111-11-11 11:11.11'
+ }
+ }
+}
diff --git a/components/wmbus_common/drivers/src/rfmtx1.xmq b/components/wmbus_common/drivers/src/rfmtx1.xmq
new file mode 100644
index 00000000..162e4c88
--- /dev/null
+++ b/components/wmbus_common/drivers/src/rfmtx1.xmq
@@ -0,0 +1,255 @@
+// Copyright (C) 2020-2026 Fredrik Öhrström (gpl-3.0-or-later)
+driver {
+ name = rfmtx1
+ meter_type = WaterMeter
+ default_fields = name,id,total_m3,meter_datetime,timestamp
+ detect {
+ mvt = BMT,05,07
+ }
+ fields {
+ // RFM-TX1 (tpl-cfg 1006) uses proprietary payload layout. The ixml rule below
+ // reconstructs pseudo DV entries so the rest of the driver can decode like normal fields.
+ field {
+ name = payload_decoder
+ quantity = Text
+ attributes = HIDE,REQUIRED
+ match_entire_frame = true
+ ixml = "decode = skip11, key_byte, skip3, skip2, data_raw_bytes, skip7, sec_bytes, min_bytes, hour_bytes, day_bytes, mon_bytes, year_bytes, rest.
+ -hex = ['A'-'F';'0'-'9'].
+ -byte = hex, hex.
+ -skip11 = byte, byte, byte, byte, byte, byte, byte, byte, byte, byte, byte.
+ -skip3 = byte, byte, byte.
+ -skip2 = byte, byte.
+ -skip7 = byte, byte, byte, byte, byte, byte, byte.
+ -rest = byte*.
+ key_byte = byte, @DV_key_raw.
+ data_raw_bytes = byte, byte, byte, byte, @DV_data_raw.
+ sec_bytes = byte, @DV_sec_raw.
+ min_bytes = byte, @DV_min_raw.
+ hour_bytes = byte, @DV_hour_raw.
+ day_bytes = byte, @DV_day_raw.
+ mon_bytes = byte, @DV_mon_raw.
+ year_bytes = byte, @DV_year_raw.
+ DV_key_raw>dvk = +'01FF10'.
+ DV_data_raw>dvk = +'04FF11'.
+ DV_sec_raw>dvk = +'01FF13'.
+ DV_min_raw>dvk = +'01FF14'.
+ DV_hour_raw>dvk = +'01FF15'.
+ DV_day_raw>dvk = +'01FF16'.
+ DV_mon_raw>dvk = +'01FF17'.
+ DV_year_raw>dvk = +'01FF18'."
+ }
+ field {
+ name = key_raw
+ quantity = Dimensionless
+ attributes = HIDE
+ vif_scaling = None
+ dif_signedness = Unsigned
+ match {
+ difvifkey = 01FF10
+ }
+ }
+ field {
+ name = data_raw
+ quantity = Dimensionless
+ attributes = HIDE
+ vif_scaling = None
+ dif_signedness = Unsigned
+ match {
+ difvifkey = 04FF11
+ }
+ }
+ field {
+ name = sec_raw
+ quantity = Dimensionless
+ attributes = HIDE
+ vif_scaling = None
+ dif_signedness = Unsigned
+ match {
+ difvifkey = 01FF13
+ }
+ }
+ field {
+ name = min_raw
+ quantity = Dimensionless
+ attributes = HIDE
+ vif_scaling = None
+ dif_signedness = Unsigned
+ match {
+ difvifkey = 01FF14
+ }
+ }
+ field {
+ name = hour_raw
+ quantity = Dimensionless
+ attributes = HIDE
+ vif_scaling = None
+ dif_signedness = Unsigned
+ match {
+ difvifkey = 01FF15
+ }
+ }
+ field {
+ name = day_raw
+ quantity = Dimensionless
+ attributes = HIDE
+ vif_scaling = None
+ dif_signedness = Unsigned
+ match {
+ difvifkey = 01FF16
+ }
+ }
+ field {
+ name = mon_raw
+ quantity = Dimensionless
+ attributes = HIDE
+ vif_scaling = None
+ dif_signedness = Unsigned
+ match {
+ difvifkey = 01FF17
+ }
+ }
+ field {
+ name = year_raw
+ quantity = Dimensionless
+ attributes = HIDE
+ vif_scaling = None
+ dif_signedness = Unsigned
+ match {
+ difvifkey = 01FF18
+ }
+ }
+ field {
+ name = row
+ quantity = Dimensionless
+ attributes = HIDE
+ // Row selector used by all t0..t3 lookup formulas.
+ // key_raw is the telegram key byte; low nibble gives a row in [0..15].
+ calculate = 'key_raw_counter & 15counter'
+ }
+ // t0..t3 are row-indexed lookup values. Each formula returns exactly one constant
+ // for the selected row (all other terms evaluate to 0).
+ // Keep each ((row==N)*value) term wrapped in parentheses because the formula parser
+ // does not apply standard operator precedence between + and *.
+ field {
+ name = t0
+ quantity = Dimensionless
+ attributes = HIDE
+ // LUT value for d0, where encrypted byte0 is (data_raw_counter & 255counter)
+ // and d0 is computed below as byte0 ^ key_raw_counter ^ t0_counter.
+ calculate = '((row_counter == 0counter)*122counter) + ((row_counter == 1counter)*112counter) + ((row_counter == 2counter)*185counter) + ((row_counter == 3counter)*121counter) + ((row_counter == 4counter)*7counter) + ((row_counter == 5counter)*50counter) + ((row_counter == 6counter)*156counter) + ((row_counter == 7counter)*180counter) + ((row_counter == 8counter)*10counter) + ((row_counter == 9counter)*240counter) + ((row_counter == 10counter)*22counter) + ((row_counter == 11counter)*12counter) + ((row_counter == 12counter)*74counter) + ((row_counter == 13counter)*119counter) + ((row_counter == 14counter)*189counter) + ((row_counter == 15counter)*57counter)'
+ }
+ field {
+ name = t1
+ quantity = Dimensionless
+ attributes = HIDE
+ // LUT value for d1, with byte1 = ((data_raw_counter >> 8counter) & 255counter)
+ // and d1 computed below as byte1 ^ key_raw_counter ^ t1_counter.
+ calculate = '((row_counter == 0counter)*16counter) + ((row_counter == 1counter)*19counter) + ((row_counter == 2counter)*11counter) + ((row_counter == 3counter)*7counter) + ((row_counter == 4counter)*154counter) + ((row_counter == 5counter)*161counter) + ((row_counter == 6counter)*126counter) + ((row_counter == 7counter)*196counter) + ((row_counter == 8counter)*206counter) + ((row_counter == 9counter)*5counter) + ((row_counter == 10counter)*152counter) + ((row_counter == 11counter)*201counter) + ((row_counter == 12counter)*72counter) + ((row_counter == 13counter)*46counter) + ((row_counter == 14counter)*120counter) + ((row_counter == 15counter)*111counter)'
+ }
+ field {
+ name = t2
+ quantity = Dimensionless
+ attributes = HIDE
+ // LUT value for d2, with byte2 = ((data_raw_counter >> 16counter) & 255counter)
+ // and d2 computed below as byte2 ^ key_raw_counter ^ t2_counter.
+ calculate = '((row_counter == 0counter)*26counter) + ((row_counter == 1counter)*34counter) + ((row_counter == 2counter)*142counter) + ((row_counter == 3counter)*74counter) + ((row_counter == 4counter)*203counter) + ((row_counter == 5counter)*57counter) + ((row_counter == 6counter)*96counter) + ((row_counter == 7counter)*128counter) + ((row_counter == 8counter)*25counter) + ((row_counter == 9counter)*165counter) + ((row_counter == 10counter)*17counter) + ((row_counter == 11counter)*125counter) + ((row_counter == 12counter)*228counter) + ((row_counter == 13counter)*138counter) + ((row_counter == 14counter)*87counter) + ((row_counter == 15counter)*40counter)'
+ }
+ field {
+ name = t3
+ quantity = Dimensionless
+ attributes = HIDE
+ // LUT value for d3, with byte3 = ((data_raw_counter >> 24counter) & 255counter)
+ // and d3 computed below as byte3 ^ key_raw_counter ^ t3_counter.
+ calculate = '((row_counter == 0counter)*10counter) + ((row_counter == 1counter)*19counter) + ((row_counter == 2counter)*153counter) + ((row_counter == 3counter)*22counter) + ((row_counter == 4counter)*105counter) + ((row_counter == 5counter)*14counter) + ((row_counter == 6counter)*153counter) + ((row_counter == 7counter)*163counter) + ((row_counter == 8counter)*3counter) + ((row_counter == 9counter)*134counter) + ((row_counter == 10counter)*94counter) + ((row_counter == 11counter)*162counter) + ((row_counter == 12counter)*31counter) + ((row_counter == 13counter)*232counter) + ((row_counter == 14counter)*140counter) + ((row_counter == 15counter)*5counter)'
+ }
+ field {
+ name = d0
+ quantity = Dimensionless
+ attributes = HIDE
+ calculate = '(data_raw_counter & 255counter) ^ key_raw_counter ^ t0_counter'
+ }
+ field {
+ name = d1
+ quantity = Dimensionless
+ attributes = HIDE
+ calculate = '((data_raw_counter >> 8counter) & 255counter) ^ key_raw_counter ^ t1_counter'
+ }
+ field {
+ name = d2
+ quantity = Dimensionless
+ attributes = HIDE
+ calculate = '((data_raw_counter >> 16counter) & 255counter) ^ key_raw_counter ^ t2_counter'
+ }
+ field {
+ name = d3
+ quantity = Dimensionless
+ attributes = HIDE
+ calculate = '((data_raw_counter >> 24counter) & 255counter) ^ key_raw_counter ^ t3_counter'
+ }
+ field {
+ name = sec
+ quantity = Dimensionless
+ attributes = HIDE
+ calculate = '((sec_raw_counter >> 4counter) * 10counter) + (sec_raw_counter % 16counter)'
+ }
+ field {
+ name = min
+ quantity = Dimensionless
+ attributes = HIDE
+ calculate = '((min_raw_counter >> 4counter) * 10counter) + (min_raw_counter % 16counter)'
+ }
+ field {
+ name = hour
+ quantity = Dimensionless
+ attributes = HIDE
+ calculate = '((hour_raw_counter >> 4counter) * 10counter) + (hour_raw_counter % 16counter)'
+ }
+ field {
+ name = day
+ quantity = Dimensionless
+ attributes = HIDE
+ calculate = '((day_raw_counter >> 4counter) * 10counter) + (day_raw_counter % 16counter)'
+ }
+ field {
+ name = mon
+ quantity = Dimensionless
+ attributes = HIDE
+ calculate = '((mon_raw_counter >> 4counter) * 10counter) + (mon_raw_counter % 16counter)'
+ }
+ field {
+ name = year
+ quantity = Dimensionless
+ attributes = HIDE
+ calculate = '((year_raw_counter >> 4counter) * 10counter) + (year_raw_counter % 16counter)'
+ }
+ field {
+ name = total
+ quantity = Volume
+ info = 'The total water consumption recorded by this meter.'
+ // d0..d3 are decrypted bytes; each byte stores two BCD digits. Rebuild the
+ // 8-digit reading and scale from liters to m3.
+ calculate = '((((d0_counter >> 4counter) * 10counter) + (d0_counter % 16counter)) + (100counter * (((d1_counter >> 4counter) * 10counter) + (d1_counter % 16counter))) + (10000counter * (((d2_counter >> 4counter) * 10counter) + (d2_counter % 16counter))) + (1000000counter * (((d3_counter >> 4counter) * 10counter) + (d3_counter % 16counter)))) / 1000counter * 1m3'
+ }
+ field {
+ name = meter
+ quantity = PointInTime
+ display_unit = datetime
+ info = 'Date time when meter sent this telegram.'
+ // Meter datetime bytes are plain BCD (yy MM dd HH mm ss) in telegram payload.
+ // Build local meter time from component fields, matching behavior confirmed in issue #101.
+ calculate = "'2000-01-01 00:00:00' +
+ (((year_counter * 12counter) + mon_counter - 1counter) * 1month) +
+ ((day_counter - 1counter) * 24h) +
+ ((((hour_counter * 3600counter) + (min_counter * 60counter) + sec_counter)) * 1s)"
+ }
+ }
+ tests {
+ test {
+ args = 'Wasser rfmtx1 74737271 NOKEY'
+ telegram = 4644B4097172737405077AA5000610_1115F78184AB0F1D1E200000005904103103208047004A4800E73C00193E00453F003E4000E64000E74100F442000144001545005B460000
+ json = '{"_":"telegram","media":"water","driver":"rfmtx1","name":"Wasser","id":"74737271","total_m3":188.56,"meter_datetime":"2020-03-31 10:04","timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'Wasser;74737271;188.56;2020-03-31 10:04;1111-11-11 11:11.11'
+ }
+ }
+}
diff --git a/components/wmbus_common/drivers/src/sanxing.xmq b/components/wmbus_common/drivers/src/sanxing.xmq
new file mode 100644
index 00000000..a3b3e485
--- /dev/null
+++ b/components/wmbus_common/drivers/src/sanxing.xmq
@@ -0,0 +1,205 @@
+// Copyright (C) 2019-2025 Fredrik Öhrström (gpl-3.0-or-later)
+// Copyright (C) 2026 Brychu (gpl-3.0-or-later)
+driver {
+ name = sanxing
+ meter_type = ElectricityMeter
+ default_fields = name,id,total_energy_consumption_kwh,current_power_consumption_kw,total_energy_production_kwh,current_power_production_kw,voltage_at_phase_1_v,voltage_at_phase_2_v,voltage_at_phase_3_v,total_energy_consumption_tariff_1_kwh,total_energy_consumption_tariff_2_kwh,total_energy_consumption_tariff_3_kwh,total_energy_production_tariff_1_kwh,total_energy_production_tariff_2_kwh,total_energy_production_tariff_3_kwh,timestamp
+ manufacturer = Sanxing
+ aliases = s34u28
+ // Sanxing S34U28 (Tauron/Poland deployment) is byte-for-byte identical to the
+ // amiplus/Apator electricity meter family, except it sends two non-2F2F bytes in
+ // place of the OMS decrypt-verification bytes: see transform_payload below and
+ // Telegram::potentiallyDecrypt in wmbus.cc.
+ transform_payload = buggy_sanxing_609B
+ detect {
+ mvt = KPL,01,02
+ }
+ library {
+ use = fabrication_no
+ }
+ fields {
+ field {
+ name = total_energy_consumption
+ quantity = Energy
+ info = 'The total energy consumption recorded by this meter.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyEnergyVIF
+ }
+ }
+ field {
+ name = current_power_consumption
+ quantity = Power
+ info = 'Current power consumption.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ measurement_type = Instantaneous
+ vif_range = PowerW
+ }
+ }
+ field {
+ name = total_energy_production
+ quantity = Energy
+ info = 'The total energy production recorded by this meter.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ difvifkey = 0E833C
+ }
+ }
+ field {
+ name = current_power_production
+ quantity = Power
+ info = 'Current power production.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ difvifkey = 0BAB3C
+ }
+ }
+ field {
+ name = voltage_at_phase_1
+ quantity = Voltage
+ info = 'Voltage at phase L1.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ measurement_type = Instantaneous
+ vif_range = Voltage
+ add_combinable = AtPhase1
+ }
+ }
+ field {
+ name = voltage_at_phase_2
+ quantity = Voltage
+ info = 'Voltage at phase L2.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ measurement_type = Instantaneous
+ vif_range = Voltage
+ add_combinable = AtPhase2
+ }
+ }
+ field {
+ name = voltage_at_phase_3
+ quantity = Voltage
+ info = 'Voltage at phase L3.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ measurement_type = Instantaneous
+ vif_range = Voltage
+ add_combinable = AtPhase3
+ }
+ }
+ field {
+ name = device
+ quantity = PointInTime
+ info = 'Device date time.'
+ display_unit = datetime
+ match {
+ measurement_type = Instantaneous
+ vif_range = DateTime
+ }
+ }
+ field {
+ name = total_energy_consumption_tariff_1
+ quantity = Energy
+ info = 'The total energy consumption recorded by this meter on tariff 1.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyEnergyVIF
+ tariff_nr = 1
+ }
+ }
+ field {
+ name = total_energy_consumption_tariff_2
+ quantity = Energy
+ info = 'The total energy consumption recorded by this meter on tariff 2.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyEnergyVIF
+ tariff_nr = 2
+ }
+ }
+ field {
+ name = total_energy_consumption_tariff_3
+ quantity = Energy
+ info = 'The total energy consumption recorded by this meter on tariff 3.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyEnergyVIF
+ tariff_nr = 3
+ }
+ }
+ field {
+ name = total_energy_production_tariff_1
+ quantity = Energy
+ info = 'The total energy production recorded by this meter on tariff 1.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ difvifkey = 8E10833C
+ }
+ }
+ field {
+ name = total_energy_production_tariff_2
+ quantity = Energy
+ info = 'The total energy production recorded by this meter on tariff 2.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ difvifkey = 8E20833C
+ }
+ }
+ field {
+ name = total_energy_production_tariff_3
+ quantity = Energy
+ info = 'The total energy production recorded by this meter on tariff 3.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ difvifkey = 8E30833C
+ }
+ }
+ field {
+ name = max_power_consumption
+ quantity = Power
+ info = 'The maximum demand indicator (maximum 15-min average power consumption recorded this month).'
+ match {
+ measurement_type = Maximum
+ vif_range = AnyPowerVIF
+ }
+ }
+ field {
+ name = absolute_max_power_consumption
+ quantity = Power
+ info = 'The maximum demand indicator for all times?'
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyPowerVIF
+ add_combinable = UpperLimit
+ add_combinable = AccumulationOfAbsoluteValue
+ }
+ }
+ }
+ tests {
+ test {
+ args = 'MyElectricity7 sanxing 11223344 38852fe01b9427ffcdf549b50190721a'
+ comment = 'Sanxing S34U28 (mfct=KPL): this device puts two non-2F2F bytes where OMS expects the decrypt-check marker; verified instead via the trailing 2F2F padding, see Telegram::potentiallyDecrypt in wmbus.cc. Synthetic telegram: real DV records re-encrypted under a fabricated key with the id changed, see https://github.com/wmbusmeters/wmbusmeters/issues/2042 for the original captures.'
+ telegram = 5E440CAE4433221101027A01005005C86FBE60C581F909A1C76C4ADB96896111C7629254158644F874083D105C737F6F56812CE5E4F9B60CDFBE01CDB2A76E38FA17D0EF61F683FE189A57BF35D23C7B8631F22FB5DD64E41ACAC3DF2476A0
+ json = '{"_":"telegram","media":"electricity","driver":"sanxing","name":"MyElectricity7","id":"11223344","fabrication_no": "11223344","current_power_consumption_kw":0.259,"current_power_production_kw":0,"device_datetime":"2026-07-28 13:02","max_power_consumption_kw":3.492,"absolute_max_power_consumption_kw":99,"total_energy_consumption_kwh":2469.595,"total_energy_production_kwh":0,"voltage_at_phase_1_v":230,"voltage_at_phase_2_v":230,"voltage_at_phase_3_v":230,"timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'MyElectricity7;11223344;2469.595;0.259;0;0;230;230;230;null;null;null;null;null;null;1111-11-11 11:11.11'
+ }
+ }
+}
diff --git a/components/wmbus_common/drivers/src/sensostar.xmq b/components/wmbus_common/drivers/src/sensostar.xmq
index 65f5ad16..3aea5aa5 100644
--- a/components/wmbus_common/drivers/src/sensostar.xmq
+++ b/components/wmbus_common/drivers/src/sensostar.xmq
@@ -1,5 +1,5 @@
/* Copyright (C) 2020-2022 Patrick Schwarz (gpl-3.0-or-later)
- Copyright (C) 2022-2024 Fredrik Öhrström (gpl-3.0-or-later) */
+ Copyright (C) 2022-2026 Fredrik Öhrström (gpl-3.0-or-later) */
driver {
name = sensostar
meter_type = HeatMeter
@@ -8,7 +8,11 @@ driver {
mvt = EFE,00,04
}
library {
- use = meter_datetime,fabrication_no,model_version,on_time_h,parameter_set
+ use = fabrication_no
+ use = meter_datetime
+ use = model_version
+ use = on_time_h
+ use = parameter_set
}
fields {
field {
@@ -76,6 +80,17 @@ driver {
vif_range = AnyEnergyVIF
}
}
+ field {
+ name = total
+ quantity = Energy
+ info = 'The total heat energy consumption recorded by this meter using mfct specific gcal field.'
+ force_scale = 0.001
+ override_vif_unit = gcal
+ match {
+ // This is a mfct
+ difvifkey = 04FB0D
+ }
+ }
field {
name = 'total_tariff{tariff_counter}'
quantity = Energy
@@ -245,21 +260,28 @@ driver {
test {
args = 'Heat sensostar 20480057 NOKEY'
telegram = 68B3B36808007257004820c51400046c100000047839803801040600000000041300000000042B00000000142B00000000043B00000000143B00000000025B1400025f15000261daff02235c00046d2c2ddc24440600000000441300000000426c000001fd171003fd0c05000084200600000000c420060000000084300600000000c430060000000084401300000000c44013000000008480401300000000c48040130000000084c0401300000000c4c0401300000000a216
- json = '{"_":"telegram","difference_c": -0.38,"fabrication_no": "01388039","flow_water_m3h": 0,"flow_water_max_m3h": 0,"forward_c": 20,"id": "20480057","media": "heat","meter": "sensostar","meter_datetime": "2022-04-28 13:44","model_version": "000005","name": "Heat","on_time_h": 2208,"power_kw": 0,"power_max_kw": 0,"return_c": 21,"status": "ERROR_FLOW_MEASUREMENT_SYSTEM_ERROR TEMPORARY_ERROR","target_date": "1999-11-30","target_kwh": 0,"target_subunit1_m3": 0,"target_subunit2_m3": 0,"target_subunit3_m3": 0,"target_tariff2_kwh": 0,"target_tariff3_kwh": 0,"target_water_m3": 0,"timestamp": "1111-11-11T11:11:11Z","total_kwh": 0,"total_subunit1_m3": 0,"total_subunit2_m3": 0,"total_subunit3_m3": 0,"total_tariff2_kwh": 0,"total_tariff3_kwh": 0,"total_water_m3": 0}'
+ json = '{"_":"telegram","difference_c": -0.38,"fabrication_no": "01388039","flow_water_m3h": 0,"flow_water_max_m3h": 0,"forward_c": 20,"id": "20480057","media": "heat","driver": "sensostar","meter_datetime": "2022-04-28 13:44","model_version": "000005","name": "Heat","on_time_h": 2208,"power_kw": 0,"power_max_kw": 0,"return_c": 21,"status": "ERROR_FLOW_MEASUREMENT_SYSTEM_ERROR TEMPORARY_ERROR","target_date": "1999-11-30","target_kwh": 0,"target_subunit1_m3": 0,"target_subunit2_m3": 0,"target_subunit3_m3": 0,"target_tariff2_kwh": 0,"target_tariff3_kwh": 0,"target_water_m3": 0,"timestamp": "1111-11-11T11:11:11Z","total_kwh": 0,"total_subunit1_m3": 0,"total_subunit2_m3": 0,"total_subunit3_m3": 0,"total_tariff2_kwh": 0,"total_tariff3_kwh": 0,"total_water_m3": 0}'
fields = 'Heat;20480057;ERROR_FLOW_MEASUREMENT_SYSTEM_ERROR TEMPORARY_ERROR;0;0;1999-11-30;0;1111-11-11 11:11.11'
}
test {
args = 'WMZ sensostar 02752560 NOKEY'
comment = 'from "Sensostar U"'
telegram = a444c5146025750200047ac20000202f2f046d2e26c62a040643160000041310f0050001fd1700426cbf2c4406570e00008401061f160000840206f6150000840306f5150000840406f3150000840506ea150000840606bf1500008407065214000084080692120000840906c5100000840a06570e0000840b06ca0b0000840c06da090000840d06ca080000840e06c8080000840f06c608000003fd0c05010002fd0b2111
- json = '{"_":"telegram","id": "02752560","media": "heat","meter": "sensostar","meter_datetime": "2022-10-06 06:46","model_version": "000105","name": "WMZ","parameter_set": "1121","status": "OK","target_10_kwh": 5610,"target_12_kwh": 5567,"target_14_kwh": 5202,"target_16_kwh": 4754,"target_18_kwh": 4293,"target_20_kwh": 3671,"target_22_kwh": 3018,"target_24_kwh": 2522,"target_26_kwh": 2250,"target_28_kwh": 2248,"target_2_kwh": 5663,"target_30_kwh": 2246,"target_4_kwh": 5622,"target_6_kwh": 5621,"target_8_kwh": 5619,"target_date": "2021-12-31","target_kwh": 3671,"timestamp": "1111-11-11T11:11:11Z","total_kwh": 5699,"total_water_m3": 389.136}'
+ json = '{"_":"telegram","id": "02752560","media": "heat","driver": "sensostar","meter_datetime": "2022-10-06 06:46","model_version": "000105","name": "WMZ","parameter_set": "1121","status": "OK","target_10_kwh": 5610,"target_12_kwh": 5567,"target_14_kwh": 5202,"target_16_kwh": 4754,"target_18_kwh": 4293,"target_20_kwh": 3671,"target_22_kwh": 3018,"target_24_kwh": 2522,"target_26_kwh": 2250,"target_28_kwh": 2248,"target_2_kwh": 5663,"target_30_kwh": 2246,"target_4_kwh": 5622,"target_6_kwh": 5621,"target_8_kwh": 5619,"target_date": "2021-12-31","target_kwh": 3671,"timestamp": "1111-11-11T11:11:11Z","total_kwh": 5699,"total_water_m3": 389.136}'
fields = 'WMZ;02752560;OK;5699;389.136;2021-12-31;3671;1111-11-11 11:11.11'
}
test {
args = 'XYZ sensostar 21750444 NOKEY'
telegram = 4944C5144404752100047AC10000202F2F046D142D073404068847000001FD170004138E8A4000043BF7FFFFFF042B00000000025B3E00025F360002612F0303FD0C05010002FD0B2011
- json = '{"_":"telegram","difference_c": 8.15,"flow_water_m3h": -0.009,"forward_c": 62,"id": "21750444","media": "heat","meter": "sensostar","meter_datetime": "2024-04-07 13:20","model_version": "000105","name": "XYZ","parameter_set": "1120","power_kw": 0,"return_c": 54,"status": "OK","timestamp": "1111-11-11T11:11:11Z","total_kwh": 18312,"total_water_m3": 4229.774}'
+ json = '{"_":"telegram","difference_c": 8.15,"flow_water_m3h": -0.009,"forward_c": 62,"id": "21750444","media": "heat","driver": "sensostar","meter_datetime": "2024-04-07 13:20","model_version": "000105","name": "XYZ","parameter_set": "1120","power_kw": 0,"return_c": 54,"status": "OK","timestamp": "1111-11-11T11:11:11Z","total_kwh": 18312,"total_water_m3": 4229.774}'
fields = 'XYZ;21750444;OK;18312;4229.774;null;null;1111-11-11 11:11.11'
}
+ test {
+ args = 'GCAL sensostar 45457891 NOKEY'
+ comment = 'This meter has been configured with a manufacturer specific giga calorie value field.'
+ telegram = 5944C5149178454500047A100040252F2F_046D11334F3904FB0DE70C000001FD170004130DAB0600043B00000000042B00000000025B1B00025F1B000261F3FF2F2F2F2F2F2F2F2F2F2F2F2F2F2F2F03FD0C05000002FD0B1011
+ json = '{"_": "telegram","difference_c": -0.13,"driver": "sensostar","flow_water_m3h": 0,"forward_c": 27,"id": "45457891","media": "heat","meter_datetime": "2026-09-15 19:17","model_version": "000005","name": "GCAL","parameter_set": "1110","power_kw": 0,"return_c": 27,"status": "OK","timestamp": "1111-11-11T11:11:11Z","total_kwh": 3841.389,"total_water_m3": 437.005}'
+ fields = 'GCAL;45457891;OK;3841.389;437.005;null;null;1111-11-11 11:11.11'
+ }
}
}
diff --git a/components/wmbus_common/drivers/src/sharky.xmq b/components/wmbus_common/drivers/src/sharky.xmq
new file mode 100644
index 00000000..fdb28676
--- /dev/null
+++ b/components/wmbus_common/drivers/src/sharky.xmq
@@ -0,0 +1,191 @@
+// Copyright (C) 2021 Vincent Privat (gpl-3.0-or-later)
+// Copyright (C) 2022 Fredrik Öhrström (gpl-3.0-or-later)
+// Diehl Sharky 775 heat meter.
+driver {
+ name = sharky
+ meter_type = HeatMeter
+ default_fields = name,id,total_energy_consumption_kwh,total_energy_consumption_tariff1_kwh,total_volume_m3,total_volume_tariff2_m3,volume_flow_m3h,power_kw,flow_temperature_c,return_temperature_c,temperature_difference_c,timestamp
+ detect {
+ mvt = HYD,20,04
+ mvt = DME,40,04
+ }
+ library {
+ use = operating_time_h
+ }
+ fields {
+ field {
+ name = status
+ quantity = Text
+ info = 'Status of meter.'
+ attributes = STATUS
+ match {
+ measurement_type = Instantaneous
+ vif_range = ErrorFlags
+ }
+ lookup {
+ name = ERROR_FLAGS
+ map_type = BitToString
+ mask_bits = 0x0000
+ default_message = OK
+ }
+ }
+ field {
+ name = total_energy_consumption
+ quantity = Energy
+ info = 'The total heat energy consumption recorded by this meter.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyEnergyVIF
+ }
+ }
+ field {
+ name = total_energy_consumption_tariff1
+ quantity = Energy
+ info = 'The total heat energy consumption recorded by this meter on tariff 1.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyEnergyVIF
+ tariff_nr = 1
+ }
+ }
+ field {
+ name = total_volume
+ quantity = Volume
+ info = 'The total heating media volume recorded by this meter.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ measurement_type = Instantaneous
+ vif_range = Volume
+ }
+ }
+ field {
+ name = total_volume_tariff2
+ quantity = Volume
+ info = 'The total heating media volume recorded by this meter on tariff 2.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ measurement_type = Instantaneous
+ vif_range = Volume
+ tariff_nr = 2
+ }
+ }
+ field {
+ name = volume_flow
+ quantity = Flow
+ info = 'The current heat media volume flow.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ measurement_type = Instantaneous
+ vif_range = VolumeFlow
+ }
+ }
+ field {
+ name = power
+ quantity = Power
+ info = 'The current power consumption.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ measurement_type = Instantaneous
+ vif_range = PowerW
+ }
+ }
+ field {
+ name = flow_temperature
+ quantity = Temperature
+ info = 'The current forward heat media temperature.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ measurement_type = Instantaneous
+ vif_range = FlowTemperature
+ }
+ }
+ field {
+ name = return_temperature
+ quantity = Temperature
+ info = 'The current return heat media temperature.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ measurement_type = Instantaneous
+ vif_range = ReturnTemperature
+ }
+ }
+ field {
+ name = temperature_difference
+ quantity = Temperature
+ info = 'The current temperature difference between flow and return.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ measurement_type = Instantaneous
+ vif_range = TemperatureDifference
+ }
+ }
+ field {
+ name = target_energy_consumption
+ quantity = Energy
+ info = 'The total heat energy consumption at the end of the previous billing period.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyEnergyVIF
+ storage_nr = 5
+ }
+ }
+ field {
+ name = target_volume
+ quantity = Volume
+ info = 'The total heating media volume at the end of the previous billing period.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ measurement_type = Instantaneous
+ vif_range = Volume
+ storage_nr = 5
+ }
+ }
+ field {
+ name = target
+ quantity = PointInTime
+ info = 'The last billing period end date.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ display_unit = DateLT
+ match {
+ measurement_type = Instantaneous
+ vif_range = Date
+ storage_nr = 5
+ }
+ }
+ }
+ tests {
+ test {
+ args = 'Heat sharky 68926025 NOKEY'
+ telegram = 534424232004256092687A370045752235854DEEEA5939FAD81C25FEEF5A23C38FB9168493C563F08DB10BAF87F660FBA91296BA2397E8F4220B86D3A192FB51E0BFCF24DCE72118E0C75A9E89F43BDFE370824B
+ json = '{"_":"telegram","media":"heat","driver":"sharky","name":"Heat","id":"68926025","total_energy_consumption_kwh":2651,"total_energy_consumption_tariff1_kwh":0,"total_volume_m3":150.347,"total_volume_tariff2_m3":0.018,"volume_flow_m3h":0,"power_kw":0,"flow_temperature_c":42.3,"return_temperature_c":28.1,"status":"OK","temperature_difference_c":14.1,"timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'Heat;68926025;2651;0;150.347;0.018;0;0;42.3;28.1;14.1;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'Heat sharky 61917653 NOKEY'
+ telegram = 5e44a5115376916140047a0B0050052f2f0c0e829311008c100e000000000c14014938000c2B751400000B3B2902000a5a52070a5e95060a6256000a279015cc020e92831100cc021478113800c2026cdf2c2f2f2f2f2f2f2f2f2f2f2f2f2f
+ json = '{"_":"telegram","media":"heat","driver":"sharky","name":"Heat","id":"61917653","flow_temperature_c":75.2,"operating_time_h":38160,"power_kw":1.475,"return_temperature_c":69.5,"target_date":"2022-12-31","target_energy_consumption_kwh":32886.666667,"target_volume_m3":3811.78,"temperature_difference_c":5.6,"total_energy_consumption_kwh":33161.666667,"total_energy_consumption_tariff1_kwh":0,"total_volume_m3":3849.01,"volume_flow_m3h":0.229,"timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'Heat;61917653;33161.666667;0;3849.01;null;0.229;1.475;75.2;69.5;5.6;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'Heato sharky 69696969 NOKEY'
+ telegram = 5e44a5116969696940047aBe0050052f2f0c06975100008c1006000000000c13849345000c2B000000000B3B0000000a5a06020a5e08020a6202f00B26110201cc020623500000cc021329554400c2026cdf2c2f2f2f2f2f2f2f2f2f2f2f2f
+ json = '{"_":"telegram","flow_temperature_c": 20.6,"id": "69696969","media": "heat","driver": "sharky","name": "Heato","operating_time_h": 10211,"power_kw": 0,"return_temperature_c": 20.8,"target_date": "2022-12-31","target_energy_consumption_kwh": 5023,"target_volume_m3": 445.529,"temperature_difference_c": -0.2,"timestamp": "1111-11-11T11:11:11Z","total_energy_consumption_kwh": 5197,"total_energy_consumption_tariff1_kwh": 0,"total_volume_m3": 459.384, "volume_flow_m3h": 0}'
+ fields = 'Heato;69696969;5197;0;459.384;null;0;0;20.6;20.8;-0.2;1111-11-11 11:11.11'
+ }
+ }
+}
diff --git a/components/wmbus_common/drivers/src/sharky774.xmq b/components/wmbus_common/drivers/src/sharky774.xmq
new file mode 100644
index 00000000..e1ad86ab
--- /dev/null
+++ b/components/wmbus_common/drivers/src/sharky774.xmq
@@ -0,0 +1,201 @@
+// Copyright (C) 2021 Vincent Privat (gpl-3.0-or-later)
+// Copyright (C) 2022 Fredrik Öhrström (gpl-3.0-or-later)
+// Diehl Sharky 774 heat meter.
+driver {
+ name = sharky774
+ meter_type = HeatMeter
+ default_fields = name,id,total_energy_consumption_kwh,energy_at_set_date_kwh,set_date,timestamp
+ detect {
+ mvt = DME,41,04
+ mvt = DME,41,0d
+ mvt = DME,41,0c
+ }
+ fields {
+ field {
+ name = total_energy_consumption
+ quantity = Energy
+ info = 'The total energy consumption recorded by this meter.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyEnergyVIF
+ }
+ }
+ field {
+ name = total_cooling_consumption
+ quantity = Energy
+ info = 'The total cooling energy consumption recorded by this meter.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyEnergyVIF
+ tariff_nr = 1
+ }
+ }
+ field {
+ name = total_volume
+ quantity = Volume
+ info = 'The total volume recorded by this meter.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyVolumeVIF
+ }
+ }
+ field {
+ name = total_cooling_volume
+ quantity = Volume
+ info = 'The total cooling volume recorded by this meter.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyVolumeVIF
+ tariff_nr = 2
+ }
+ }
+ field {
+ name = volume_flow
+ quantity = Flow
+ info = 'The current flow.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ measurement_type = Instantaneous
+ vif_range = VolumeFlow
+ }
+ }
+ field {
+ name = power
+ quantity = Power
+ info = 'The power.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyPowerVIF
+ }
+ }
+ field {
+ name = flow_temperature
+ quantity = Temperature
+ info = 'The flow temperature.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ measurement_type = Instantaneous
+ vif_range = FlowTemperature
+ }
+ }
+ field {
+ name = return_temperature
+ quantity = Temperature
+ info = 'The return temperature.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ measurement_type = Instantaneous
+ vif_range = ReturnTemperature
+ }
+ }
+ field {
+ name = operating_time
+ quantity = Time
+ info = 'How long the meter has been collecting data.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ measurement_type = Instantaneous
+ vif_range = OperatingTime
+ }
+ }
+ field {
+ name = operating_time_in_error
+ quantity = Time
+ info = 'How long the meter has been in an error state and not collected data.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ measurement_type = Instantaneous
+ vif_range = OperatingTime
+ add_combinable = RecordErrorCodeMeterToController
+ }
+ }
+ field {
+ name = energy_at_set_date
+ quantity = Energy
+ info = 'The total energy consumption recorded by this meter at the set date.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyEnergyVIF
+ storage_nr = 1
+ }
+ }
+ field {
+ name = cooling_at_set_date
+ quantity = Energy
+ info = 'The total cooling energy consumption recorded by this meter at the set date.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyEnergyVIF
+ storage_nr = 1
+ tariff_nr = 1
+ }
+ }
+ field {
+ name = set
+ quantity = PointInTime
+ info = 'The last billing set date.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ display_unit = DateLT
+ match {
+ measurement_type = Instantaneous
+ vif_range = Date
+ storage_nr = 1
+ }
+ }
+ }
+ tests {
+ test {
+ args = 'Heato sharky774 58496405 NOKEY'
+ telegram = 3E44A5110564495841047A700030052F2F_0C06846800000C13195364000B3B0400000C2B110100000A5A17050A5E76020AA61800004C0647630000426CBF25
+ json = '{"_":"telegram","media":"heat","driver":"sharky774","name":"Heato","id":"58496405","total_energy_consumption_kwh":6884,"total_volume_m3":645.319,"volume_flow_m3h":0.004,"power_kw":0.111,"flow_temperature_c":51.7,"return_temperature_c":27.6,"operating_time_in_error_h":0,"energy_at_set_date_kwh":6347,"set_date":"2021-05-31","timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'Heato;58496405;6884;6347;2021-05-31;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'diehl_meter sharky774 52173898 NOKEY'
+ telegram = 3E44A51198381752410C7AA80030052F2F_0C06105104000C13093835020B3B9401000C2B342600000A5A12060A5E91040AA61800004C0641460400426CFF21
+ json = '{"_":"telegram","media":"heat volume at inlet","driver":"sharky774","name":"diehl_meter","id":"52173898","total_energy_consumption_kwh":45110,"total_volume_m3":2353.809,"volume_flow_m3h":0.194,"power_kw":2.634,"flow_temperature_c":61.2,"return_temperature_c":49.1,"operating_time_in_error_h":0,"energy_at_set_date_kwh":44641,"set_date":"2023-01-31","timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'diehl_meter;52173898;45110;44641;2023-01-31;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'Heatoo sharky774 72615127 NOKEY'
+ comment = 'This test telegram has more historical data!'
+ telegram = 5E44A5112751617241047A8B0050052F2F0C0E000000000C13010000000B3B0000000C2B000000000A5A26020A5E18020B260321000AA6180000C2026CBE2BCC020E00000000CC021301000000DB023B000000DC022B000000002F2F2F2F2F
+ json = '{"_":"telegram","media":"heat","driver":"sharky774","name":"Heatoo","id":"72615127","total_energy_consumption_kwh":0,"total_volume_m3":0.001,"volume_flow_m3h":0,"power_kw":0,"flow_temperature_c":22.6,"return_temperature_c":21.8,"operating_time_h":2103,"operating_time_in_error_h":0,"timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'Heatoo;72615127;0;null;null;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'Heatooo sharky774 61243590 NOKEY'
+ comment = 'This telegram contains a negative power value encoded in bcd.'
+ telegram = 3E44A5119035246141047A1A0030052F2F_0C06026301000C13688609040B3B0802000C2B220000F00A5A71020A5E72020AA61800004C0636370100426CBF25
+ json = '{"_":"telegram","media":"heat","driver":"sharky774","name":"Heatooo","id":"61243590","total_energy_consumption_kwh":16302,"total_volume_m3":4098.668,"volume_flow_m3h":0.208,"power_kw":-0.022,"flow_temperature_c":27.1,"return_temperature_c":27.2,"operating_time_in_error_h":0,"energy_at_set_date_kwh":13736,"set_date":"2021-05-31","timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'Heatooo;61243590;16302;13736;2021-05-31;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'Coolo sharky774 71942539 NOKEY'
+ comment = 'This telegram contains cooling data as well.'
+ telegram = 5E44A51139259471410D7A720050052F2F_0C06742400008C1006000000000C13823522008C2013494400000B3B0000000C2B000000000A5A22030A5E91020AA61800004C0619130000CC100600000000426CDF252F2F2F2F2F2F2F2F2F2F2F
+ json = '{"_":"telegram","media":"heat/cooling load","driver":"sharky774","name":"Coolo","id":"71942539","total_energy_consumption_kwh":2474,"total_cooling_consumption_kwh":0,"total_volume_m3":223.582,"total_cooling_volume_m3":4.449,"volume_flow_m3h":0,"power_kw":0,"flow_temperature_c":32.2,"return_temperature_c":29.1,"operating_time_in_error_h":0,"energy_at_set_date_kwh":1319,"cooling_at_set_date_kwh":0,"set_date":"2022-05-31","timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'Coolo;71942539;2474;1319;2022-05-31;1111-11-11 11:11.11'
+ }
+ }
+}
diff --git a/components/wmbus_common/drivers/src/sontex868.xmq b/components/wmbus_common/drivers/src/sontex868.xmq
new file mode 100644
index 00000000..864b4b35
--- /dev/null
+++ b/components/wmbus_common/drivers/src/sontex868.xmq
@@ -0,0 +1,107 @@
+// Copyright (C) 2020-2022 Fredrik Öhrström (gpl-3.0-or-later)
+driver {
+ name = sontex868
+ meter_type = HeatCostAllocationMeter
+ default_fields = name,id,current_consumption_hca,set_date,consumption_at_set_date_hca,timestamp
+ detect {
+ mvt = SON,16,08
+ }
+ fields {
+ field {
+ name = current_consumption
+ quantity = HCA
+ info = 'The current heat cost allocation for this meter.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ measurement_type = Instantaneous
+ vif_range = HeatCostAllocation
+ }
+ }
+ field {
+ name = set_date
+ quantity = Text
+ info = 'The most recent billing period date.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = Date
+ storage_nr = 1
+ }
+ }
+ field {
+ name = consumption_at_set_date
+ quantity = HCA
+ info = 'Heat cost allocation at the most recent billing period date.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ measurement_type = Instantaneous
+ vif_range = HeatCostAllocation
+ storage_nr = 1
+ }
+ }
+ field {
+ name = current_temp
+ quantity = Temperature
+ info = 'The current temperature of the heating element.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ measurement_type = Instantaneous
+ vif_range = FlowTemperature
+ }
+ }
+ field {
+ name = current_room_temp
+ quantity = Temperature
+ info = 'The current room temperature.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ measurement_type = Instantaneous
+ vif_range = ExternalTemperature
+ }
+ }
+ field {
+ name = max_temp
+ quantity = Temperature
+ info = 'The maximum temperature so far during this billing period.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ measurement_type = Maximum
+ vif_range = FlowTemperature
+ }
+ }
+ field {
+ name = max_temp_previous_period
+ quantity = Temperature
+ info = 'The maximum temperature during the previous billing period.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ measurement_type = Maximum
+ vif_range = FlowTemperature
+ storage_nr = 1
+ }
+ }
+ field {
+ name = device
+ quantity = PointInTime
+ info = 'Date and time when the meter sent the telegram.'
+ display_unit = datetime
+ match {
+ measurement_type = Instantaneous
+ vif_range = DateTime
+ }
+ }
+ }
+ tests {
+ test {
+ args = 'MyHeatCoster sontex868 27282728 NOKEY'
+ telegram = AF46EE4D2827282716087A80000000_046D040A9F2A036E770000426CE1F7436E660000525900008288016C61258388016E0000008D8801EE1E3533FE00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005FF2D0000803F8520FF2D0000803F0259AD0A0265D8041259AD0A8310FD3100000082106C01018110FD610082206C9F2A0BFD0F01030102FF2C000002FD66AC08
+ json = '{"_":"telegram","media":"heat cost allocation","driver":"sontex868","name":"MyHeatCoster","id":"27282728","current_consumption_hca":119,"set_date":"2127-07-01","consumption_at_set_date_hca":102,"current_temp_c":27.33,"current_room_temp_c":12.4,"max_temp_c":27.33,"max_temp_previous_period_c":0,"device_datetime":"2020-10-31 10:04","timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'MyHeatCoster;27282728;119;2127-07-01;102;1111-11-11 11:11.11'
+ }
+ }
+}
diff --git a/components/wmbus_common/drivers/src/supercal.xmq b/components/wmbus_common/drivers/src/supercal.xmq
index d8c97332..4b2f2660 100644
--- a/components/wmbus_common/drivers/src/supercal.xmq
+++ b/components/wmbus_common/drivers/src/supercal.xmq
@@ -68,7 +68,7 @@ driver {
args = 'sonsupercal supercal 89508019 NOKEY'
comment = 'Sontex Supercal 739 test case'
telegram = 4444EE4D198050891B047AFB000000_046D3612E423820A6CE1F1040628010000840A060000000004144A110000840A14000000000259E30F025D2A0C023B1D01032C3A0100
- json = '{"_":"telegram","flow_temperature_c": 40.67,"id": "89508019","media": "heat","meter": "supercal","meter_datetime": "2023-03-04 18:54","name": "sonsupercal","power_kw": 3.14,"return_temperature_c": 31.14,"target_date": "2127-01-01","target_kwh": 0,"target_m3": 0,"timestamp": "1111-11-11T11:11:11Z","total_kwh": 296,"total_m3": 44.26,"volume_flow_m3h": 0.285}'
+ json = '{"_":"telegram","flow_temperature_c": 40.67,"id": "89508019","media": "heat","driver": "supercal","meter_datetime": "2023-03-04 18:54","name": "sonsupercal","power_kw": 3.14,"return_temperature_c": 31.14,"target_date": "2127-01-01","target_kwh": 0,"target_m3": 0,"timestamp": "1111-11-11T11:11:11Z","total_kwh": 296,"total_m3": 44.26,"volume_flow_m3h": 0.285}'
fields = 'sonsupercal;89508019;296;1111-11-11 11:11.11'
}
}
diff --git a/components/wmbus_common/drivers/src/supercom587.xmq b/components/wmbus_common/drivers/src/supercom587.xmq
new file mode 100644
index 00000000..fc6c7110
--- /dev/null
+++ b/components/wmbus_common/drivers/src/supercom587.xmq
@@ -0,0 +1,46 @@
+// Copyright (C) 2017-2022 Fredrik Öhrström (gpl-3.0-or-later)
+driver {
+ name = supercom587
+ meter_type = WaterMeter
+ default_fields = name,id,total_m3,timestamp
+ detect {
+ mvt = SON,3c,06
+ mvt = SON,3c,07
+ }
+ library {
+ use = software_version
+ use = total_m3
+ }
+ fields {
+ field {
+ name = status
+ quantity = Text
+ info = 'Status of meter.'
+ attributes = STATUS,INCLUDE_TPL_STATUS
+ match {
+ measurement_type = Instantaneous
+ vif_range = ErrorFlags
+ }
+ lookup {
+ name = ERROR_FLAGS
+ map_type = BitToString
+ mask_bits = 0x000f
+ default_message = OK
+ }
+ }
+ }
+ tests {
+ test {
+ args = 'MyWarmWater supercom587 12345678 NOKEY'
+ telegram = A244EE4D785634123C067A8F000000_0C1348550000426CE1F14C130000000082046C21298C0413330000008D04931E3A3CFE3300000033000000330000003300000033000000330000003300000033000000330000003300000033000000330000004300000034180000046D0D0B5C2B03FD6C5E150082206C5C290BFD0F0200018C4079678885238310FD3100000082106C01018110FD610002FD66020002FD170000
+ json = '{"_":"telegram","media":"warm water","driver":"supercom587","name":"MyWarmWater","id":"12345678","total_m3":5.548,"software_version":"010002","status":"OK","timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'MyWarmWater;12345678;5.548;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'MyColdWater supercom587 11111111 NOKEY'
+ telegram = A244EE4D111111113C077AAC000000_0C1389490000426CE1F14C130000000082046C21298C0413010000008D04931E3A3CFE0100000001000000010000000100000001000000010000000100000001000000010000000100000001000000010000001600000031130000046D0A0C5C2B03FD6C60150082206C5C290BFD0F0200018C4079629885238310FD3100000082106C01018110FD610002FD66020002FD170000
+ json = '{"_":"telegram","media":"water","driver":"supercom587","name":"MyColdWater","id":"11111111","total_m3":4.989,"software_version":"010002","status":"OK","timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'MyColdWater;11111111;4.989;1111-11-11 11:11.11'
+ }
+ }
+}
diff --git a/components/wmbus_common/drivers/src/topaseskr.xmq b/components/wmbus_common/drivers/src/topaseskr.xmq
new file mode 100644
index 00000000..299e9b34
--- /dev/null
+++ b/components/wmbus_common/drivers/src/topaseskr.xmq
@@ -0,0 +1,121 @@
+// Copyright (C) 2017-2022 Fredrik Öhrström (gpl-3.0-or-later)
+// Copyright (C) 2020 Avandorp (gpl-3.0-or-later)
+driver {
+ name = topaseskr
+ meter_type = WaterMeter
+ default_fields = name,id,total_m3,temperature_c,current_flow_m3h,volume_year_period_m3,reverse_volume_year_period_m3,meter_year_period_end_date,volume_month_period_m3,meter_month_period_end_datetime,timestamp
+ detect {
+ mvt = AMT,f1,06
+ mvt = AMT,f1,07
+ }
+ library {
+ use = total_m3
+ use = access_counter
+ }
+ fields {
+ field {
+ name = temperature
+ quantity = Temperature
+ info = 'Current water temperature recorded by this meter.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ measurement_type = Instantaneous
+ vif_range = FlowTemperature
+ }
+ }
+ field {
+ name = current_flow
+ quantity = Flow
+ info = 'The current water flow.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ measurement_type = Instantaneous
+ vif_range = VolumeFlow
+ }
+ }
+ field {
+ name = volume_year_period
+ quantity = Volume
+ info = 'Volume up to end of last year-period.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ measurement_type = Instantaneous
+ vif_range = Volume
+ storage_nr = 1
+ }
+ }
+ field {
+ name = reverse_volume_year_period
+ quantity = Volume
+ info = 'Reverse volume in this year-period (?)'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ measurement_type = Instantaneous
+ vif_range = Volume
+ storage_nr = 1
+ tariff_nr = 1
+ }
+ }
+ field {
+ name = meter_year_period_end_date
+ quantity = Text
+ info = 'Meter date for year-period end.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = Date
+ storage_nr = 1
+ }
+ }
+ field {
+ name = volume_month_period
+ quantity = Volume
+ info = 'Volume up to end of last month-period.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ measurement_type = Instantaneous
+ vif_range = Volume
+ storage_nr = 3
+ }
+ }
+ field {
+ name = meter_month_period_end
+ quantity = PointInTime
+ info = 'Meter timestamp for month-period end.'
+ display_unit = datetime
+ match {
+ measurement_type = Instantaneous
+ vif_range = DateTime
+ storage_nr = 3
+ }
+ }
+ field {
+ name = battery
+ quantity = Time
+ info = 'Remaining battery life in years.'
+ display_unit = y
+ match {
+ measurement_type = Instantaneous
+ vif_range = RemainingBattery
+ }
+ }
+ }
+ tests {
+ test {
+ args = 'Witer topaseskr 78563412 NOKEY'
+ telegram = 4E44B40512345678F1077A310040052F2F_01FD08040C13991848004C1359423500CC101300000000CC201359423500426C7F2C0B3B00000002FD74DA10025AD300C4016D3B179F27CC011387124600
+ json = '{"_":"telegram","media":"water","driver":"topaseskr","name":"Witer","id":"78563412","total_m3":481.899,"access_counter":4,"temperature_c":21.1,"current_flow_m3h":0,"volume_year_period_m3":354.259,"reverse_volume_year_period_m3":0,"meter_year_period_end_date":"2019-12-31","volume_month_period_m3":461.287,"meter_month_period_end_datetime":"2020-07-31 23:59","battery_y":11.811331,"timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'Witer;78563412;481.899;21.1;0;354.259;0;2019-12-31;461.287;2020-07-31 23:59;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'Woter topaseskr 69190253 NOKEY'
+ telegram = 4E44B40553021969F1077A0C0040052F2F_01FD08800C13914544004C1393673500CC101300000000CC201393673500426CDF2C0B3B0100F002FD747912025AAE00C4016D3B17FF25CC011325584100
+ json = '{"_":"telegram","access_counter": 128,"battery_y": 12.947562,"current_flow_m3h": -0.001,"id": "69190253","media": "water","driver": "topaseskr","meter_month_period_end_datetime": "2023-05-31 23:59","meter_year_period_end_date": "2022-12-31","name": "Woter","reverse_volume_year_period_m3": 0,"temperature_c": 17.4,"timestamp": "1111-11-11T11:11:11Z","total_m3": 444.591,"volume_month_period_m3": 415.825,"volume_year_period_m3": 356.793}'
+ fields = 'Woter;69190253;444.591;17.4;-0.001;356.793;0;2022-12-31;415.825;2023-05-31 23:59;1111-11-11 11:11.11'
+ }
+ }
+}
diff --git a/components/wmbus_common/drivers/src/topaseskr2.xmq b/components/wmbus_common/drivers/src/topaseskr2.xmq
new file mode 100644
index 00000000..c83e6cda
--- /dev/null
+++ b/components/wmbus_common/drivers/src/topaseskr2.xmq
@@ -0,0 +1,91 @@
+// Copyright (C) 2026 Fredrik Öhrström (gpl-3.0-or-later)
+driver {
+ name = topaseskr2
+ meter_type = WaterMeter
+ default_fields = name,id,status,total_m3,timestamp
+ detect {
+ mvt = IMT,f2,07
+ }
+ library {
+ use = total_m3
+ use = access_counter
+ use = flow_temperature_c
+ use = volume_flow_m3h
+ }
+ fields {
+ field {
+ name = status
+ quantity = Text
+ info = 'Status and error flags.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = ErrorFlags
+ }
+ lookup {
+ name = ERROR_FLAGS
+ mask_bits = 0xffffffff
+ map_type = BitToString
+ default_message = OK
+ }
+ }
+ field {
+ name = battery
+ quantity = Time
+ info = 'Remaining battery life in years.'
+ display_unit = y
+ match {
+ measurement_type = Instantaneous
+ vif_range = RemainingBattery
+ }
+ }
+ field {
+ name = target_year
+ quantity = Volume
+ info = 'Volume up to end of last year.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = Volume
+ storage_nr = 1
+ }
+ }
+ field {
+ name = target_year
+ quantity = PointInTime
+ info = 'Meter date for end of last year.'
+ display_unit = date
+ match {
+ measurement_type = Instantaneous
+ vif_range = Date
+ storage_nr = 1
+ }
+ }
+ field {
+ name = target
+ quantity = Volume
+ info = 'Total volume when previous billing period ended.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = Volume
+ storage_nr = 8
+ }
+ }
+ field {
+ name = target
+ quantity = PointInTime
+ info = 'Previous billing period ended at this time.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = DateTime
+ storage_nr = 8
+ }
+ }
+ }
+ tests {
+ test {
+ args = 'W topaseskr2 88475766 NOKEY'
+ telegram = 3E44B42566574788F2077A6F0030052F2F_0C138150020004FD170000000002FD74AD150A5A1001426C3F3C4C130002000084046D3B375C328C041388380100
+ json = '{"_": "telegram","battery_y": 15.192646,"driver": "topaseskr2","flow_temperature_c": 11,"id": "88475766","media": "water","name": "W","status": "OK","target_datetime": "2026-02-28 23:59","target_m3": 13.888,"target_year_date": "2025-12-31","target_year_m3": 0.2,"timestamp": "1111-11-11T11:11:11Z","total_m3": 25.081}'
+ fields = 'W;88475766;OK;25.081;1111-11-11 11:11.11'
+ }
+ }
+}
diff --git a/components/wmbus_common/drivers/src/tsd2.xmq b/components/wmbus_common/drivers/src/tsd2.xmq
new file mode 100644
index 00000000..064174eb
--- /dev/null
+++ b/components/wmbus_common/drivers/src/tsd2.xmq
@@ -0,0 +1,135 @@
+// Copyright (C) 2020-2026 Fredrik Öhrström (gpl-3.0-or-later)
+driver {
+ name = tsd2
+ meter_type = SmokeDetector
+ default_fields = name,id,status,prev_date,timestamp
+ manufacturer = Techem
+ detect {
+ mvt = TCH,76,F0
+ }
+ fields {
+ field {
+ name = payload
+ quantity = Text
+ attributes = REQUIRED
+ match_entire_payload = true
+ ixml = "decode = status_byte, date_word, -byte*.
+ status_byte = byte, @DV_status.
+ date_word = byte, byte, @DV_date_raw.
+ -hex = ['A'-'F'; '0'-'9'].
+ -byte = hex, hex.
+ DV_status>dvk = +'0101'.
+ DV_date_raw>dvk = +'02FD3A'."
+ }
+ field {
+ name = status
+ quantity = Text
+ info = 'The current status: OK or SMOKE or errors.'
+ attributes = STATUS,INCLUDE_TPL_STATUS
+ }
+ field {
+ name = detector
+ quantity = Text
+ info = 'The current status: OK or SMOKE.'
+ attributes = INJECT_INTO_STATUS,HIDE
+ vif_scaling = None
+ dif_signedness = Unsigned
+ match {
+ measurement_type = Instantaneous
+ difvifkey = 0101
+ }
+ lookup {
+ name = STATUS
+ map_type = IndexToString
+ mask_bits = 0xff
+ map {
+ name = OK
+ value = 0x00
+ test = Set
+ }
+ map {
+ name = SMOKE
+ value = 0x01
+ test = Set
+ }
+ map {
+ name = DEFECT
+ value = 0x80
+ test = Set
+ }
+ }
+ }
+ field {
+ name = prev_date_raw
+ quantity = Dimensionless
+ attributes = HIDE
+ match {
+ difvifkey = 02FD3A
+ }
+ }
+ field {
+ name = prev_day
+ quantity = Dimensionless
+ attributes = HIDE
+ calculate = 'prev_date_raw_counter % 32counter'
+ }
+ field {
+ name = prev_month
+ quantity = Dimensionless
+ attributes = HIDE
+ calculate = '(prev_date_raw_counter >> 5counter) % 16counter'
+ }
+ field {
+ name = prev_year
+ quantity = Dimensionless
+ attributes = HIDE
+ calculate = 'prev_date_raw_counter >> 9counter'
+ }
+ field {
+ name = prev
+ quantity = PointInTime
+ display_unit = DateLT
+ info = 'Date of previous billing period.'
+ calculate = "'2000-01-01' +
+ (((prev_year_counter * 12counter) + prev_month_counter - 1counter) * 1month) +
+ ((prev_day_counter - 1counter) * 24h)"
+ }
+ }
+ tests {
+ test {
+ comment = 'Status OK with date'
+ args = 'Smokey tsd2 91633569 NOKEY'
+ telegram = 294468506935639176F0A0_009F2782290060822900000401D6311AF93E1BF93E008DC3009ED4000FE500
+ json = '{"_":"telegram","media":"smoke detector","driver":"tsd2","name":"Smokey","id":"91633569","status":"OK","prev_date":"2019-12-31","timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'Smokey;91633569;OK;2019-12-31;1111-11-11 11:11.11'
+ }
+ test {
+ comment = 'Status SMOKE with date'
+ args = 'Smokey tsd2 91633569 NOKEY'
+ telegram = 294468506935639176F0A0_019F2782290060822900000401D6311AF93E1BF93E008DC3009ED4000FE500
+ json = '{"_":"telegram","media":"smoke detector","driver":"tsd2","name":"Smokey","id":"91633569","status":"SMOKE","prev_date":"2019-12-31","timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'Smokey;91633569;SMOKE;2019-12-31;1111-11-11 11:11.11'
+ }
+ test {
+ comment = 'Status DEFECT with date'
+ args = 'Smokey tsd2 91633569 NOKEY'
+ telegram = 294468506935639176F0A0_809F2782290060822900000401D6311AF93E1BF93E008DC3009ED4000FE580
+ json = '{"_":"telegram","media":"smoke detector","driver":"tsd2","name":"Smokey","id":"91633569","status":"DEFECT","prev_date":"2019-12-31","timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'Smokey;91633569;DEFECT;2019-12-31;1111-11-11 11:11.11'
+ }
+ test {
+ comment = 'Invalid status byte.'
+ args = 'Smokey tsd2 91633569 NOKEY'
+ telegram = 294468506935639176F0A0_719F2782290060822900000401D6311AF93E1BF93E008DC3009ED4000FE500
+ json = '{"_":"telegram","media":"smoke detector","driver":"tsd2","name":"Smokey","id":"91633569","status":"STATUS_71","prev_date":"2019-12-31","timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'Smokey;91633569;STATUS_71;2019-12-31;1111-11-11 11:11.11'
+ }
+ test {
+ comment = 'Broken payload, decoding error in status and prev_date is null'
+ args = 'Smokey tsd2 91633569 NOKEY'
+ telegram = 0C4468506935639176F0A0_019F
+ json = '{"_":"telegram","media":"smoke detector","driver":"tsd2","name":"Smokey","id":"91633569","status":"DECODING_ERROR_payload","prev_date":null,"timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'Smokey;91633569;DECODING_ERROR_payload;null;1111-11-11 11:11.11'
+ }
+ }
+}
diff --git a/components/wmbus_common/drivers/src/uiws.xmq b/components/wmbus_common/drivers/src/uiws.xmq
new file mode 100644
index 00000000..897d3f53
--- /dev/null
+++ b/components/wmbus_common/drivers/src/uiws.xmq
@@ -0,0 +1,104 @@
+// Copyright (C) 2026 Fredrik Öhrström (gpl-3.0-or-later)
+driver {
+ name = uiws
+ meter_type = WaterMeter
+ default_fields = name,id,status,total_m3,target_m3,timestamp
+ manufacturer = Zenner
+ model = 'IUWS ultrasonic water meter'
+ detect {
+ mvt = ZRI,99,07
+ }
+ library {
+ use = total_m3
+ use = meter_datetime
+ use = flow_temperature_c
+ use = external_temperature_c
+ }
+ fields {
+ field {
+ name = status
+ quantity = Text
+ info = 'Status and error flags.'
+ attributes = STATUS,INCLUDE_TPL_STATUS
+ match {
+ measurement_type = Instantaneous
+ vif_range = ErrorFlags
+ }
+ lookup {
+ name = ERROR_FLAGS
+ map_type = BitToString
+ mask_bits = 0xffff
+ default_message = OK
+ /* map {
+ name = WootPleaseHelpOutHere
+ bit = 0
+ test = Set
+ } */
+ }
+ }
+ field {
+ name = unknown
+ quantity = Volume
+ info = 'What is this value?'
+ null_value = -2147483.648
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyVolumeVIF
+ storage_nr = 1
+ }
+ }
+ field {
+ name = target
+ quantity = PointInTime
+ info = 'Base date of compact profile (most recent monthly value).'
+ display_unit = date
+ match {
+ measurement_type = Instantaneous
+ vif_range = Date
+ storage_nr = 30
+ }
+ }
+ field {
+ name = target
+ quantity = Volume
+ info = 'Base HCA value of compact profile.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyVolumeVIF
+ storage_nr = 30
+ }
+ }
+ field {
+ name = 'target_{storage_counter-30counter}'
+ quantity = Volume
+ info = 'Compact-profile history values (most recent first after base month).'
+ null_value = -2147483.648
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyVolumeVIF
+ add_combinable = Synthetic
+ storage_nr = 30,40
+ }
+ }
+ field {
+ name = 'target_{storage_counter-30counter}'
+ quantity = PointInTime
+ info = 'Billing period date for each compact-profile history slot.'
+ display_unit = date
+ match {
+ measurement_type = Instantaneous
+ vif_range = Date
+ add_combinable = Synthetic
+ storage_nr = 30,40
+ }
+ }
+ }
+ tests {
+ test {
+ args = 'ZW uiws 61330764 NOKEY'
+ telegram = 6E44496A6407336199077A361260252F2F_046D22484136041301000000441300000080820F6C4136840F13010000008D0F93132E34FE000000000000008000000080000000800000008000000080000000800000008000000080000000800000008002FD1728000259340702656C07
+ json = '{"_": "telegram","external_temperature_c": 19,"flow_temperature_c": 18.44,"id": "61330764","media": "water","driver": "uiws","meter_datetime": "2026-06-01 08:34","name": "ZW","status": "ERROR ERROR_FLAGS_28 TEMPORARY_ERROR","target_10_date": "2025-08-01","target_10_m3": null,"target_1_date": "2026-05-01","target_1_m3": 0,"target_2_date": "2026-04-01","target_2_m3": null,"target_3_date": "2026-03-01","target_3_m3": null,"target_4_date": "2026-02-01","target_4_m3": null,"target_5_date": "2026-01-01","target_5_m3": null,"target_6_date": "2025-12-01","target_6_m3": null,"target_7_date": "2025-11-01","target_7_m3": null,"target_8_date": "2025-10-01","target_8_m3": null,"target_9_date": "2025-09-01","target_9_m3": null,"target_date": "2026-06-01","target_m3": 0.001,"timestamp": "1111-11-11T11:11:11Z","total_m3": 0.001,"unknown_m3": null}'
+ fields = 'ZW;61330764;ERROR ERROR_FLAGS_28 TEMPORARY_ERROR;0.001;0.001;1111-11-11 11:11.11'
+ }
+ }
+}
diff --git a/components/wmbus_common/drivers/src/ultraheat.xmq b/components/wmbus_common/drivers/src/ultraheat.xmq
new file mode 100644
index 00000000..185ae3d0
--- /dev/null
+++ b/components/wmbus_common/drivers/src/ultraheat.xmq
@@ -0,0 +1,120 @@
+// Copyright (C) 2022-2023 Fredrik Öhrström (gpl-3.0-or-later)
+// Lumon Ultraheat heat meter.
+driver {
+ name = ultraheat
+ meter_type = HeatMeter
+ default_fields = name,id,heat_kwh,timestamp
+ detect {
+ mvt = LUG,04,04
+ }
+ library {
+ use = meter_datetime
+ use = fabrication_no
+ use = actuality_duration_s
+ use = target_kwh
+ }
+ fields {
+ field {
+ name = heat
+ quantity = Energy
+ info = 'The total heat energy consumption recorded by this meter.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyEnergyVIF
+ }
+ }
+ field {
+ name = volume
+ quantity = Volume
+ info = 'The total heating media volume recorded by this meter.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ measurement_type = Instantaneous
+ vif_range = Volume
+ }
+ }
+ field {
+ name = power
+ quantity = Power
+ info = 'The current power consumption.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ measurement_type = Instantaneous
+ vif_range = PowerW
+ }
+ }
+ field {
+ name = flow
+ quantity = Flow
+ info = 'The current heat media volume flow.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ measurement_type = Instantaneous
+ vif_range = VolumeFlow
+ }
+ }
+ field {
+ name = flow
+ quantity = Temperature
+ info = 'The current forward heat media temperature.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ measurement_type = Instantaneous
+ vif_range = FlowTemperature
+ }
+ }
+ field {
+ name = return
+ quantity = Temperature
+ info = 'The current return heat media temperature.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ measurement_type = Instantaneous
+ vif_range = ReturnTemperature
+ }
+ }
+ field {
+ name = status
+ quantity = Text
+ info = 'Meter status.'
+ attributes = STATUS,INCLUDE_TPL_STATUS
+ match {
+ measurement_type = Instantaneous
+ vif_range = ErrorFlags
+ }
+ lookup {
+ name = ERROR_FLAGS
+ map_type = BitToString
+ mask_bits = 0xffff
+ default_message = OK
+ }
+ }
+ }
+ tests {
+ test {
+ args = 'MyUltra ultraheat 70444600 NOKEY'
+ telegram = 68F8F86808007200464470A7320404270000000974040970040C0E082303000C14079519000B2D0500000B3B0808000A5B52000A5F51000A6206004C14061818004C0E490603000C7800464470891071609B102D020100DB102D0201009B103B6009009A105B78009A105F74000C22726701003C22000000007C2200000000426C01018C2006000000008C3006000000008C80100600000000CC200600000000CC300600000000CC801006000000009A115B64009A115F63009B113B5208009B112D020100BC0122000000008C010E490603008C2106000000008C3106000000008C811006000000008C011406181800046D310ACA210F21040010A0C116
+ json = '{"_":"telegram","media":"heat","driver":"ultraheat","name":"MyUltra","id":"70444600","actuality_duration_s":4,"flow_m3h":0.808,"flow_c":52,"heat_kwh":8974.444444,"power_kw":0.5,"return_c":51,"target_kwh":8513.611111,"volume_m3":1995.07,"fabrication_no":"70444600","meter_datetime":"2022-01-10 10:49","status":"OK","timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'MyUltra;70444600;8974.444444;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'MyUltra2 ultraheat 71635605 NOKEY'
+ telegram = 3b44a7320556637104047afa2000202f2f0c06774202000c14399956000b2d0200f00b3b3018000a5a51030a5e520302fd170000066d0c080af42500
+ json = '{"_":"telegram","flow_c": 35.1,"flow_m3h": 1.83,"heat_kwh": 24277,"id": "71635605","media": "heat","driver": "ultraheat","meter_datetime": "2023-05-20 10:08:12","name": "MyUltra2","power_kw": -0.2,"return_c": 35.2,"status": "UNKNOWN_20","timestamp": "1111-11-11T11:11:11Z","volume_m3": 5699.39}'
+ fields = 'MyUltra2;71635605;24277;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'MyUltra3 ultraheat 72905678 NOKEY'
+ telegram = 6344A7327856907204048C202B900F002C2592F604003504D66D5BEDC8577A2B004007102F2F_0C06690900000C14162800000B2D0000000B3B0000000A5A49070A5E19054C06000000008C01065100000002FD170000066D1138145B370002742F012F2F
+ json = '{"_":"telegram","media":"heat","driver":"ultraheat","name":"MyUltra3","id":"72905678","actuality_duration_s":303,"flow_m3h":0,"flow_c":74.9,"heat_kwh":969,"power_kw":0,"return_c":51.9,"target_kwh":0,"volume_m3":28.16,"meter_datetime":"2026-07-27 20:56:17","status":"OK","timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'MyUltra3;72905678;969;1111-11-11 11:11.11'
+ }
+ }
+}
diff --git a/components/wmbus_common/drivers/src/ultrimisv2.xmq b/components/wmbus_common/drivers/src/ultrimisv2.xmq
new file mode 100644
index 00000000..1fd5714e
--- /dev/null
+++ b/components/wmbus_common/drivers/src/ultrimisv2.xmq
@@ -0,0 +1,200 @@
+// Copyright (C) 2017-2026 Fredrik Öhrström (gpl-3.0-or-later)
+driver {
+ name = ultrimisv2
+ meter_type = WaterMeter
+ default_fields = name,id,status,total_m3,target_m3,timestamp
+ manufacturer = Apator
+ detect {
+ mvt = APA,01,16
+ mvt = APA,20,16
+ }
+ library {
+ use = total_m3
+ use = target_m3
+ use = total_backward_m3
+ }
+ fields {
+ field {
+ name = status
+ quantity = Text
+ info = 'Status and error flags.'
+ match {
+ difvifkey = 03FD17
+ }
+ lookup {
+ name = ERROR_FLAGS
+ map_type = BitToString
+ mask_bits = 0xffffff
+ default_message = OK
+ // Ongoing status, realtime status.
+ map {
+ name = TAMPER
+ value = 0x01
+ test = Set
+ }
+ map {
+ name = LOW_BATTERY
+ value = 0x02
+ test = Set
+ }
+ map {
+ name = DRY
+ value = 0x04
+ test = Set
+ }
+ map {
+ name = NO_FLOW
+ value = 0x08
+ test = Set
+ }
+ map {
+ name = HIGH_TEMPERATURE
+ value = 0x10
+ test = Set
+ }
+ map {
+ name = BURST
+ value = 0x20
+ test = Set
+ }
+ map {
+ name = REVERSE
+ value = 0x40
+ test = Set
+ }
+ map {
+ name = LEAK
+ value = 0x80
+ test = Set
+ }
+ // Status triggered at any time during this month.
+ map {
+ name = TAMPER_THIS_MONTH
+ value = 0x0100
+ test = Set
+ }
+ map {
+ name = LOW_BATTERY_THIS_MONTH
+ value = 0x0200
+ test = Set
+ }
+ map {
+ name = DRY_THIS_MONTH
+ value = 0x0400
+ test = Set
+ }
+ map {
+ name = NO_FLOW_THIS_MONTH
+ value = 0x0800
+ test = Set
+ }
+ map {
+ name = HIGH_TEMPERATURE_THIS_MONTH
+ value = 0x1000
+ test = Set
+ }
+ map {
+ name = BURST_THIS_MONTH
+ value = 0x2000
+ test = Set
+ }
+ map {
+ name = REVERSE_THIS_MONTH
+ value = 0x4000
+ test = Set
+ }
+ map {
+ name = LEAK_THIS_MONTH
+ value = 0x8000
+ test = Set
+ }
+ // Status triggered at any time during the previous month.
+ map {
+ name = TAMPER_PREV_MONTH
+ value = 0x010000
+ test = Set
+ }
+ map {
+ name = LOW_BATTERY_PREV_MONTH
+ value = 0x020000
+ test = Set
+ }
+ map {
+ name = DRY_PREV_MONTH
+ value = 0x040000
+ test = Set
+ }
+ map {
+ name = NO_FLOW_PREV_MONTH
+ value = 0x080000
+ test = Set
+ }
+ map {
+ name = HIGH_TEMPERATURE_PREV_MONTH
+ value = 0x100000
+ test = Set
+ }
+ map {
+ name = BURST_PREV_MONTH
+ value = 0x200000
+ test = Set
+ }
+ map {
+ name = REVERSE_PREV_MONTH
+ value = 0x400000
+ test = Set
+ }
+ map {
+ name = LEAK_PREV_MONTH
+ value = 0x800000
+ test = Set
+ }
+ }
+ }
+ }
+ tests {
+ test {
+ args = 'Water ultrimisv2 95969798 NOKEY'
+ telegram = 2E4401069897969501167A4B0320052F2F_0413320C000003FD1700000044132109000004933C000000002F2F2F2F2F
+ json = '{"_":"telegram","media":"cold water","driver":"ultrimisv2","name":"Water","id":"95969798","total_m3":3.122,"target_m3":2.337,"status":"OK","total_backward_m3":0,"timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'Water;95969798;OK;3.122;2.337;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'Water ultrimisv2 95969798 NOKEY'
+ telegram = 2E4401069897969501167A4B0320052F2F_0413320C000003FD1700000044132109000004933C000000002F2F2F2F2F
+ json = '{"_":"telegram","media":"cold water","driver":"ultrimisv2","name":"Water","id":"95969798","total_m3":3.122,"target_m3":2.337,"status":"OK","total_backward_m3":0,"timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'Water;95969798;OK;3.122;2.337;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'Water2 ultrimisv2 95969798 NOKEY'
+ comment = 'Force set error flags to test historical states.'
+ telegram = 2E4401069897969501167A4B0320052F2F_0413320C000003FD1708080144132109000004933C000000002F2F2F2F2F
+ json = '{"_": "telegram","id": "95969798","media": "cold water","driver": "ultrimisv2","name": "Water2","status": "NO_FLOW NO_FLOW_THIS_MONTH TAMPER_PREV_MONTH","target_m3": 2.337,"timestamp": "1111-11-11T11:11:11Z","total_backward_m3": 0,"total_m3": 3.122}'
+ fields = 'Water2;95969798;NO_FLOW NO_FLOW_THIS_MONTH TAMPER_PREV_MONTH;3.122;2.337;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'WW ultrimisv2 80291792 NOKEY'
+ telegram = 434401069217298020168C00B3900F002C252471316AA947CD928D1D6C377AC5002007102F2F_0413A001000003FD171400084413F500000004933C000000002F2F2F2F2F
+ json = '{"_": "telegram","id": "80291792","media": "cold water","driver": "ultrimisv2","name": "WW","status": "DRY HIGH_TEMPERATURE NO_FLOW_PREV_MONTH","target_m3": 0.245,"timestamp": "1111-11-11T11:11:11Z","total_backward_m3": 0,"total_m3": 0.416}'
+ fields = 'WW;80291792;DRY HIGH_TEMPERATURE NO_FLOW_PREV_MONTH;0.416;0.245;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'WWW ultrimisv2 80024171 NOKEY'
+ telegram = 434401067141028020168C008B900F002C257F53316ABC7526B6B75FEBC27AAD002007102F2F_04130E05000003FD1700400844135504000004933C540000002F2F2F2F2F
+ json = '{"_": "telegram","id": "80024171","media": "cold water","driver": "ultrimisv2","name": "WWW","status": "NO_FLOW_PREV_MONTH REVERSE_THIS_MONTH","target_m3": 1.109,"timestamp": "1111-11-11T11:11:11Z","total_backward_m3": 0.084,"total_m3": 1.294}'
+ fields = 'WWW;80024171;NO_FLOW_PREV_MONTH REVERSE_THIS_MONTH;1.294;1.109;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'Water ultrimisv2 95969798 NOKEY'
+ telegram = 2E4401069897969501167A4B0320052F2F_0413320C000003FD1700000044132109000004933C000000002F2F2F2F2F
+ json = '{"_":"telegram","media":"cold water","driver":"ultrimisv2","name":"Water","id":"95969798","total_m3":3.122,"target_m3":2.337,"status":"OK","total_backward_m3":0,"timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'Water;95969798;OK;3.122;2.337;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'Water ultrimisv2 95969798 NOKEY'
+ telegram = 2E4401069897969501167A4B0320052F2F_0413320C000003FD1700000044132109000004933C000000002F2F2F2F2F
+ json = '{"_":"telegram","media":"cold water","driver":"ultrimisv2","name":"Water","id":"95969798","total_m3":3.122,"target_m3":2.337,"status":"OK","total_backward_m3":0,"timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'Water;95969798;OK;3.122;2.337;1111-11-11 11:11.11'
+ }
+ }
+}
diff --git a/components/wmbus_common/drivers/src/unismart.xmq b/components/wmbus_common/drivers/src/unismart.xmq
new file mode 100644
index 00000000..c65bb9f3
--- /dev/null
+++ b/components/wmbus_common/drivers/src/unismart.xmq
@@ -0,0 +1,132 @@
+// Copyright (C) 2021-2022 Fredrik Öhrström (gpl-3.0-or-later)
+driver {
+ name = unismart
+ meter_type = GasMeter
+ default_fields = name,id,total_m3,target_m3,timestamp
+ detect {
+ mvt = AMX,01,03
+ }
+ library {
+ use = fabrication_no
+ }
+ fields {
+ field {
+ name = status
+ quantity = Text
+ info = 'Status of meter?'
+ attributes = STATUS
+ match {
+ difvifkey = 02FD74
+ }
+ lookup {
+ name = STATUS_FLAGS
+ map_type = BitToString
+ mask_bits = 0xffff
+ default_message = OK
+ }
+ }
+ field {
+ name = other
+ quantity = Text
+ info = 'Other status of meter?'
+ attributes = STATUS
+ match {
+ difvifkey = 017F
+ }
+ lookup {
+ name = OTHER_FLAGS
+ map_type = BitToString
+ mask_bits = 0xff
+ }
+ }
+ field {
+ name = total
+ quantity = PointInTime
+ info = 'Timestamp for this total measurement.'
+ display_unit = datetime
+ match {
+ measurement_type = Instantaneous
+ vif_range = DateTime
+ index_nr = 1
+ }
+ }
+ field {
+ name = total
+ quantity = Volume
+ info = 'The total gas consumption recorded by this meter.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = Volume
+ add_combinable = UncorrectedMeterUnit
+ }
+ }
+ field {
+ name = target
+ quantity = PointInTime
+ info = 'Timestamp for gas consumption recorded at the beginning of this month.'
+ display_unit = datetime
+ match {
+ measurement_type = Instantaneous
+ vif_range = DateTime
+ storage_nr = 1
+ }
+ }
+ field {
+ name = target
+ quantity = Volume
+ info = 'The total gas consumption recorded by this meter at the beginning of this month.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = Volume
+ storage_nr = 1
+ add_combinable = UncorrectedMeterUnit
+ }
+ }
+ field {
+ name = version
+ quantity = Text
+ info = 'Model version.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = ModelVersion
+ }
+ }
+ field {
+ name = supplier_info
+ quantity = Text
+ info = 'Supplier info?'
+ match {
+ measurement_type = Instantaneous
+ vif_range = SpecialSupplierInformation
+ }
+ }
+ field {
+ name = parameter_set
+ quantity = Text
+ info = 'Meter configued with this parameter set?'
+ match {
+ measurement_type = Instantaneous
+ vif_range = ParameterSet
+ }
+ }
+ field {
+ name = meter
+ quantity = PointInTime
+ info = 'Timestamp when this measurement was sent. This field actually contains the seconds as well, currently truncated.'
+ display_unit = datetime
+ match {
+ measurement_type = Instantaneous
+ vif_range = DateTime
+ index_nr = 2
+ }
+ }
+ }
+ tests {
+ test {
+ args = 'GasMeter unismart 00043094 00000000000000000000000000000000'
+ telegram = 6044B8059430040001037A1D005085E2B670BCF1A5C87E0C1A51DA18924EF984613DA2A9CD39D8F4C7208326C76D42DBEADF80D574192B71BD7C4F56A7F1513151768A9DB804883B28CB085CA2D0F7438C361CB9E2734712ED9BFBB2A14EF55208
+ json = '{"_":"telegram","media":"gas","driver":"unismart","name":"GasMeter","id":"00043094","fabrication_no":"03162296","status":"STATUS_FLAGS_CF0","other":"OTHER_FLAGS_14","total_datetime":"2021-09-15 13:18","total_m3":917,"target_datetime":"2021-09-01 06:00","target_m3":911.32,"version":" 4GGU","supplier_info":"00","parameter_set":"02","meter_datetime":"2021-09-15 13:18","timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'GasMeter;00043094;917;911.32;1111-11-11 11:11.11'
+ }
+ }
+}
diff --git a/components/wmbus_common/drivers/src/vario411.xmq b/components/wmbus_common/drivers/src/vario411.xmq
new file mode 100644
index 00000000..f8778fc3
--- /dev/null
+++ b/components/wmbus_common/drivers/src/vario411.xmq
@@ -0,0 +1,40 @@
+// Copyright (C) 2022 Fredrik Öhrström (gpl-3.0-or-later)
+driver {
+ name = vario411
+ meter_type = HeatMeter
+ default_fields = name,id,target_kwh,target_date,timestamp
+ detect {
+ mvt = TCH,28,04
+ }
+ fields {
+ field {
+ name = target
+ quantity = Energy
+ info = 'Total energy consumption at the end of the year.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyEnergyVIF
+ storage_nr = 1
+ }
+ }
+ field {
+ name = target
+ quantity = PointInTime
+ info = 'Date when previous year ended.'
+ display_unit = date
+ match {
+ measurement_type = Instantaneous
+ vif_range = Date
+ storage_nr = 1
+ }
+ }
+ }
+ tests {
+ test {
+ args = 'Howdy vario411 67627875 NOKEY'
+ telegram = 624468507578626728048C00F3900F002C25FEEB0600BA84134D9202A1327AFF003007102F2F_4406E1190000426CBF2C0F206730E2E7516874F5DB46B5A97816F575A29A1EA2717D6ADE5C2FE64517ED2B0497EE0FF64C2674CD0832572C484DDFED30
+ json = '{"_":"telegram","media":"heat","driver":"vario411","name":"Howdy","id":"67627875","target_kwh":6625,"target_date":"2021-12-31","timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'Howdy;67627875;6625;2021-12-31;1111-11-11 11:11.11'
+ }
+ }
+}
diff --git a/components/wmbus_common/drivers/src/vario451.xmq b/components/wmbus_common/drivers/src/vario451.xmq
new file mode 100644
index 00000000..dc5db9ce
--- /dev/null
+++ b/components/wmbus_common/drivers/src/vario451.xmq
@@ -0,0 +1,78 @@
+// Copyright (C) 2019-2022 Fredrik Öhrström (gpl-3.0-or-later)
+driver {
+ name = vario451
+ meter_type = HeatMeter
+ default_fields = name,id,total_kwh,current_kwh,previous_kwh,timestamp
+ detect {
+ mvt = TCH,27,04
+ mvt = TCH,27,C3
+ }
+ fields {
+ field {
+ name = payload_decoder
+ quantity = Text
+ info = 'Decode mfct specific payload (CI=0xA2) into prev/curr billing values.'
+ attributes = HIDE
+ match_entire_payload = true
+ ixml = "decode = -hdr, prev_val, -two_bytes, curr_val, -rest.
+ -hdr = hex, hex, hex, hex, hex, hex.
+ -two_bytes = hex, hex, hex, hex.
+ -rest = hex*.
+ -hex = ['A'-'F'; '0'-'9'].
+ prev_val = hex, hex, hex, hex, @DV_PREV.
+ curr_val = hex, hex, hex, hex, @DV_CURR.
+ DV_PREV>dvk = +'027E'.
+ DV_CURR>dvk = +'027F'."
+ }
+ field {
+ name = prev_raw
+ quantity = Dimensionless
+ info = 'Raw previous billing period value in units of mGJ (1/1000 GJ).'
+ attributes = HIDE
+ display_unit = counter
+ vif_scaling = None
+ dif_signedness = Unsigned
+ match {
+ difvifkey = 027E
+ }
+ }
+ field {
+ name = curr_raw
+ quantity = Dimensionless
+ info = 'Raw current billing period value in units of mGJ (1/1000 GJ).'
+ attributes = HIDE
+ display_unit = counter
+ vif_scaling = None
+ dif_signedness = Unsigned
+ match {
+ difvifkey = 027F
+ }
+ }
+ field {
+ name = total
+ quantity = Energy
+ info = 'The total energy consumption recorded by this meter.'
+ calculate = '(prev_raw_counter + curr_raw_counter) / 3.6 counter * 1 kwh'
+ }
+ field {
+ name = current
+ quantity = Energy
+ info = 'Energy consumption so far in this billing period.'
+ calculate = 'curr_raw_counter / 3.6 counter * 1 kwh'
+ }
+ field {
+ name = previous
+ quantity = Energy
+ info = 'Energy consumption in previous billing period.'
+ calculate = 'prev_raw_counter / 3.6 counter * 1 kwh'
+ }
+ }
+ tests {
+ test {
+ args = 'HeatMeter vario451 58234965 NOKEY'
+ telegram = 374468506549235827C3A2_129F25383300A8622600008200800A2AF862115175552877A36F26C9AB1CB24400000004000000000004908002
+ json = '{"_":"telegram","media":"heat","driver":"vario451","name":"HeatMeter","id":"58234965","total_kwh":6371.666667,"current_kwh":2729.444444,"previous_kwh":3642.222222,"timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'HeatMeter;58234965;6371.666667;2729.444444;3642.222222;1111-11-11 11:11.11'
+ }
+ }
+}
diff --git a/components/wmbus_common/drivers/src/vario451mid.xmq b/components/wmbus_common/drivers/src/vario451mid.xmq
new file mode 100644
index 00000000..9f5a088e
--- /dev/null
+++ b/components/wmbus_common/drivers/src/vario451mid.xmq
@@ -0,0 +1,78 @@
+// Copyright (C) 2022 Fredrik Öhrström (gpl-3.0-or-later)
+driver {
+ name = vario451mid
+ meter_type = HeatMeter
+ default_fields = name,id,total_energy_consumption_kwh,energy_at_old_date_kwh,energy_at_set_date_kwh,timestamp
+ detect {
+ mvt = TCH,17,04
+ }
+ fields {
+ field {
+ name = total_energy_consumption
+ quantity = Energy
+ info = 'The total energy consumption recorded by this meter.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyEnergyVIF
+ }
+ }
+ field {
+ name = energy_at_old_date
+ quantity = Energy
+ info = 'The total energy consumption recorded when?'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyEnergyVIF
+ storage_nr = 1
+ }
+ }
+ field {
+ name = old
+ quantity = PointInTime
+ info = 'The last billing old date?'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ display_unit = DateLT
+ match {
+ measurement_type = Instantaneous
+ vif_range = Date
+ storage_nr = 1
+ }
+ }
+ field {
+ name = energy_at_set_date
+ quantity = Energy
+ info = 'The total energy consumption recorded by this meter at the due date.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyEnergyVIF
+ storage_nr = 8
+ }
+ }
+ field {
+ name = set
+ quantity = PointInTime
+ info = 'The last billing set date.'
+ display_unit = DateLT
+ match {
+ measurement_type = Instantaneous
+ vif_range = Date
+ storage_nr = 8
+ }
+ }
+ }
+ tests {
+ test {
+ args = 'Heato vario451mid 94430412 NOKEY'
+ telegram = 734468501204439417048c0084900f002c2536700000B767B64527c50ac67a33005007102f2f8404062846000082046c9f2c8d04861f1e72fe00000000000000000000000000000000000000000000000000000000440600000000426cffff0406c94700002f2f2f2f2f2f2f2f2f2f2f2f2f2f2f
+ json = '{"_":"telegram","media":"heat","driver":"vario451mid","name":"Heato","id":"94430412","total_energy_consumption_kwh":18377,"energy_at_old_date_kwh":0,"old_date":null,"energy_at_set_date_kwh":17960,"set_date":"2020-12-31","timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'Heato;94430412;18377;0;17960;1111-11-11 11:11.11'
+ }
+ }
+}
diff --git a/components/wmbus_common/drivers/src/waterstarm.xmq b/components/wmbus_common/drivers/src/waterstarm.xmq
new file mode 100644
index 00000000..e78e6149
--- /dev/null
+++ b/components/wmbus_common/drivers/src/waterstarm.xmq
@@ -0,0 +1,223 @@
+// Copyright (C) 2020-2026 Fredrik Öhrström (gpl-3.0-or-later)
+driver {
+ name = waterstarm
+ meter_type = WaterMeter
+ default_fields = name,id,total_m3,total_backwards_m3,status,timestamp
+ detect {
+ mvt = DWZ,00,06
+ mvt = DWZ,02,06
+ mvt = DWZ,02,07
+ mvt = EFE,03,07
+ mvt = EFE,70,07
+ mvt = DWZ,00,07
+ }
+ library {
+ use = total_m3
+ use = battery_v
+ }
+ fields {
+ field {
+ name = status
+ quantity = Text
+ info = 'Status and error flags.'
+ attributes = STATUS,INCLUDE_TPL_STATUS
+ match {
+ measurement_type = Instantaneous
+ vif_range = ErrorFlags
+ }
+ lookup {
+ name = ERROR_FLAGS
+ map_type = BitToString
+ mask_bits = 0xffff
+ default_message = OK
+ map {
+ name = SW_ERROR
+ value = 0x01
+ test = Set
+ }
+ map {
+ name = CRC_ERROR
+ value = 0x02
+ test = Set
+ }
+ map {
+ name = SENSOR_ERROR
+ value = 0x04
+ test = Set
+ }
+ map {
+ name = MEASUREMENT_ERROR
+ value = 0x08
+ test = Set
+ }
+ map {
+ name = BATTERY_VOLTAGE_ERROR
+ value = 0x10
+ test = Set
+ }
+ map {
+ name = MANIPULATION
+ value = 0x20
+ test = Set
+ }
+ map {
+ name = LEAKAGE_OR_NO_USAGE
+ value = 0x40
+ test = Set
+ }
+ map {
+ name = REVERSE_FLOW
+ value = 0x80
+ test = Set
+ }
+ map {
+ name = OVERLOAD
+ value = 0x100
+ test = Set
+ }
+ }
+ }
+ field {
+ name = total_backwards
+ quantity = Volume
+ info = 'The total backward water volume recorded by this meter.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyVolumeVIF
+ add_combinable = BackwardFlow
+ }
+ }
+ field {
+ name = meter_version
+ quantity = Text
+ info = 'Meter model/version.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = ModelVersion
+ }
+ }
+ field {
+ name = parameter_set
+ quantity = Text
+ info = 'Parameter set.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = ParameterSet
+ }
+ }
+ field {
+ name = set
+ quantity = PointInTime
+ info = 'The most recent billing period date.'
+ display_unit = DateLT
+ match {
+ measurement_type = Instantaneous
+ vif_range = Date
+ storage_nr = 1
+ }
+ }
+ field {
+ name = consumption_at_set_date
+ quantity = Volume
+ info = 'The total water consumption at the most recent billing period date.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = Volume
+ storage_nr = 1
+ }
+ }
+ field {
+ name = total_backwards_at_set_date
+ quantity = Volume
+ info = 'The total backward water volume recorded by this meter at the most recent billing period date.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = AnyVolumeVIF
+ storage_nr = 1
+ add_combinable = BackwardFlow
+ }
+ }
+ field {
+ name = 'consumption_at_history_{storage_counter - 1 counter}'
+ quantity = Volume
+ info = 'The total backward water volume recorded by this meter at the most recent billing period date.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = Volume
+ storage_nr = 2,16
+ }
+ }
+ field {
+ name = meter
+ quantity = PointInTime
+ info = 'Meter datetime.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = DateTime
+ }
+ }
+ // These history_7_date values should probably be removed.
+ field {
+ name = 'history_{storage_counter - 1 counter}'
+ quantity = PointInTime
+ info = 'The historic date.'
+ display_unit = DateLT
+ calculate = 'meter_datetime - ((storage_counter-1counter) * 1 month)'
+ match {
+ measurement_type = Instantaneous
+ vif_range = Volume
+ storage_nr = 2,16
+ }
+ }
+ }
+ tests {
+ test {
+ args = 'Woter waterstarm 20096221 BEDB81B52C29B5C143388CBB0D15A051'
+ telegram = 3944FA122162092002067A3600202567C94D48D00DC47B11213E23383DB51968A705AAFA60C60E263D50CD259D7C9A03FD0C08000002FD0B0011
+ json = '{"_":"telegram","media":"warm water","driver":"waterstarm","name":"Woter","id":"20096221","meter_datetime":"2020-07-30 10:40","total_m3":0.106,"total_backwards_m3":0,"status":"OK","meter_version":"000008","parameter_set":"1100","timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'Woter;20096221;0.106;0;OK;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'Woter waterstarm 20096221 BEDB81B52C29B5C143388CBB0D15A051'
+ telegram = 3944FA122162092002067A3604202567C94D48D00DC47B11213E23383DB51968A705AAFA60C60E263D50CD259D7C9A03FD0C08000002FD0B0011
+ json = '{"_":"telegram","media":"warm water","driver":"waterstarm","name":"Woter","id":"20096221","meter_datetime":"2020-07-30 10:40","total_m3":0.106,"total_backwards_m3":0,"status":"POWER_LOW","meter_version":"000008","parameter_set":"1100","timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'Woter;20096221;0.106;0;POWER_LOW;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'Water waterstarm 22996221 NOKEY'
+ telegram = 3944FA122162992202067A360420252F2F_046D282A9E2704136A00000002FD17400004933C000000002F2F2F2F2F2F03FD0C08000002FD0B0011
+ json = '{"_":"telegram","media":"warm water","driver":"waterstarm","name":"Water","id":"22996221","meter_datetime":"2020-07-30 10:40","total_m3":0.106,"total_backwards_m3":0,"status":"LEAKAGE_OR_NO_USAGE POWER_LOW","meter_version":"000008","parameter_set":"1100","timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'Water;22996221;0.106;0;LEAKAGE_OR_NO_USAGE POWER_LOW;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'Water waterstarm 11559999 NOKEY'
+ telegram = 2E44FA129999551100077A070020252F2F_046D0F28C22404139540000002FD17000001FD481D2F2F2F2F2F2F2F2F2F
+ json = '{"_":"telegram","media":"water","driver":"waterstarm","name":"Water","id":"11559999","meter_datetime":"2022-04-02 08:15","total_m3":16.533,"status":"OK","battery_v":2.9,"timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'Water;11559999;16.533;null;OK;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'WarmLorenz waterstarm 20050666 NOKEY'
+ telegram = 9644FA126606052000067A1E000020_046D3B2ED729041340D8000002FD17000001FD481D426CBF2C4413026C000084011348D20000C40113F3CB0000840213DCC40000C40213B8B60000840313849B0000C403138B8C0000840413E3800000C4041337770000840513026C0000C40513D65F00008406134F560000C40613604700008407139D370000C407137F3300008408135B2C0000
+ json = '{"_":"telegram","battery_v":2.9,"consumption_at_history_10_m3":24.534,"consumption_at_history_11_m3":22.095,"consumption_at_history_12_m3":18.272,"consumption_at_history_13_m3":14.237,"consumption_at_history_14_m3":13.183,"consumption_at_history_15_m3":11.355,"consumption_at_history_1_m3":53.832,"consumption_at_history_2_m3":52.211,"consumption_at_history_3_m3":50.396,"consumption_at_history_4_m3":46.776,"consumption_at_history_5_m3":39.812,"consumption_at_history_6_m3":35.979,"consumption_at_history_7_m3":32.995,"consumption_at_history_8_m3":30.519,"consumption_at_history_9_m3":27.65,"consumption_at_set_date_m3":27.65,"status":"OK","history_10_date":"2021-11-23","history_11_date":"2021-10-23","history_12_date":"2021-09-23","history_13_date":"2021-08-23","history_14_date":"2021-07-23","history_15_date":"2021-06-23","history_1_date":"2022-08-23","history_2_date":"2022-07-23","history_3_date":"2022-06-23","history_4_date":"2022-05-23","history_5_date":"2022-04-23","history_6_date":"2022-03-23","history_7_date":"2022-02-23","history_8_date":"2022-01-23","history_9_date":"2021-12-23","id":"20050666","media":"warm water","driver":"waterstarm","meter_datetime":"2022-09-23 14:59","name":"WarmLorenz","set_date":"2021-12-31","timestamp":"1111-11-11T11:11:11Z","total_m3":55.36}'
+ fields = 'WarmLorenz;20050666;55.36;null;OK;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'ColdLorenz waterstarm 20065160 NOKEY'
+ telegram = 9644FA126051062000077A78000020_046D392DD7290413901A000002FD17000001FD481D426CBF2C4413D312000084011399190000C40113841800008402130C180000C40213EC16000084031395150000C40313E3140000840413BD130000C404134C130000840513D3120000C4051322120000840613AF110000C4061397100000840713D00F0000C40713890E0000840813980C0000
+ json = '{"_":"telegram","battery_v":2.9,"consumption_at_history_10_m3":4.642,"consumption_at_history_11_m3":4.527,"consumption_at_history_12_m3":4.247,"consumption_at_history_13_m3":4.048,"consumption_at_history_14_m3":3.721,"consumption_at_history_15_m3":3.224,"consumption_at_history_1_m3":6.553,"consumption_at_history_2_m3":6.276,"consumption_at_history_3_m3":6.156,"consumption_at_history_4_m3":5.868,"consumption_at_history_5_m3":5.525,"consumption_at_history_6_m3":5.347,"consumption_at_history_7_m3":5.053,"consumption_at_history_8_m3":4.94,"consumption_at_history_9_m3":4.819,"consumption_at_set_date_m3":4.819,"status":"OK","history_10_date":"2021-11-23","history_11_date":"2021-10-23","history_12_date":"2021-09-23","history_13_date":"2021-08-23","history_14_date":"2021-07-23","history_15_date":"2021-06-23","history_1_date":"2022-08-23","history_2_date":"2022-07-23","history_3_date":"2022-06-23","history_4_date":"2022-05-23","history_5_date":"2022-04-23","history_6_date":"2022-03-23","history_7_date":"2022-02-23","history_8_date":"2022-01-23","history_9_date":"2021-12-23","id":"20065160","media":"water","driver":"waterstarm","meter_datetime":"2022-09-23 13:57","name":"ColdLorenz","set_date":"2021-12-31","timestamp":"1111-11-11T11:11:11Z","total_m3":6.8}'
+ fields = 'ColdLorenz;20065160;6.8;null;OK;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'water waterstarm 50496629 FFEEDDCCBBAA00998877665544332211'
+ telegram = a144c5142966495070078c20d07a0d0090257db8bb9938a1ff7c4b9a4492f4ea5f278b725057eee6837c17397e9605f3a448a338bd8407a2a5846632c334dff577f427f054f55e68a00fa5c85ccbc808ccdd2bb537a83234a50392968f00c1d7473455f9fc4c88fb195ca1712325da8c6aa7fdb7c4b77b9b84a4a0ccac4f586775fa66007dbdc2c615b69247401e9b9a863ac5a5873484b1b7d0178198e88f701e53
+ json = '{"_": "telegram","consumption_at_history_10_m3": -0.001,"consumption_at_history_11_m3": -0.001,"consumption_at_history_12_m3": -0.001,"consumption_at_history_13_m3": -0.001,"consumption_at_history_14_m3": -0.001,"consumption_at_history_15_m3": -0.001,"consumption_at_history_1_m3": 0.003,"consumption_at_history_2_m3": 0.003,"consumption_at_history_3_m3": 0.003,"consumption_at_history_4_m3": 0.002,"consumption_at_history_5_m3": 0.002,"consumption_at_history_6_m3": 0,"consumption_at_history_7_m3": 0,"consumption_at_history_8_m3": -0.001,"consumption_at_history_9_m3": -0.001,"consumption_at_set_date_m3": 0,"history_10_date": "2024-10-20","history_11_date": "2024-09-20","history_12_date": "2024-08-20","history_13_date": "2024-07-20","history_14_date": "2024-06-20","history_15_date": "2024-05-20","history_1_date": "2025-07-20","history_2_date": "2025-06-20","history_3_date": "2025-05-20","history_4_date": "2025-04-20","history_5_date": "2025-03-20","history_6_date": "2025-02-20","history_7_date": "2025-01-20","history_8_date": "2024-12-20","history_9_date": "2024-11-20","id": "50496629","media": "water","driver": "waterstarm","meter_datetime": "2025-08-20 14:51","name": "water","set_date":null,"status": "OK","timestamp": "1111-11-11T11:11:11Z","total_backwards_at_set_date_m3": 0,"total_m3": 0.003}'
+ fields = 'water;50496629;0.003;null;OK;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'water2 waterstarm 51398765 NOKEY'
+ telegram = A144C5146587395170078C20947AD10090252F2F_046D862D4A3104131300000001FD1700426C3F3C44130000000044933C0000000084011300000000C401130100000084021300000000C402130000000084031300000000C4031300000000840413FFFFFFFFC40413FFFFFFFF840513FFFFFFFFC40513FFFFFFFF840613FFFFFFFFC40613FFFFFFFF840713FFFFFFFFC40713FFFFFFFF840813FFFFFFFF2F2F2F2F
+ json = '{"_": "telegram","consumption_at_history_10_m3": -0.001,"consumption_at_history_11_m3": -0.001,"consumption_at_history_12_m3": -0.001,"consumption_at_history_13_m3": -0.001,"consumption_at_history_14_m3": -0.001,"consumption_at_history_15_m3": -0.001,"consumption_at_history_1_m3": 0,"consumption_at_history_2_m3": 0.001,"consumption_at_history_3_m3": 0,"consumption_at_history_4_m3": 0,"consumption_at_history_5_m3": 0,"consumption_at_history_6_m3": 0,"consumption_at_history_7_m3": -0.001,"consumption_at_history_8_m3": -0.001,"consumption_at_history_9_m3": -0.001,"consumption_at_set_date_m3": 0,"history_10_date": "2025-03-10","history_11_date": "2025-02-10","history_12_date": "2025-01-10","history_13_date": "2024-12-10","history_14_date": "2024-11-10","history_15_date": "2024-10-10","history_1_date": "2025-12-10","history_2_date": "2025-11-10","history_3_date": "2025-10-10","history_4_date": "2025-09-10","history_5_date": "2025-08-10","history_6_date": "2025-07-10","history_7_date": "2025-06-10","history_8_date": "2025-05-10","history_9_date": "2025-04-10","id": "51398765","media": "water","driver": "waterstarm","meter_datetime": "2026-01-10 13:06","name": "water2","set_date": "2025-12-31","status": "OK","timestamp": "1111-11-11T11:11:11Z","total_backwards_at_set_date_m3": 0,"total_m3": 0.019}'
+ fields = 'water2;51398765;0.019;null;OK;1111-11-11 11:11.11'
+ }
+ }
+}
diff --git a/components/wmbus_common/drivers/src/watertech.xmq b/components/wmbus_common/drivers/src/watertech.xmq
new file mode 100644
index 00000000..a7cd9cb3
--- /dev/null
+++ b/components/wmbus_common/drivers/src/watertech.xmq
@@ -0,0 +1,50 @@
+// Copyright (C) 2020-2023 Fredrik Öhrström (gpl-3.0-or-later)
+driver {
+ name = watertech
+ meter_type = WaterMeter
+ default_fields = name,id,status,total_m3,timestamp
+ detect {
+ mvt = WTT,59,07
+ }
+ library {
+ use = software_version
+ use = meter_datetime
+ }
+ fields {
+ field {
+ name = status
+ quantity = Text
+ info = 'Status and error flags.'
+ attributes = STATUS,INCLUDE_TPL_STATUS
+ match {
+ measurement_type = Instantaneous
+ vif_range = ErrorFlags
+ }
+ lookup {
+ name = ERROR_FLAGS
+ map_type = BitToString
+ mask_bits = 0xffff
+ default_message = OK
+ }
+ }
+ field {
+ name = total
+ quantity = Volume
+ info = 'The total water consumption recorded by this meter.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ measurement_type = Instantaneous
+ vif_range = Volume
+ }
+ }
+ }
+ tests {
+ test {
+ args = 'wtt watertech 38383838 NOKEY'
+ telegram = 3144945E3838383859078C20007A010020252F2F_066D1C1F0EF021000413B91E000002FD17000002FD0F4C2B2F2F2F2F2F2F
+ json = '{"_":"telegram","media":"water","driver":"watertech","name":"wtt","id":"38383838","software_version":"+L","meter_datetime":"2023-01-16 14:31:28","status":"OK","total_m3":7.865,"timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'wtt;38383838;OK;7.865;1111-11-11 11:11.11'
+ }
+ }
+}
diff --git a/components/wmbus_common/drivers/src/weh_07.xmq b/components/wmbus_common/drivers/src/weh_07.xmq
new file mode 100644
index 00000000..1671d404
--- /dev/null
+++ b/components/wmbus_common/drivers/src/weh_07.xmq
@@ -0,0 +1,60 @@
+// Copyright (C) 2021-2022 Fredrik Öhrström (gpl-3.0-or-later)
+driver {
+ name = weh_07
+ meter_type = WaterMeter
+ default_fields = name,id,total_m3,timestamp
+ detect {
+ mvt = WEH,fe,07
+ mvt = WEH,03,07
+ }
+ fields {
+ field {
+ name = total
+ quantity = Volume
+ info = 'The total water consumption recorded by this meter.'
+ vif_scaling = Auto
+ dif_signedness = Signed
+ match {
+ measurement_type = Instantaneous
+ vif_range = Volume
+ }
+ }
+ field {
+ name = target
+ quantity = PointInTime
+ info = 'The most recent billing period date.'
+ display_unit = date
+ match {
+ measurement_type = Instantaneous
+ vif_range = Date
+ storage_nr = 1
+ }
+ }
+ field {
+ name = target
+ quantity = Volume
+ info = 'The total water consumption at the most recent billing period date.'
+ match {
+ measurement_type = Instantaneous
+ vif_range = Volume
+ storage_nr = 1
+ }
+ }
+ }
+ tests {
+ test {
+ args = 'Vatten weh_07 86868686 NOKEY'
+ comment = 'Techem radio convert + Wehrle water meter combo.'
+ telegram = 494468509494949495377286868686A85CFE07A90030052F2F_0413100000000F52FCF6A52A90A8D83CA8F7FEAE86990502323D0C70EFF49833C7C1696F75BCABC1E52E6305308D0F31FB
+ json = '{"_":"telegram","media":"water","driver":"weh_07","name":"Vatten","id":"86868686","total_m3":0.016,"timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'Vatten;86868686;0.016;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'Vattenn weh_07 27604781 NOKEY'
+ comment = 'A normal water meter.'
+ telegram = 5244A85C8147602703077A5B0840252F2F_0413B39100004413000000004D931E2C73FE0000000000000000000000000000000000000000000000000000000000000000000000009885001A0C002F2F426CBE29
+ json = '{"_":"telegram","media":"water","driver":"weh_07","name":"Vattenn","id":"27604781","total_m3":37.299,"target_date":"2021-09-30","target_m3":0,"timestamp":"1111-11-11T11:11:11Z"}'
+ fields = 'Vattenn;27604781;37.299;1111-11-11 11:11.11'
+ }
+ }
+}
diff --git a/components/wmbus_common/drivers/src/werhlemodwm.xmq b/components/wmbus_common/drivers/src/werhlemodwm.xmq
index 78cbb156..187e0e19 100644
--- a/components/wmbus_common/drivers/src/werhlemodwm.xmq
+++ b/components/wmbus_common/drivers/src/werhlemodwm.xmq
@@ -49,7 +49,7 @@ driver {
test {
args = 'licznik werhlemodwm 16466583 NOKEY'
telegram = 68C4C46808637283654616475F03160C000000_041300000000046D2B030000426C992B44133D00000042EC7EB92B0C785506140082016C9E2B8401133D000000C2016C7F2CC401133D00000082026C9F218402133D000000C2026C9D22C402133D00000082036C9F238403133D000000C2036C9E24C403133D00000082046C9F258404133D000000C2046C9E26C404133D00000082056C9F278405133D000000C2056C9F28C405133D00000082066C9E298406133D000000C2066C9F2AC406133D0000000F010000_C316
- json = '{"_":"telegram","fabrication_no": "00140655","id": "16466583","media": "cold water","meter": "werhlemodwm","meter_datetime": "2000-00-00 03:43","name": "licznik","next_target_date": "2021-11-25","target_10_date": "2020-07-31","target_10_m3": 0.061,"target_11_date": "2020-08-31","target_11_m3": 0.061,"target_12_date": "2020-09-30","target_12_m3": 0.061,"target_13_date": "2020-10-31","target_13_m3": 0.061,"target_1_date": "2020-11-25","target_1_m3": 0.061,"target_2_date": "2020-11-30","target_2_m3": 0.061,"target_3_date": "2019-12-31","target_3_m3": 0.061,"target_4_date": "2020-01-31","target_4_m3": 0.061,"target_5_date": "2020-02-29","target_5_m3": 0.061,"target_6_date": "2020-03-31","target_6_m3": 0.061,"target_7_date": "2020-04-30","target_7_m3": 0.061,"target_8_date": "2020-05-31","target_8_m3": 0.061,"target_9_date": "2020-06-30","target_9_m3": 0.061,"target_date": "2020-11-25","target_m3": 0.061,"timestamp": "1111-11-11T11:11:11Z","total_m3": 0}'
+ json = '{"_":"telegram","fabrication_no": "00140655","id": "16466583","media": "cold water","driver": "werhlemodwm","meter_datetime": "2000-00-00 03:43","name": "licznik","next_target_date": "2021-11-25","target_10_date": "2020-07-31","target_10_m3": 0.061,"target_11_date": "2020-08-31","target_11_m3": 0.061,"target_12_date": "2020-09-30","target_12_m3": 0.061,"target_13_date": "2020-10-31","target_13_m3": 0.061,"target_1_date": "2020-11-25","target_1_m3": 0.061,"target_2_date": "2020-11-30","target_2_m3": 0.061,"target_3_date": "2019-12-31","target_3_m3": 0.061,"target_4_date": "2020-01-31","target_4_m3": 0.061,"target_5_date": "2020-02-29","target_5_m3": 0.061,"target_6_date": "2020-03-31","target_6_m3": 0.061,"target_7_date": "2020-04-30","target_7_m3": 0.061,"target_8_date": "2020-05-31","target_8_m3": 0.061,"target_9_date": "2020-06-30","target_9_m3": 0.061,"target_date": "2020-11-25","target_m3": 0.061,"timestamp": "1111-11-11T11:11:11Z","total_m3": 0}'
fields = 'licznik;16466583;0;1111-11-11 11:11.11'
}
}
diff --git a/components/wmbus_common/drivers/src/zenner0b.xmq b/components/wmbus_common/drivers/src/zenner0b.xmq
new file mode 100644
index 00000000..30d5e4c2
--- /dev/null
+++ b/components/wmbus_common/drivers/src/zenner0b.xmq
@@ -0,0 +1,97 @@
+// Copyright (C) 2025 Fredrik Öhrström (gpl-3.0-or-later)
+driver {
+ name = zenner0b
+ meter_type = WaterMeter
+ default_fields = name,id,status,total_m3,target_m3,timestamp
+ detect {
+ mvt = ZRI,0B,16
+ }
+ fields {
+ field {
+ name = mfct_specific_data
+ quantity = Text
+ attributes = HIDE
+ match_entire_payload = true
+ info = 'Decode manufacturer data after DIF 0x0F.'
+ ixml = "decode = -'0F', status, target, total, -byte?.
+ status = byte, byte, byte, byte, @DV_STATUS.
+ target = byte, byte, byte, byte, @DV_TARGET.
+ total = byte, byte, byte, byte, @DV_TOTAL.
+ -hex = ['A'-'F'; '0'-'9'].
+ -byte = hex, hex.
+ DV_STATUS>dvk = +'04FF0B'.
+ DV_TARGET>dvk = +'04FF0C'.
+ DV_TOTAL>dvk = +'04FF0D'."
+ }
+ field {
+ name = status
+ quantity = Text
+ attributes = STATUS
+ info = 'Meter status flags (bytes 0-3 from manufacturer data).'
+ default_message = OK
+ match {
+ difvifkey = 04FF0B
+ }
+ lookup {
+ name = STATUS
+ map_type = IndexToString
+ mask_bits = 0xffffffff
+ map {
+ name = OK
+ info = OK
+ value = 0x00000
+ test = Set
+ }
+ }
+ }
+ field {
+ name = target_raw
+ quantity = Dimensionless
+ attributes = HIDE
+ info = 'Raw current month water consumption in units of 1/256000 m3.'
+ vif_scaling = None
+ display_unit = counter
+ match {
+ difvifkey = 04FF0C
+ }
+ }
+ field {
+ name = target
+ quantity = Volume
+ info = 'The current month water consumption.'
+ calculate = 'target_raw_counter / 256000counter * 1m3'
+ }
+ field {
+ name = total_raw
+ quantity = Dimensionless
+ attributes = HIDE
+ info = 'Raw total water consumption in units of 1/256000 m3.'
+ vif_scaling = None
+ display_unit = counter
+ match {
+ difvifkey = 04FF0D
+ }
+ }
+ field {
+ name = total
+ quantity = Volume
+ change = Increasing
+ info = 'The total water consumption recorded by this meter.'
+ calculate = 'total_raw_counter / 256000counter * 1m3'
+ }
+ }
+ tests {
+ test {
+ args = 'TestWater zenner0b 50087367 NOKEY'
+ telegram = 1E44496A677308500B167AD80010252F2F0F0000000080BF1B0000A6420000
+ json = '{"_":"telegram","id":"50087367","media":"cold water","driver":"zenner0b","name":"TestWater","status":"OK","target_m3":7.1035,"timestamp":"1111-11-11T11:11:11Z","total_m3":17.062}'
+ fields = 'TestWater;50087367;OK;17.062;7.1035;1111-11-11 11:11.11'
+ }
+ test {
+ args = 'StatusWater zenner0b 50087367 NOKEY'
+ telegram = 1E44496A677308500B167AD80010252F2F0F80BF1B0080BF1B0000A6420000
+ json = '{"_":"telegram","id":"50087367","media":"cold water","driver":"zenner0b","name":"StatusWater","status":"STATUS_1BBF80","target_m3":7.1035,"timestamp":"1111-11-11T11:11:11Z","total_m3":17.062}'
+ fields = 'StatusWater;50087367;STATUS_1BBF80;17.062;7.1035;1111-11-11 11:11.11'
+ }
+ }
+}
diff --git a/components/wmbus_common/dvparser.cpp b/components/wmbus_common/dvparser.cpp
deleted file mode 100644
index fbf30474..00000000
--- a/components/wmbus_common/dvparser.cpp
+++ /dev/null
@@ -1,1541 +0,0 @@
-/*
- Copyright (C) 2018-2024 Fredrik Öhrström (gpl-3.0-or-later)
-
- 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 .
-*/
-
-#include"dvparser.h"
-#include"wmbus.h"
-#include"util.h"
-
-#include
-#include
-#include
-#include
-#include
-
-// The parser should not crash on invalid data, but yeah, when I
-// need to debug it because it crashes on invalid data, then
-// I enable the following define...
-//#define DEBUG_PARSER(...) fprintf(stdout, __VA_ARGS__)
-#define DEBUG_PARSER(...)
-
-union RealConversion
-{
- uint32_t i;
- float f;
-};
-
-const char *toString(VIFRange v)
-{
- switch (v) {
- case VIFRange::None: return "None";
- case VIFRange::Any: return "Any";
-#define X(name,from,to,quantity,unit) case VIFRange::name: return #name;
-LIST_OF_VIF_RANGES
-#undef X
- }
- assert(0);
-}
-
-VIFRange toVIFRange(const char *s)
-{
- if (!strcmp(s, "None")) return VIFRange::None;
- if (!strcmp(s, "Any")) return VIFRange::Any;
-#define X(name,from,to,quantity,unit) if (!strcmp(s, #name)) return VIFRange::name;
-LIST_OF_VIF_RANGES
-#undef X
-
- return VIFRange::None;
-}
-
-VIFCombinable toVIFCombinable(const char *s)
-{
- if (!strcmp(s, "None")) return VIFCombinable::None;
- if (!strcmp(s, "Any")) return VIFCombinable::Any;
-#define X(name,from,to) if (!strcmp(s, #name)) return VIFCombinable::name;
-LIST_OF_VIF_COMBINABLES
-#undef X
-
- return VIFCombinable::None;
-}
-
-const char *toString(VIFCombinable v)
-{
- switch (v) {
- case VIFCombinable::None: return "None";
- case VIFCombinable::Any: return "Any";
-#define X(name,from,to) case VIFCombinable::name: return #name;
-LIST_OF_VIF_COMBINABLES
-#undef X
- }
- assert(0);
-}
-
-VIFCombinable toVIFCombinable(int i)
-{
-#define X(name,from,to) if (from <= i && i <= to) return VIFCombinable::name;
-LIST_OF_VIF_COMBINABLES
-#undef X
- return VIFCombinable::None;
-}
-
-Unit toDefaultUnit(Vif v)
-{
-#define X(name,from,to,quantity,unit) { if (from <= v.intValue() && v.intValue() <= to) return unit; }
-LIST_OF_VIF_RANGES
-#undef X
- return Unit::Unknown;
-}
-
-Unit toDefaultUnit(VIFRange v)
-{
- switch (v) {
- case VIFRange::Any:
- case VIFRange::None:
- assert(0);
- break;
-#define X(name,from,to,quantity,unit) case VIFRange::name: return unit;
-LIST_OF_VIF_RANGES
-#undef X
- }
- assert(0);
-}
-
-VIFRange toVIFRange(int i)
-{
-#define X(name,from,to,quantity,unit) if (from <= i && i <= to) return VIFRange::name;
-LIST_OF_VIF_RANGES
-#undef X
- return VIFRange::None;
-}
-
-bool isInsideVIFRange(Vif vif, VIFRange vif_range)
-{
- if (vif_range == VIFRange::AnyVolumeVIF)
- {
- // There are more volume units in the standard that will be added here.
- return isInsideVIFRange(vif, VIFRange::Volume);
- }
- if (vif_range == VIFRange::AnyEnergyVIF)
- {
- return
- isInsideVIFRange(vif, VIFRange::EnergyWh) ||
- isInsideVIFRange(vif, VIFRange::EnergyMJ) ||
- isInsideVIFRange(vif, VIFRange::EnergyMWh) ||
- isInsideVIFRange(vif, VIFRange::EnergyGJ);
- }
- if (vif_range == VIFRange::AnyPowerVIF)
- {
- // There are more power units in the standard that will be added here.
- return isInsideVIFRange(vif, VIFRange::PowerW) ||
- isInsideVIFRange(vif, VIFRange::PowerJh);
- }
-
-#define X(name,from,to,quantity,unit) if (VIFRange::name == vif_range) { return from <= vif.intValue() && vif.intValue() <= to; }
-LIST_OF_VIF_RANGES
-#undef X
- return false;
-}
-
-std::map hash_to_format_;
-
-bool loadFormatBytesFromSignature(uint16_t format_signature, std::vector *format_bytes)
-{
- if (hash_to_format_.count(format_signature) > 0) {
- debug("(dvparser) found remembered format for hash %x\n", format_signature);
- // Return the proper hash!
- hex2bin(hash_to_format_[format_signature], format_bytes);
- return true;
- }
- // Unknown format signature.
- return false;
-}
-
-bool parseDV(Telegram *t,
- std::vector &databytes,
- std::vector::iterator data,
- size_t data_len,
- std::map> *dv_entries,
- std::vector::iterator *format,
- size_t format_len,
- uint16_t *format_hash)
-{
- std::map dv_count;
- std::vector format_bytes;
- std::vector id_bytes;
- std::vector