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 -![ESPHome](https://img.shields.io/badge/dynamic/yaml?url=https%3A%2F%2Fraw.githubusercontent.com%2FIoTLabs-pl%2FESPHome-Components%2Frefs%2Fheads%2Fmaster%2F.github%2Fworkflows%2Fbuild.yaml&query=%24.jobs.build.steps...with.version&label=ESPHome&style=for-the-badge&color=f3922d&labelColor=2b4c5a) +![ESPHome](https://img.shields.io/badge/dynamic/yaml?url=https%3A%2F%2Fraw.githubusercontent.com%2FIoTLabs-pl%2FESPHome-Components%2Frefs%2Fheads%2Fmaster%2F.github%2Fworkflows%2Fbuild.yaml&query=%24.env.ESPHOME_VERSION&label=ESPHome&style=for-the-badge&color=f3922d&labelColor=2b4c5a) ![GitHub License](https://img.shields.io/github/license/IoTLabs-pl/esphome-components?style=for-the-badge&color=f3922d&labelColor=2b4c5a) 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 data_bytes; - std::string dv, key; - size_t start_parse_here = t->parsed.size(); - std::vector::iterator data_start = data; - std::vector::iterator data_end = data+data_len; - std::vector::iterator format_end; - bool data_has_difvifs = true; - bool variable_length = false; - int force_mfct_index = t->force_mfct_index; - - if (format == NULL) { - // No format std::string was supplied, we therefore assume - // that the difvifs necessary to parse the data is - // part of the data! This is the default. - format = &data; - format_end = data_end; - } else { - // A format std::string has been supplied. The data is compressed, - // and can only be decoded using the supplied difvifs. - // Since the data does not have the difvifs. - data_has_difvifs = false; - format_end = *format+format_len; - std::string s = bin2hex(*format, format_end, format_len); - debug("(dvparser) using format \"%s\"\n", s.c_str()); - } - - dv_entries->clear(); - - // Data format is: - - // DIF byte (defines how the binary data bits should be decoded and howy man data bytes there are) - // Sometimes followed by one or more dife bytes, if the 0x80 high bit is set. - // The last dife byte does not have the 0x80 bit set. - - // VIF byte (defines what the decoded value means, water,energy,power,etc.) - // Sometimes followed by one or more vife bytes, if the 0x80 high bit is set. - // The last vife byte does not have the 0x80 bit set. - - // Data bytes, the number of data bytes are defined by the dif format. - // Or if the dif says variable length, then the first data byte specifies the number of data bytes. - - // DIF again... - - // A Dif(Difes)Vif(Vifes) identifier can be for example be the 02FF20 for the Multical21 - // vendor specific status bits. The parser then uses this identifier as a key to store the - // data bytes in a map. The same identifier could occur several times in a telegram, - // even though it often don't. Since the first occurence is stored under 02FF20, - // the second identical identifier stores its data under the key "02FF20_2" etc for 3 and forth... - // A proper meter would use storagenr etc to differentiate between different measurements of - // the same value. - - format_bytes.clear(); - id_bytes.clear(); - for (;;) - { - id_bytes.clear(); - DEBUG_PARSER("(dvparser debug) Remaining format data %ju\n", std::distance(*format,format_end)); - if (*format == format_end) break; - - if (force_mfct_index != -1) - { - // This is an old meter without a proper 0f or other hear start manufacturer data marker. - int index = std::distance(data_start, data); - - if (index >= force_mfct_index) - { - DEBUG_PARSER("(dvparser) manufacturer specific data, parsing is done.\n", dif); - size_t datalen = std::distance(data, data_end); - std::string value = bin2hex(data, data_end, datalen); - t->addExplanationAndIncrementPos(data, datalen, KindOfData::CONTENT, Understanding::NONE, "manufacturer specific data %s", value.c_str()); - break; - } - } - - uchar dif = **format; - - MeasurementType mt = difMeasurementType(dif); - int datalen = difLenBytes(dif); - DEBUG_PARSER("(dvparser debug) dif=%02x datalen=%d \"%s\" type=%s\n", dif, datalen, difType(dif).c_str(), - measurementTypeName(mt).c_str()); - - if (datalen == -2) - { - if (dif == 0x0f) - { - DEBUG_PARSER("(dvparser) reached dif %02x manufacturer specific data, parsing is done.\n", dif); - datalen = std::distance(data,data_end); - std::string value = bin2hex(data+1, data_end, datalen-1); - t->mfct_0f_index = 1+std::distance(data_start, data); - assert(t->mfct_0f_index >= 0); - t->addExplanationAndIncrementPos(data, datalen, KindOfData::CONTENT, Understanding::NONE, "%02X manufacturer specific data %s", dif, value.c_str()); - break; - } - if (dif == 0x1f) - { - DEBUG_PARSER("(dvparser) reached dif %02x more records in next telegram.\n", dif); - datalen = std::distance(data,data_end); - std::string value = bin2hex(data+1, data_end, datalen-1); - t->mfct_0f_index = 1+std::distance(data_start, data); - t->mfct_1f_index = 1+std::distance(data_start, data); - assert(t->mfct_0f_index >= 0); - t->addExplanationAndIncrementPos(data, datalen, KindOfData::CONTENT, Understanding::FULL, "%02X more data in next telegram %s", dif, value.c_str()); - break; - } - DEBUG_PARSER("(dvparser) reached unknown dif %02x treating remaining data as manufacturer specific, parsing is done.\n", dif); - datalen = std::distance(data,data_end); - std::string value = bin2hex(data+1, data_end, datalen-1); - t->mfct_0f_index = 1+std::distance(data_start, data); - assert(t->mfct_0f_index >= 0); - t->addExplanationAndIncrementPos(data, datalen, KindOfData::CONTENT, Understanding::NONE, "%02X unknown dif treating remaining data as mfct specific %s", dif, value.c_str()); - break; - } - if (dif == 0x2f) { - t->addExplanationAndIncrementPos(*format, 1, KindOfData::PROTOCOL, Understanding::FULL, "%02X skip", dif); - DEBUG_PARSER("\n"); - continue; - } - if (datalen == -1) { - variable_length = true; - } else { - variable_length = false; - } - if (data_has_difvifs) { - format_bytes.push_back(dif); - id_bytes.push_back(dif); - t->addExplanationAndIncrementPos(*format, 1, KindOfData::PROTOCOL, Understanding::FULL, "%02X dif (%s)", dif, difType(dif).c_str()); - } else { - id_bytes.push_back(**format); - (*format)++; - } - - - int difenr = 0; - int subunit = 0; - int tariff = 0; - int lsb_of_storage_nr = (dif & 0x40) >> 6; - int storage_nr = lsb_of_storage_nr; - - bool has_another_dife = (dif & 0x80) == 0x80; - int num_dife = 0; - - while (has_another_dife) - { - num_dife++; - if (num_dife > 10) { debug("(dvparser) warning: too many dife found!\n"); break; } - if (*format == format_end) { debug("(dvparser) warning: unexpected end of data (dife expected)\n"); break; } - - uchar dife = **format; - int subunit_bit = (dife & 0x40) >> 6; - subunit |= subunit_bit << difenr; - int tariff_bits = (dife & 0x30) >> 4; - tariff |= tariff_bits << (difenr*2); - int storage_nr_bits = (dife & 0x0f); - storage_nr |= storage_nr_bits << (1+difenr*4); - - DEBUG_PARSER("(dvparser debug) dife=%02x (subunit=%d tariff=%d storagenr=%d)\n", dife, subunit, tariff, storage_nr); - - if (data_has_difvifs) - { - format_bytes.push_back(dife); - id_bytes.push_back(dife); - t->addExplanationAndIncrementPos(*format, 1, KindOfData::PROTOCOL, Understanding::FULL, - "%02X dife (subunit=%d tariff=%d storagenr=%d)", - dife, subunit, tariff, storage_nr); - } - else - { - id_bytes.push_back(**format); - (*format)++; - } - - has_another_dife = (dife & 0x80) == 0x80; - difenr++; - } - - if (*format == format_end) { debug("(dvparser) warning: unexpected end of data (vif expected)\n"); break; } - - uchar vif = **format; - int full_vif = vif & 0x7f; - bool extension_vif = false; - int combinable_full_vif = 0; - bool combinable_extension_vif = false; - std::set found_combinable_vifs; - std::set found_combinable_vifs_raw; - - DEBUG_PARSER("(dvparser debug) vif=%04x \"%s\"\n", vif, vifType(vif).c_str()); - - if (data_has_difvifs) - { - format_bytes.push_back(vif); - id_bytes.push_back(vif); - t->addExplanationAndIncrementPos(*format, 1, KindOfData::PROTOCOL, Understanding::FULL, - "%02X vif (%s)", vif, vifType(vif).c_str()); - } else - { - id_bytes.push_back(**format); - (*format)++; - } - - // Check if this is marker for one of the extended sets of vifs: first, second and third or manufacturer. - if (vif == 0xfb || vif == 0xfd || vif == 0xef || vif == 0xff) - { - // Extension vifs. - full_vif <<= 8; - extension_vif = true; - } - - // Grabbing a variable length vif. This does not currently work - // with the compact format. Should the high bit be set here? - // One meter (qsmoke) does not set the high bit, since the next byte is not strictly a vife, it is a length byte. - // Another meter does set the hight bit.... - // We accept both hear, I do not yet know what the standard says. - if (vif == 0x7c || vif == 0xfc) - { - DEBUG_PARSER("(dvparser debug) variable length vif found\n"); - if (*format == format_end) { debug("(dvparser) warning: unexpected end of data (vif varlen expected)\n"); break; } - uchar viflen = **format; - id_bytes.push_back(viflen); - t->addExplanationAndIncrementPos(*format, 1, KindOfData::PROTOCOL, Understanding::FULL, - "%02X viflen (%d)", viflen, viflen); - for (uchar i = 0; i < viflen; ++i) - { - if (*format == format_end) { debug("(dvparser) warning: unexpected end of data (vif varlen byte %d/%d expected)\n", - i+1, viflen); break; } - uchar v = **format; - t->addExplanationAndIncrementPos(*format, 1, KindOfData::PROTOCOL, Understanding::FULL, - "%02X vif (%c)", v, v); - id_bytes.push_back(v); - } - } - - // Do we have another vife byte? We better have one, if extension_vif is true. - bool has_another_vife = (vif & 0x80) == 0x80; - int num_vife = 0; - - while (has_another_vife) - { - num_vife++; - if (num_vife > 10) { debug("(dvparser) warning: too many vife found!\n"); break; } - - if (*format == format_end) { debug("(dvparser) warning: unexpected end of data (vife expected)\n"); break; } - - uchar vife = **format; - DEBUG_PARSER("(dvparser debug) vife=%02x (%s)\n", vife, vifeType(dif, vif, vife).c_str()); - - if (data_has_difvifs) - { - // Collect the difvifs to create signature for future use. - format_bytes.push_back(vife); - id_bytes.push_back(vife); - } - else - { - // Reuse the existing - id_bytes.push_back(**format); - (*format)++; - } - - has_another_vife = (vife & 0x80) == 0x80; - - if (extension_vif) - { - // First vife after the extension marker is the real vif. - full_vif |= (vife & 0x7f); - extension_vif = false; - if (data_has_difvifs) - { - t->addExplanationAndIncrementPos(*format, 1, KindOfData::PROTOCOL, Understanding::FULL, - "%02X vife (%s)", vife, vifeType(dif, vif, vife).c_str()); - } - } - else - { - if (combinable_extension_vif) - { - // First vife after the combinable extension marker is the real vif. - combinable_full_vif |= (vife & 0x7f); - combinable_extension_vif = false; - VIFCombinable vc = toVIFCombinable(combinable_full_vif); - found_combinable_vifs.insert(vc); - found_combinable_vifs_raw.insert(combinable_full_vif); - - if (data_has_difvifs) - { - t->addExplanationAndIncrementPos(*format, 1, KindOfData::PROTOCOL, Understanding::FULL, - "%02X combinable extension vife (%s)", vife, toString(vc)); - } - } - else - { - combinable_full_vif = vife & 0x7f; - // Check if this is marker for one of the extended combinable vifs - if (combinable_full_vif == 0x7c || combinable_full_vif == 0x7f) - { - combinable_full_vif <<= 8; - combinable_extension_vif = true; - VIFCombinable vc = toVIFCombinable(vife & 0x7f); - if (data_has_difvifs) - { - t->addExplanationAndIncrementPos(*format, 1, KindOfData::PROTOCOL, Understanding::FULL, - "%02X combinable vif (%s)", vife, toString(vc)); - } - } - else - { - VIFCombinable vc = toVIFCombinable(combinable_full_vif); - found_combinable_vifs.insert(vc); - found_combinable_vifs_raw.insert(combinable_full_vif); - - if (data_has_difvifs) - { - t->addExplanationAndIncrementPos(*format, 1, KindOfData::PROTOCOL, Understanding::FULL, - "%02X combinable vif (%s)", vife, toString(vc)); - } - } - } - } - } - - dv = ""; - for (uchar c : id_bytes) { - char hex[3]; - hex[2] = 0; - snprintf(hex, 3, "%02X", c); - dv.append(hex); - } - DEBUG_PARSER("(dvparser debug) key \"%s\"\n", dv.c_str()); - - int count = ++dv_count[dv]; - if (count > 1) { - strprintf(&key, "%s_%d", dv.c_str(), count); - } else { - strprintf(&key, "%s", dv.c_str()); - } - DEBUG_PARSER("(dvparser debug) DifVif key is %s\n", key.c_str()); - - int remaining = std::distance(data, data_end); - if (remaining < 1) - { - debug("(dvparser) warning: unexpected end of data\n"); - break; - } - - if (variable_length) { - DEBUG_PARSER("(dvparser debug) varlen %02x\n", *(data+0)); - datalen = *(data); - t->addExplanationAndIncrementPos(data, 1, KindOfData::PROTOCOL, Understanding::FULL, "%02X varlen=%d", *(data+0), datalen); - remaining--; // Drop the length byte. - } - DEBUG_PARSER("(dvparser debug) remaining data %d len=%d\n", remaining, datalen); - if (remaining < datalen) - { - debug("(dvparser) warning: unexpected end of data\n"); - datalen = remaining-1; - } - - std::string value = bin2hex(data, data_end, datalen); - int offset = start_parse_here+data-data_start; - - (*dv_entries)[key] = { offset, DVEntry(offset, - key, - mt, - Vif(full_vif), - found_combinable_vifs, - found_combinable_vifs_raw, - StorageNr(storage_nr), - TariffNr(tariff), - SubUnitNr(subunit), - value) }; - - DVEntry *dve = &(*dv_entries)[key].second; - - trace("[DVPARSER] entry %s\n", dve->str().c_str()); - - assert(key == dve->dif_vif_key.str()); - - if (value.length() > 0) { - // This call increments data with datalen. - t->addExplanationAndIncrementPos(data, datalen, KindOfData::CONTENT, Understanding::NONE, "%s", value.c_str()); - DEBUG_PARSER("(dvparser debug) data \"%s\"\n\n", value.c_str()); - } - if (remaining == datalen || data == databytes.end()) { - // We are done here! - break; - } - } - - std::string format_string = bin2hex(format_bytes); - uint16_t hash = crc16_EN13757(safeButUnsafeVectorPtr(format_bytes), format_bytes.size()); - - if (data_has_difvifs) { - if (hash_to_format_.count(hash) == 0) { - hash_to_format_[hash] = format_string; - debug("(dvparser) found new format \"%s\" with hash %x, remembering!\n", format_string.c_str(), hash); - } - } - - return true; -} - -bool hasKey(std::map> *dv_entries, std::string key) -{ - return dv_entries->count(key) > 0; -} - -bool findKey(MeasurementType mit, VIFRange vif_range, StorageNr storagenr, TariffNr tariffnr, - std::string *key, std::map> *dv_entries) -{ - return findKeyWithNr(mit, vif_range, storagenr, tariffnr, 1, key, dv_entries); -} - -bool findKeyWithNr(MeasurementType mit, VIFRange vif_range, StorageNr storagenr, TariffNr tariffnr, int nr, - std::string *key, std::map> *dv_entries) -{ - /*debug("(dvparser) looking for type=%s vifrange=%s storagenr=%d tariffnr=%d\n", - measurementTypeName(mit).c_str(), toString(vif_range), storagenr.intValue(), tariffnr.intValue());*/ - - for (auto& v : *dv_entries) - { - MeasurementType ty = v.second.second.measurement_type; - Vif vi = v.second.second.vif; - StorageNr sn = v.second.second.storage_nr; - TariffNr tn = v.second.second.tariff_nr; - - /* debug("(dvparser) match? %s type=%s vife=%x (%s) and storagenr=%d\n", - v.first.c_str(), - measurementTypeName(ty).c_str(), vi.intValue(), storagenr, sn);*/ - - if (isInsideVIFRange(vi, vif_range) && - (mit == MeasurementType::Instantaneous || mit == ty) && - (storagenr == AnyStorageNr || storagenr == sn) && - (tariffnr == AnyTariffNr || tariffnr == tn)) - { - *key = v.first; - nr--; - if (nr <= 0) return true; - debug("(dvparser) found key %s for type=%s vif=%x storagenr=%d\n", - v.first.c_str(), measurementTypeName(ty).c_str(), - vi.intValue(), storagenr.intValue()); - } - } - return false; -} - -void extractDV(DifVifKey &dvk, uchar *dif, int *vif, bool *has_difes, bool *has_vifes) -{ - std::string tmp = dvk.str(); - extractDV(tmp, dif, vif, has_difes, has_vifes); -} - -void extractDV(std::string &s, uchar *dif, int *vif, bool *has_difes, bool *has_vifes) -{ - std::vector bytes; - hex2bin(s, &bytes); - size_t i = 0; - *has_difes = false; - *has_vifes = false; - if (bytes.size() == 0) - { - *dif = 0; - *vif = 0; - return; - } - - *dif = bytes[i]; - while (i < bytes.size() && (bytes[i] & 0x80)) - { - i++; - *has_difes = true; - } - i++; - - if (i >= bytes.size()) - { - *vif = 0; - return; - } - - *vif = bytes[i]; - if (*vif == 0xfb || // first extension - *vif == 0xfd || // second extensio - *vif == 0xef || // third extension - *vif == 0xff) // vendor extension - { - if (i+1 < bytes.size()) - { - // Create an extended vif, like 0xfd31 for example. - *vif = bytes[i] << 8 | bytes[i+1]; - i++; - } - } - - while (i < bytes.size() && (bytes[i] & 0x80)) - { - i++; - *has_vifes = true; - } -} - -bool extractDVuint8(std::map> *dv_entries, - std::string key, - int *offset, - uchar *value) -{ - if ((*dv_entries).count(key) == 0) { - verbose("(dvparser) warning: cannot extract uint8 from non-existant key \"%s\"\n", key.c_str()); - *offset = -1; - *value = 0; - return false; - } - - std::pair& p = (*dv_entries)[key]; - *offset = p.first; - std::vector v; - hex2bin(p.second.value, &v); - - *value = v[0]; - return true; -} - -bool extractDVuint16(std::map> *dv_entries, - std::string key, - int *offset, - uint16_t *value) -{ - if ((*dv_entries).count(key) == 0) { - verbose("(dvparser) warning: cannot extract uint16 from non-existant key \"%s\"\n", key.c_str()); - *offset = -1; - *value = 0; - return false; - } - - std::pair& p = (*dv_entries)[key]; - *offset = p.first; - std::vector v; - hex2bin(p.second.value, &v); - - *value = v[1]<<8 | v[0]; - return true; -} - -bool extractDVuint24(std::map> *dv_entries, - std::string key, - int *offset, - uint32_t *value) -{ - if ((*dv_entries).count(key) == 0) { - verbose("(dvparser) warning: cannot extract uint24 from non-existant key \"%s\"\n", key.c_str()); - *offset = -1; - *value = 0; - return false; - } - - std::pair& p = (*dv_entries)[key]; - *offset = p.first; - std::vector v; - hex2bin(p.second.value, &v); - - *value = v[2] << 16 | v[1]<<8 | v[0]; - return true; -} - -bool extractDVuint32(std::map> *dv_entries, - std::string key, - int *offset, - uint32_t *value) -{ - if ((*dv_entries).count(key) == 0) { - verbose("(dvparser) warning: cannot extract uint32 from non-existant key \"%s\"\n", key.c_str()); - *offset = -1; - *value = 0; - return false; - } - - std::pair& p = (*dv_entries)[key]; - *offset = p.first; - std::vector v; - hex2bin(p.second.value, &v); - - *value = (uint32_t(v[3]) << 24) | (uint32_t(v[2]) << 16) | (uint32_t(v[1])<<8) | uint32_t(v[0]); - return true; -} - -bool extractDVdouble(std::map> *dv_entries, - std::string key, - int *offset, - double *value, - bool auto_scale, - bool force_unsigned) -{ - if ((*dv_entries).count(key) == 0) { - verbose("(dvparser) warning: cannot extract double from non-existant key \"%s\"\n", key.c_str()); - *offset = 0; - *value = 0; - return false; - } - std::pair& p = (*dv_entries)[key]; - *offset = p.first; - - if (p.second.value.length() == 0) { - verbose("(dvparser) warning: key found but no data \"%s\"\n", key.c_str()); - *offset = 0; - *value = 0; - return false; - } - - return p.second.extractDouble(value, auto_scale, force_unsigned); -} - -bool checkSizeHex(size_t expected_len, DifVifKey &dvk, std::string &v) -{ - if (v.length() == expected_len) return true; - - warning("(dvparser) bad decode since difvif %s expected %d hex chars but got \"%s\"\n", - dvk.str().c_str(), expected_len, v.c_str()); - return false; -} - -bool is_all_F(std::string &v) -{ - for (size_t i = 0; i < v.length(); ++i) - { - if (v[i] != 'F') return false; - } - return true; -} - -bool DVEntry::extractDouble(double *out, bool auto_scale, bool force_unsigned) -{ - int t = dif_vif_key.dif() & 0xf; - if (t == 0x0 || - t == 0x8 || - t == 0xd || - t == 0xf) - { - // Cannot extract from nothing, selection for readout, variable length or special. - // Variable length is used for compact varlen history. Should be added in the future. - return false; - } - else - if (t == 0x1 || // 8 Bit Integer/Binary - t == 0x2 || // 16 Bit Integer/Binary - t == 0x3 || // 24 Bit Integer/Binary - t == 0x4 || // 32 Bit Integer/Binary - t == 0x6 || // 48 Bit Integer/Binary - t == 0x7) // 64 Bit Integer/Binary - { - std::vector v; - hex2bin(value, &v); - uint64_t raw = 0; - bool negate = false; - uint64_t negate_mask = 0; - if (t == 0x1) { - if (!checkSizeHex(2, dif_vif_key, value)) return false; - assert(v.size() == 1); - raw = v[0]; - if (!force_unsigned && (raw & (uint64_t)0x80UL) != 0) { negate = true; negate_mask = ~((uint64_t)0)<<8; } - } else if (t == 0x2) { - if (!checkSizeHex(4, dif_vif_key, value)) return false; - assert(v.size() == 2); - raw = v[1]*256 + v[0]; - if (!force_unsigned && (raw & (uint64_t)0x8000UL) != 0) { negate = true; negate_mask = ~((uint64_t)0)<<16; } - } else if (t == 0x3) { - if (!checkSizeHex(6, dif_vif_key, value)) return false; - assert(v.size() == 3); - raw = v[2]*256*256 + v[1]*256 + v[0]; - if (!force_unsigned && (raw & (uint64_t)0x800000UL) != 0) { negate = true; negate_mask = ~((uint64_t)0)<<24; } - } else if (t == 0x4) { - if (!checkSizeHex(8, dif_vif_key, value)) return false; - assert(v.size() == 4); - raw = ((unsigned int)v[3])*256*256*256 - + ((unsigned int)v[2])*256*256 - + ((unsigned int)v[1])*256 - + ((unsigned int)v[0]); - if (!force_unsigned && (raw & (uint64_t)0x80000000UL) != 0) { negate = true; negate_mask = ~((uint64_t)0)<<32; } - } else if (t == 0x6) { - if (!checkSizeHex(12, dif_vif_key, value)) return false; - assert(v.size() == 6); - raw = ((uint64_t)v[5])*256*256*256*256*256 - + ((uint64_t)v[4])*256*256*256*256 - + ((uint64_t)v[3])*256*256*256 - + ((uint64_t)v[2])*256*256 - + ((uint64_t)v[1])*256 - + ((uint64_t)v[0]); - if (!force_unsigned && (raw & (uint64_t)0x800000000000UL) != 0) { negate = true; negate_mask = ~((uint64_t)0)<<48; } - } else if (t == 0x7) { - if (!checkSizeHex(16, dif_vif_key, value)) return false; - assert(v.size() == 8); - raw = ((uint64_t)v[7])*256*256*256*256*256*256*256 - + ((uint64_t)v[6])*256*256*256*256*256*256 - + ((uint64_t)v[5])*256*256*256*256*256 - + ((uint64_t)v[4])*256*256*256*256 - + ((uint64_t)v[3])*256*256*256 - + ((uint64_t)v[2])*256*256 - + ((uint64_t)v[1])*256 - + ((uint64_t)v[0]); - if (!force_unsigned && (raw & (uint64_t)0x8000000000000000UL) != 0) { negate = true; negate_mask = 0; } - } - double scale = 1.0; - double draw = (double)raw; - if (negate) - { - draw = (double)((int64_t)(negate_mask | raw)); - } - if (auto_scale) scale = vifScale(dif_vif_key.vif()); - *out = (draw) / scale; - } - else - if (t == 0x9 || // 2 digit BCD - t == 0xA || // 4 digit BCD - t == 0xB || // 6 digit BCD - t == 0xC || // 8 digit BCD - t == 0xE) // 12 digit BCD - { - // Negative BCD values are always visible in bcd. I.e. they are always signed. - // Ignore assumption on signedness. - // 74140000 -> 00001474 - std::string& v = value; - uint64_t raw = 0; - bool negate = false; - - if (is_all_F(v)) - { - *out = std::nan(""); - return false; - } - if (t == 0x9) { - if (!checkSizeHex(2, dif_vif_key, v)) return false; - if (v[0] == 'F') { negate = true; v[0] = '0'; } - raw = (v[0]-'0')*10 + (v[1]-'0'); - } else if (t == 0xA) { - if (!checkSizeHex(4, dif_vif_key, v)) return false; - if (v[2] == 'F') { negate = true; v[2] = '0'; } - raw = (v[2]-'0')*10*10*10 + (v[3]-'0')*10*10 - + (v[0]-'0')*10 + (v[1]-'0'); - } else if (t == 0xB) { - if (!checkSizeHex(6, dif_vif_key, v)) return false; - if (v[4] == 'F') { negate = true; v[4] = '0'; } - raw = (v[4]-'0')*10*10*10*10*10 + (v[5]-'0')*10*10*10*10 - + (v[2]-'0')*10*10*10 + (v[3]-'0')*10*10 - + (v[0]-'0')*10 + (v[1]-'0'); - } else if (t == 0xC) { - if (!checkSizeHex(8, dif_vif_key, v)) return false; - if (v[6] == 'F') { negate = true; v[6] = '0'; } - raw = (v[6]-'0')*10*10*10*10*10*10*10 + (v[7]-'0')*10*10*10*10*10*10 - + (v[4]-'0')*10*10*10*10*10 + (v[5]-'0')*10*10*10*10 - + (v[2]-'0')*10*10*10 + (v[3]-'0')*10*10 - + (v[0]-'0')*10 + (v[1]-'0'); - } else if (t == 0xE) { - if (!checkSizeHex(12, dif_vif_key, v)) return false; - if (v[10] == 'F') { negate = true; v[10] = '0'; } - raw =(v[10]-'0')*10*10*10*10*10*10*10*10*10*10*10 + (v[11]-'0')*10*10*10*10*10*10*10*10*10*10 - + (v[8]-'0')*10*10*10*10*10*10*10*10*10 + (v[9]-'0')*10*10*10*10*10*10*10*10 - + (v[6]-'0')*10*10*10*10*10*10*10 + (v[7]-'0')*10*10*10*10*10*10 - + (v[4]-'0')*10*10*10*10*10 + (v[5]-'0')*10*10*10*10 - + (v[2]-'0')*10*10*10 + (v[3]-'0')*10*10 - + (v[0]-'0')*10 + (v[1]-'0'); - } - double scale = 1.0; - double draw = (double)raw; - if (negate) - { - draw = (double)draw * -1; - } - if (auto_scale) scale = vifScale(dif_vif_key.vif()); - *out = (draw) / scale; - } - else - if (t == 0x5) // 32 Bit Real - { - std::vector v; - hex2bin(value, &v); - if (!checkSizeHex(8, dif_vif_key, value)) return false; - assert(v.size() == 4); - RealConversion rc; - rc.i = v[3]<<24 | v[2]<<16 | v[1]<<8 | v[0]; - - // Assumes float uses the standard IEEE 754 bit set. - // 1 bit sign, 8 bit exp, 23 bit mantissa - // RealConversion is tested on an amd64 platform. How about - // other platsforms with different byte ordering? - double draw = rc.f; - double scale = 1.0; - if (auto_scale) scale = vifScale(dif_vif_key.vif()); - *out = (draw) / scale; - } - else - { - warning("(dvparser) Unsupported dif format for extraction to double! dif=%02x\n", dif_vif_key.dif()); - return false; - } - - return true; -} - -bool extractDVlong(std::map> *dv_entries, - std::string key, - int *offset, - uint64_t *out) -{ - if ((*dv_entries).count(key) == 0) { - verbose("(dvparser) warning: cannot extract long from non-existant key \"%s\"\n", key.c_str()); - *offset = 0; - *out = 0; - return false; - } - - std::pair& p = (*dv_entries)[key]; - *offset = p.first; - - if (p.second.value.length() == 0) { - verbose("(dvparser) warning: key found but no data \"%s\"\n", key.c_str()); - *offset = 0; - *out = 0; - return false; - } - - return p.second.extractLong(out); -} - -bool DVEntry::extractLong(uint64_t *out) -{ - int t = dif_vif_key.dif() & 0xf; - if (t == 0x1 || // 8 Bit Integer/Binary - t == 0x2 || // 16 Bit Integer/Binary - t == 0x3 || // 24 Bit Integer/Binary - t == 0x4 || // 32 Bit Integer/Binary - t == 0x6 || // 48 Bit Integer/Binary - t == 0x7) // 64 Bit Integer/Binary - { - std::vector v; - hex2bin(value, &v); - uint64_t raw = 0; - if (t == 0x1) { - if (!checkSizeHex(2, dif_vif_key, value)) return false; - assert(v.size() == 1); - raw = v[0]; - } else if (t == 0x2) { - if (!checkSizeHex(4, dif_vif_key, value)) return false; - assert(v.size() == 2); - raw = v[1]*256 + v[0]; - } else if (t == 0x3) { - if (!checkSizeHex(6, dif_vif_key, value)) return false; - assert(v.size() == 3); - raw = v[2]*256*256 + v[1]*256 + v[0]; - } else if (t == 0x4) { - if (!checkSizeHex(8, dif_vif_key, value)) return false; - assert(v.size() == 4); - raw = ((unsigned int)v[3])*256*256*256 - + ((unsigned int)v[2])*256*256 - + ((unsigned int)v[1])*256 - + ((unsigned int)v[0]); - } else if (t == 0x6) { - if (!checkSizeHex(12, dif_vif_key, value)) return false; - assert(v.size() == 6); - raw = ((uint64_t)v[5])*256*256*256*256*256 - + ((uint64_t)v[4])*256*256*256*256 - + ((uint64_t)v[3])*256*256*256 - + ((uint64_t)v[2])*256*256 - + ((uint64_t)v[1])*256 - + ((uint64_t)v[0]); - } else if (t == 0x7) { - if (!checkSizeHex(16, dif_vif_key, value)) return false; - assert(v.size() == 8); - raw = ((uint64_t)v[7])*256*256*256*256*256*256*256 - + ((uint64_t)v[6])*256*256*256*256*256*256 - + ((uint64_t)v[5])*256*256*256*256*256 - + ((uint64_t)v[4])*256*256*256*256 - + ((uint64_t)v[3])*256*256*256 - + ((uint64_t)v[2])*256*256 - + ((uint64_t)v[1])*256 - + ((uint64_t)v[0]); - } - *out = raw; - } - else - if (t == 0x9 || // 2 digit BCD - t == 0xA || // 4 digit BCD - t == 0xB || // 6 digit BCD - t == 0xC || // 8 digit BCD - t == 0xE) // 12 digit BCD - { - // 74140000 -> 00001474 - std::string& v = value; - if (is_all_F(v)) - { - return false; - } - uint64_t raw = 0; - bool negate = false; - if (t == 0x9) { - if (!checkSizeHex(2, dif_vif_key, value)) return false; - if (v[0] == 'F') { negate = true; v[0] = '0'; } - assert(v.size() == 2); - raw = (v[0]-'0')*10 + (v[1]-'0'); - } else if (t == 0xA) { - if (!checkSizeHex(4, dif_vif_key, value)) return false; - if (v[2] == 'F') { negate = true; v[2] = '0'; } - assert(v.size() == 4); - raw = (v[2]-'0')*10*10*10 + (v[3]-'0')*10*10 - + (v[0]-'0')*10 + (v[1]-'0'); - } else if (t == 0xB) { - if (!checkSizeHex(6, dif_vif_key, value)) return false; - if (v[4] == 'F') { negate = true; v[4] = '0'; } - assert(v.size() == 6); - raw = (v[4]-'0')*10*10*10*10*10 + (v[5]-'0')*10*10*10*10 - + (v[2]-'0')*10*10*10 + (v[3]-'0')*10*10 - + (v[0]-'0')*10 + (v[1]-'0'); - } else if (t == 0xC) { - if (!checkSizeHex(8, dif_vif_key, value)) return false; - if (v[6] == 'F') { negate = true; v[6] = '0'; } - assert(v.size() == 8); - raw = (v[6]-'0')*10*10*10*10*10*10*10 + (v[7]-'0')*10*10*10*10*10*10 - + (v[4]-'0')*10*10*10*10*10 + (v[5]-'0')*10*10*10*10 - + (v[2]-'0')*10*10*10 + (v[3]-'0')*10*10 - + (v[0]-'0')*10 + (v[1]-'0'); - } else if (t == 0xE) { - if (!checkSizeHex(12, dif_vif_key, value)) return false; - if (v[10] == 'F') { negate = true; v[10] = '0'; } - assert(v.size() == 12); - raw =(v[10]-'0')*10*10*10*10*10*10*10*10*10*10*10 + (v[11]-'0')*10*10*10*10*10*10*10*10*10*10 - + (v[8]-'0')*10*10*10*10*10*10*10*10*10 + (v[9]-'0')*10*10*10*10*10*10*10*10 - + (v[6]-'0')*10*10*10*10*10*10*10 + (v[7]-'0')*10*10*10*10*10*10 - + (v[4]-'0')*10*10*10*10*10 + (v[5]-'0')*10*10*10*10 - + (v[2]-'0')*10*10*10 + (v[3]-'0')*10*10 - + (v[0]-'0')*10 + (v[1]-'0'); - } - - if (negate) - { - raw = (uint64_t)(((int64_t)raw)*-1); - } - - *out = raw; - } - else - { - error("Unsupported dif format for extraction to long! dif=%02x\n", dif_vif_key.dif()); - } - - return true; -} - -bool extractDVHexString(std::map> *dv_entries, - std::string key, - int *offset, - std::string *value) -{ - if ((*dv_entries).count(key) == 0) { - verbose("(dvparser) warning: cannot extract std::string from non-existant key \"%s\"\n", key.c_str()); - *offset = -1; - return false; - } - std::pair& p = (*dv_entries)[key]; - *offset = p.first; - *value = p.second.value; - - return true; -} - - -bool extractDVReadableString(std::map> *dv_entries, - std::string key, - int *offset, - std::string *out) -{ - if ((*dv_entries).count(key) == 0) { - verbose("(dvparser) warning: cannot extract std::string from non-existant key \"%s\"\n", key.c_str()); - *offset = -1; - return false; - } - std::pair& p = (*dv_entries)[key]; - *offset = p.first; - - return p.second.extractReadableString(out); -} - -bool DVEntry::extractReadableString(std::string *out) -{ - int t = dif_vif_key.dif() & 0xf; - - std::string v = value; - - if (t == 0x1 || // 8 Bit Integer/Binary - t == 0x2 || // 16 Bit Integer/Binary - t == 0x3 || // 24 Bit Integer/Binary - t == 0x4 || // 32 Bit Integer/Binary - t == 0x6 || // 48 Bit Integer/Binary - t == 0x7 || // 64 Bit Integer/Binary - t == 0xD) // Variable length - { - if (isLikelyAscii(v)) - { - // For example an enhanced id 32 bits binary looks like: - // 44434241 and will be reversed to: 41424344 and translated using ascii - // to ABCD - v = reverseBinaryAsciiSafeToString(v); - } - else - { - v = reverseBCD(v); - } - } - if (t == 0x9 || // 2 digit BCD - t == 0xA || // 4 digit BCD - t == 0xB || // 6 digit BCD - t == 0xC || // 8 digit BCD - t == 0xE) // 12 digit BCD - { - // For example an enhanced id 12 digit bcd looks like: - // 618171183100 and will be reversed to: 003118718161 - v = reverseBCD(v); - } - - *out = v; - return true; -} - -double DVEntry::getCounter(DVEntryCounterType ct) -{ - switch (ct) - { - case DVEntryCounterType::STORAGE_COUNTER: return storage_nr.intValue(); - case DVEntryCounterType::TARIFF_COUNTER: return tariff_nr.intValue(); - case DVEntryCounterType::SUBUNIT_COUNTER: return subunit_nr.intValue(); - case DVEntryCounterType::UNKNOWN: break; - } - - return std::numeric_limits::quiet_NaN(); -} - -std::string DVEntry::str() -{ - std::string s = - tostrprintf("%d: %s %s vif=%x %s%s st=%d ta=%d su=%d", - offset, - dif_vif_key.str().c_str(), - toString(measurement_type), - vif.intValue(), - combinable_vifs.size() > 0 ? "HASCOMB ":"", - combinable_vifs_raw.size() > 0 ? "HASCOMBRAW ":"", - storage_nr.intValue(), - tariff_nr.intValue(), - subunit_nr.intValue() - ); - - return s; -} - -bool extractDate(uchar hi, uchar lo, struct tm *date) -{ - // | hi | lo | - // | YYYY MMMM | YYY DDDDD | - - int day = (0x1f) & lo; - int year1 = ((0xe0) & lo) >> 5; - int month = (0x0f) & hi; - int year2 = ((0xf0) & hi) >> 1; - int year = (2000 + year1 + year2); - - date->tm_mday = day; /* Day of the month (1-31) */ - date->tm_mon = month - 1; /* Month (0-11) */ - date->tm_year = year - 1900; /* Year - 1900 */ - - if (month > 12) return false; - return true; -} - -bool extractTime(uchar hi, uchar lo, struct tm *date) -{ - // | hi | lo | - // | ...hhhhh | ..mmmmmm | - int min = (0x3f) & lo; - int hour = (0x1f) & hi; - - date->tm_min = min; - date->tm_hour = hour; - - if (min > 59) return false; - if (hour > 23) return false; - return true; -} - -bool extractDVdate(std::map> *dv_entries, - std::string key, - int *offset, - struct tm *out) -{ - if ((*dv_entries).count(key) == 0) - { - verbose("(dvparser) warning: cannot extract date from non-existant key \"%s\"\n", key.c_str()); - *offset = -1; - memset(out, 0, sizeof(struct tm)); - return false; - } - std::pair& p = (*dv_entries)[key]; - *offset = p.first; - - return p.second.extractDate(out); -} - -bool DVEntry::extractDate(struct tm *out) -{ - memset(out, 0, sizeof(*out)); - out->tm_isdst = -1; // Figure out the dst automatically! - - std::vector v; - hex2bin(value, &v); - - bool ok = true; - if (v.size() == 2) { - ok &= ::extractDate(v[1], v[0], out); - } - else if (v.size() == 4) { - ok &= ::extractDate(v[3], v[2], out); - ok &= ::extractTime(v[1], v[0], out); - } - else if (v.size() == 6) { - ok &= ::extractDate(v[4], v[3], out); - ok &= ::extractTime(v[2], v[1], out); - // ..ss ssss - int sec = (0x3f) & v[0]; - out->tm_sec = sec; - // There are also bits for day of week, week of year. - // A bit for if daylight saving is in use or not and its offset. - // A bit if it is a leap year. - // I am unsure how to deal with this here..... TODO - } - - return ok; -} - -bool FieldMatcher::matches(DVEntry &dv_entry) -{ - if (!active) return false; - - // Test an explicit dif vif key. - if (match_dif_vif_key) - { - bool b = dv_entry.dif_vif_key == dif_vif_key; - return b; - } - - // Test ranges and types. - bool range = (!match_vif_range || isInsideVIFRange(dv_entry.vif, vif_range)); - bool raw = (!match_vif_raw || dv_entry.vif == vif_raw); - bool type = (!match_measurement_type || dv_entry.measurement_type == measurement_type); - bool storage = (!match_storage_nr || (dv_entry.storage_nr >= storage_nr_from && dv_entry.storage_nr <= storage_nr_to)); - bool tariff = (!match_tariff_nr || (dv_entry.tariff_nr >= tariff_nr_from && dv_entry.tariff_nr <= tariff_nr_to)); - bool subunit = (!match_subunit_nr || (dv_entry.subunit_nr >= subunit_nr_from && dv_entry.subunit_nr <= subunit_nr_to)); - - //printf("Match? range=%d raw=%d type=%d storage=%d tariff=%d subunit=%d \n", range, raw, type, storage, tariff, subunit); - - bool b = range & raw & type & storage & tariff & subunit; - - if (!b) return false; - - // So far so good, now test the combinables. - - // If field matcher has no combinables, then do NOT match any dventry with a combinable! - if (vif_combinables.size()== 0 && vif_combinables_raw.size() == 0) - { - // If there is a combinable vif, then there is a raw combinable vif. So comparing both not strictly necessary. - if (dv_entry.combinable_vifs.size() == 0 && dv_entry.combinable_vifs_raw.size() == 0) return true; - // Oups, field matcher does not expect any combinables, but the dv_entry has combinables. - // This means no match for us since combinables must be handled explicitly. - return false; - } - - // Lets check that the dv_entry vif combinables raw contains the field matcher requested combinable raws. - // The raws are used for meters using reserved and manufacturer specific vif combinables. - for (uint16_t vcr : vif_combinables_raw) - { - if (dv_entry.combinable_vifs_raw.count(vcr) == 0) - { - // Ouch, one of the requested vif combinables raw did not exist in the dv_entry. No match! - return false; - } - } - - // Lets check that the dv_entry combinables contains the field matcher requested combinables. - // The named vif combinables are used by well behaved meters. - for (VIFCombinable vc : vif_combinables) - { - if (vc != VIFCombinable::Any && dv_entry.combinable_vifs.count(vc) == 0) - { - // Ouch, one of the requested combinables did not exist in the dv_entry. No match! - return false; - } - } - - // Now if we have not selected the Any combinable match pattern, - // then we need to check if there are unmatched combinables in the telegram, if so fail the match. - if (vif_combinables.count(VIFCombinable::Any) == 0) - { - if (vif_combinables.size() > 0) - { - for (VIFCombinable vc : dv_entry.combinable_vifs) - { - if (vif_combinables.count(vc) == 0) - { - // Oups, the telegram entry had a vif combinable that we had no matcher for. - return false; - } - } - } - else - { - for (uint16_t vcr : dv_entry.combinable_vifs_raw) - { - if (vif_combinables_raw.count(vcr) == 0) - { - // Oups, the telegram entry had a vif combinable raw that we had no matcher for. - return false; - } - } - } - } - - // Yay, they were all found. - return true; -} - -const char *toString(MeasurementType mt) -{ - switch (mt) - { - case MeasurementType::Any: return "Any"; - case MeasurementType::Instantaneous: return "Instantaneous"; - case MeasurementType::Minimum: return "Minimum"; - case MeasurementType::Maximum: return "Maximum"; - case MeasurementType::AtError: return "AtError"; - case MeasurementType::Unknown: return "Unknown"; - } - return "?"; -} - -MeasurementType toMeasurementType(const char *s) -{ - if (!strcmp(s, "Any")) return MeasurementType::Any; - if (!strcmp(s, "Instantaneous")) return MeasurementType::Instantaneous; - if (!strcmp(s, "Minimum")) return MeasurementType::Minimum; - if (!strcmp(s, "Maximum")) return MeasurementType::Maximum; - if (!strcmp(s, "AtError")) return MeasurementType::AtError; - if (!strcmp(s, "Unknown")) return MeasurementType::Unknown; - - return MeasurementType::Unknown; -} - -std::string FieldMatcher::str() -{ - std::string s = ""; - - if (match_dif_vif_key) - { - s = s+"DVK("+dif_vif_key.str()+") "; - } - - if (match_measurement_type) - { - s = s+"MT("+toString(measurement_type)+") "; - } - - if (match_vif_range) - { - s = s+"VR("+toString(vif_range)+") "; - } - - if (match_vif_raw) - { - s = s+"VRR("+std::to_string(vif_raw)+") "; - } - - if (vif_combinables.size() > 0) - { - s += "Comb("; - - for (auto vc : vif_combinables) - { - s = s+toString(vc)+" "; - } - - s.pop_back(); - s += ")"; - } - - if (match_storage_nr) - { - s = s+"S("+std::to_string(storage_nr_from.intValue())+"-"+std::to_string(storage_nr_to.intValue())+") "; - } - - if (match_tariff_nr) - { - s = s+"T("+std::to_string(tariff_nr_from.intValue())+"-"+std::to_string(tariff_nr_to.intValue())+") "; - } - - if (match_subunit_nr) - { - s += "U("+std::to_string(subunit_nr_from.intValue())+"-"+std::to_string(subunit_nr_to.intValue())+") "; - } - - if (index_nr.intValue() != 1) - { - s += "I("+std::to_string(index_nr.intValue())+")"; - } - - if (s.size() > 0) - { - s.pop_back(); - } - - return s; -} - -DVEntryCounterType toDVEntryCounterType(const std::string &s) -{ - if (s == "storage_counter") return DVEntryCounterType::STORAGE_COUNTER; - if (s == "tariff_counter") return DVEntryCounterType::TARIFF_COUNTER; - if (s == "subunit_counter") return DVEntryCounterType::SUBUNIT_COUNTER; - return DVEntryCounterType::UNKNOWN; -} - -const char *toString(DVEntryCounterType ct) -{ - switch (ct) - { - case DVEntryCounterType::UNKNOWN: return "unknown"; - case DVEntryCounterType::STORAGE_COUNTER: return "storage_counter"; - case DVEntryCounterType::TARIFF_COUNTER: return "tariff_counter"; - case DVEntryCounterType::SUBUNIT_COUNTER: return "subunit_counter"; - } - - return "unknown"; -} - -std::string available_vif_ranges_; - -const std::string &availableVIFRanges() -{ - if (available_vif_ranges_ != "") return available_vif_ranges_; - -#define X(n,from,to,q,u) available_vif_ranges_ += std::string(#n) + "\n"; -LIST_OF_VIF_RANGES -#undef X - - // Remove last newline - available_vif_ranges_.pop_back(); - return available_vif_ranges_; -} - -std::string available_vif_combinables_; - -const std::string &availableVIFCombinables() -{ - if (available_vif_combinables_ != "") return available_vif_combinables_; - -#define X(n,from,to) available_vif_combinables_ += std::string(#n) + "\n"; -LIST_OF_VIF_COMBINABLES -#undef X - - // Remove last newline - available_vif_combinables_.pop_back(); - return available_vif_combinables_; -} diff --git a/components/wmbus_common/dvparser.h b/components/wmbus_common/dvparser.h deleted file mode 100644 index 49c88ac7..00000000 --- a/components/wmbus_common/dvparser.h +++ /dev/null @@ -1,597 +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 . -*/ - -#ifndef DVPARSER_H -#define DVPARSER_H - -#include"util.h" -#include"units.h" - -#include -#include -#include -#include -#include -#include - -#define LIST_OF_VIF_RANGES \ - X(Volume,0x10,0x17,Quantity::Volume,Unit::M3) \ - X(OnTime,0x20,0x23, Quantity::Time, Unit::Hour) \ - X(OperatingTime,0x24,0x27, Quantity::Time, Unit::Hour) \ - X(VolumeFlow,0x38,0x3F, Quantity::Flow, Unit::M3H) \ - X(FlowTemperature,0x58,0x5B, Quantity::Temperature, Unit::C) \ - X(ReturnTemperature,0x5C,0x5F, Quantity::Temperature, Unit::C) \ - X(TemperatureDifference,0x60,0x63, Quantity::Temperature, Unit::C) \ - X(ExternalTemperature,0x64,0x67, Quantity::Temperature, Unit::C) \ - X(Pressure,0x68,0x6B, Quantity::Pressure, Unit::BAR) \ - X(HeatCostAllocation,0x6E,0x6E, Quantity::HCA, Unit::HCA) \ - X(Date,0x6C,0x6C, Quantity::PointInTime, Unit::DateLT) \ - X(DateTime,0x6D,0x6D, Quantity::PointInTime, Unit::DateTimeLT) \ - X(EnergyMJ,0x08,0x0F, Quantity::Energy, Unit::MJ) \ - X(EnergyWh,0x00,0x07, Quantity::Energy, Unit::KWH) \ - X(PowerW,0x28,0x2f, Quantity::Power, Unit::KW) \ - X(PowerJh,0x30,0x37, Quantity::Power, Unit::MJH) \ - X(ActualityDuration,0x74,0x77, Quantity::Time, Unit::Hour) \ - X(FabricationNo,0x78,0x78, Quantity::Text, Unit::TXT) \ - X(EnhancedIdentification,0x79,0x79, Quantity::Text, Unit::TXT) \ - X(EnergyMWh,0x7B00,0x7B01, Quantity::Energy, Unit::KWH) \ - X(EnergyGJ,0x7B08,0x7B09, Quantity::Energy, Unit::MJ) \ - X(RelativeHumidity,0x7B1A,0x7B1B, Quantity::RH, Unit::RH) \ - X(AccessNumber,0x7D08,0x7D08, Quantity::Counter, Unit::COUNTER) \ - X(Medium,0x7D09,0x7D09, Quantity::Text, Unit::TXT) \ - X(Manufacturer,0x7D0A,0x7D0A, Quantity::Text, Unit::TXT) \ - X(ParameterSet,0x7D0B,0x7D0B, Quantity::Text, Unit::TXT) \ - X(ModelVersion,0x7D0C,0x7D0C, Quantity::Text, Unit::TXT) \ - X(HardwareVersion,0x7D0D,0x7D0D, Quantity::Text, Unit::TXT) \ - X(FirmwareVersion,0x7D0E,0x7D0E, Quantity::Text, Unit::TXT) \ - X(SoftwareVersion,0x7D0F,0x7D0F, Quantity::Text, Unit::TXT) \ - X(Location,0x7D10,0x7D10, Quantity::Text, Unit::TXT) \ - X(Customer,0x7D11,0x7D11, Quantity::Text, Unit::TXT) \ - X(ErrorFlags,0x7D17,0x7D17, Quantity::Text, Unit::TXT) \ - X(DigitalOutput,0x7D1A,0x7D1A, Quantity::Text, Unit::TXT) \ - X(DigitalInput,0x7D1B,0x7D1B, Quantity::Text, Unit::TXT) \ - X(DurationSinceReadout,0x7D2c,0x7D2f, Quantity::Time, Unit::Hour) \ - X(DurationOfTariff,0x7D31,0x7D33, Quantity::Time, Unit::Hour) \ - X(Dimensionless,0x7D3A,0x7D3A, Quantity::Counter, Unit::COUNTER) \ - X(Voltage,0x7D40,0x7D4F, Quantity::Voltage, Unit::Volt) \ - X(Amperage,0x7D50,0x7D5F, Quantity::Amperage, Unit::Ampere) \ - X(ResetCounter,0x7D60,0x7D60, Quantity::Counter, Unit::COUNTER) \ - X(CumulationCounter,0x7D61,0x7D61, Quantity::Counter, Unit::COUNTER) \ - X(SpecialSupplierInformation,0x7D67,0x7D67, Quantity::Text, Unit::TXT) \ - X(RemainingBattery,0x7D74,0x7D74, Quantity::Time, Unit::Day) \ - X(AnyVolumeVIF,0x00,0x00, Quantity::Volume, Unit::Unknown) \ - X(AnyEnergyVIF,0x00,0x00, Quantity::Energy, Unit::Unknown) \ - X(AnyPowerVIF,0x00,0x00, Quantity::Power, Unit::Unknown) - - -enum class VIFRange -{ - None, - Any, -#define X(name,from,to,quantity,unit) name, -LIST_OF_VIF_RANGES -#undef X -}; - -struct VIFRaw { - VIFRaw(uint16_t v) : value(v) {} - uint16_t value; -}; - -const char *toString(VIFRange v); -VIFRange toVIFRange(const char *s); -Unit toDefaultUnit(VIFRange v); -VIFRange toVIFRange(int i); -bool isInsideVIFRange(int i, VIFRange range); - -#define LIST_OF_VIF_COMBINABLES \ - X(Reserved,0x00,0x11) \ - X(Average,0x12,0x12) \ - X(InverseCompactProfile,0x13,0x13) \ - X(RelativeDeviation,0x14,0x14) \ - X(RecordErrorCodeMeterToController,0x15,0x1c) \ - X(StandardConformantDataContent,0x1d,0x1d) \ - X(CompactProfileWithRegister,0x1e,0x1e) \ - X(CompactProfile,0x1f,0x1f) \ - X(PerSecond,0x20,0x20) \ - X(PerMinute,0x21,0x21) \ - X(PerHour,0x22,0x22) \ - X(PerDay,0x23,0x23) \ - X(PerWeek,0x24,0x24) \ - X(PerMonth,0x25,0x25) \ - X(PerYear,0x26,0x26) \ - X(PerRevolutionMeasurement,0x27,0x27) \ - X(IncrPerInputPulseChannel0,0x28,0x28) \ - X(IncrPerInputPulseChannel1,0x29,0x29) \ - X(IncrPerOutputPulseChannel0,0x2a,0x2a) \ - X(IncrPerOutputPulseChannel1,0x2b,0x2b) \ - X(PerLitre,0x2c,0x2c) \ - X(PerM3,0x2d,0x2d) \ - X(PerKg,0x2e,0x2e) \ - X(PerKelvin,0x2f,0x2f) \ - X(PerKWh,0x30,0x30) \ - X(PerGJ,0x31,0x31) \ - X(PerKW,0x32,0x32) \ - X(PerKelvinLitreW,0x33,0x33) \ - X(PerVolt,0x34,0x34) \ - X(PerAmpere,0x35,0x35) \ - X(MultipliedByS,0x36,0x36) \ - X(MultipliedBySDivV,0x37,0x37) \ - X(MultipliedBySDivA,0x38,0x38) \ - X(StartDateTimeOfAB,0x39,0x39) \ - X(UncorrectedMeterUnit,0x3a,0x3a) \ - X(ForwardFlow,0x3b,0x3b) \ - X(BackwardFlow,0x3c,0x3c) \ - X(ReservedNonMetric,0x3d,0x3d) \ - X(ValueAtBaseCondC,0x3e,0x3e) \ - X(ObisDeclaration,0x3f,0x3f) \ - X(LowerLimit,0x40,0x40) \ - X(ExceedsLowerLimit,0x41,0x41) \ - X(DateTimeExceedsLowerFirstBegin, 0x42,0x42) \ - X(DateTimeExceedsLowerFirstEnd, 0x43,0x43) \ - X(DateTimeExceedsLowerLastBegin, 0x46,0x46) \ - X(DateTimeExceedsLowerLastEnd, 0x47,0x47) \ - X(UpperLimit,0x48,0x48) \ - X(ExceedsUpperLimit,0x49,0x49) \ - X(DateTimeExceedsUpperFirstBegin, 0x4a,0x4a) \ - X(DateTimeExceedsUpperFirstEnd, 0x4b,0x4b) \ - X(DateTimeExceedsUpperLastBegin, 0x4d,0x4d) \ - X(DateTimeExceedsUpperLastEnd, 0x4e,0x4e) \ - X(DurationExceedsLowerFirst,0x50,0x53) \ - X(DurationExceedsLowerLast,0x54,0x57) \ - X(DurationExceedsUpperFirst,0x58,0x5b) \ - X(DurationExceedsUpperLast,0x5c,0x5f) \ - X(DurationOfDFirst,0x60,0x63) \ - X(DurationOfDLast,0x64,0x67) \ - X(ValueDuringLowerLimitExceeded,0x68,0x68) \ - X(LeakageValues,0x69,0x69) \ - X(OverflowValues,0x6a,0x6a) \ - X(ValueDuringUpperLimitExceeded,0x6c,0x6c) \ - X(DateTimeOfDEFirstBegin,0x6a,0x6a) \ - X(DateTimeOfDEFirstEnd,0x6b,0x6b) \ - X(DateTimeOfDELastBegin,0x6e,0x6e) \ - X(DateTimeOfDELastEnd,0x6f,0x6f) \ - X(MultiplicativeCorrectionFactorForValue,0x70,0x77) \ - X(AdditiveCorrectionConstant,0x78,0x7b) \ - X(CombinableVIFExtension,0x7c,0x7c) \ - X(MultiplicativeCorrectionFactorForValue103,0x7d,0x7d) \ - X(FutureValue,0x7e,0x7e) \ - X(MfctSpecific,0x7f,0x7f) \ - X(AtPhase1,0x7c01,0x7c01) \ - X(AtPhase2,0x7c02,0x7c02) \ - X(AtPhase3,0x7c03,0x7c03) \ - X(AtNeutral,0x7c04,0x7c04) \ - X(BetweenPhaseL1AndL2,0x7c05,0x7c05) \ - X(BetweenPhaseL2AndL3,0x7c06,0x7c06) \ - X(BetweenPhaseL3AndL1,0x7c07,0x7c07) \ - X(AtQuadrantQ1,0x7c08,0x7c08) \ - X(AtQuadrantQ2,0x7c09,0x7c09) \ - X(AtQuadrantQ3,0x7c0a,0x7c0a) \ - X(AtQuadrantQ4,0x7c0b,0x7c0b) \ - X(DeltaBetweenImportAndExport,0x7c0c,0x7c0c) \ - X(AccumulationOfAbsoluteValue,0x7c10,0x7c10) \ - X(DataPresentedWithTypeC,0x7c11,0x7c11) \ - X(DataPresentedWithTypeD,0x7c12,0x7c12) \ - X(Mfct00,0x7f00,0x7f00) \ - X(Mfct01,0x7f01,0x7f01) \ - X(Mfct02,0x7f02,0x7f02) \ - X(Mfct03,0x7f03,0x7f03) \ - X(Mfct04,0x7f04,0x7f04) \ - X(Mfct05,0x7f05,0x7f05) \ - X(Mfct06,0x7f06,0x7f06) \ - X(Mfct07,0x7f07,0x7f07) \ - X(Mfct08,0x7f08,0x7f08) \ - X(Mfct21,0x7f21,0x7f21) \ - -enum class VIFCombinable -{ - None, - Any, -#define X(name,from,to) name, -LIST_OF_VIF_COMBINABLES -#undef X -}; - -struct VIFCombinableRaw { - VIFCombinableRaw(uint16_t v) : value(v) {} - uint16_t value; -}; - -VIFCombinable toVIFCombinable(const char *s); -VIFCombinable toVIFCombinable(int i); -const char *toString(VIFCombinable v); - -enum class MeasurementType -{ - Any, - Instantaneous, - Minimum, - Maximum, - AtError, - Unknown -}; - -const char *toString(MeasurementType mt); -MeasurementType toMeasurementType(const char *s); - -void extractDV(std::string &s, uchar *dif, int *vif, bool *has_difes, bool *has_vifes); - -struct DifVifKey -{ - DifVifKey(std::string key) : key_(key) { - extractDV(key, &dif_, &vif_, &has_difes_, &has_vifes_); - } - std::string str() { return key_; } - bool operator==(DifVifKey &dvk) { return key_ == dvk.key_; } - uchar dif() { return dif_; } - int vif() { return vif_; } - bool hasDifes() { return has_difes_; } - bool hasVifes() { return has_vifes_; } - -private: - - std::string key_; - uchar dif_; - int vif_; - bool has_difes_; - bool has_vifes_; -}; - -void extractDV(DifVifKey &s, uchar *dif, int *vif, bool *has_difes, bool *has_vifes); - -static DifVifKey NoDifVifKey = DifVifKey(""); - -struct Vif -{ - Vif(int n) : nr_(n) {} - int intValue() { return nr_; } - bool operator==(Vif s) { return nr_ == s.nr_; } - -private: - int nr_; -}; - -Unit toDefaultUnit(Vif v); - -enum class DVEntryCounterType -{ - UNKNOWN, - STORAGE_COUNTER, - TARIFF_COUNTER, - SUBUNIT_COUNTER -}; - -DVEntryCounterType toDVEntryCounterType(const std::string &s); -const char *toString(DVEntryCounterType ct); - -struct StorageNr -{ - StorageNr(int n) : nr_(n) {} - int intValue() { return nr_; } - bool operator==(StorageNr s) { return nr_ == s.nr_; } - bool operator!=(StorageNr s) { return nr_ != s.nr_; } - bool operator>=(StorageNr s) { return nr_ >= s.nr_; } - bool operator<=(StorageNr s) { return nr_ <= s.nr_; } - -private: - int nr_; -}; - -static StorageNr AnyStorageNr = StorageNr(-1); - -struct TariffNr -{ - TariffNr(int n) : nr_(n) {} - int intValue() { return nr_; } - bool operator==(TariffNr s) { return nr_ == s.nr_; } - bool operator!=(TariffNr s) { return nr_ != s.nr_; } - bool operator>=(TariffNr s) { return nr_ >= s.nr_; } - bool operator<=(TariffNr s) { return nr_ <= s.nr_; } - -private: - int nr_; -}; - -static TariffNr AnyTariffNr = TariffNr(-1); - -struct SubUnitNr -{ - SubUnitNr(int n) : nr_(n) {} - int intValue() { return nr_; } - bool operator==(SubUnitNr s) { return nr_ == s.nr_; } - bool operator!=(SubUnitNr s) { return nr_ != s.nr_; } - bool operator>=(SubUnitNr s) { return nr_ >= s.nr_; } - bool operator<=(SubUnitNr s) { return nr_ <= s.nr_; } - -private: - int nr_; -}; - -struct IndexNr -{ - IndexNr(int n) : nr_(n) {} - int intValue() { return nr_; } - bool operator==(IndexNr s) { return nr_ == s.nr_; } - bool operator!=(IndexNr s) { return nr_ != s.nr_; } - -private: - int nr_; -}; - -static IndexNr AnyIndexNr = IndexNr(-1); - -struct FieldInfo; - -struct DVEntry -{ - int offset {}; // Where in the telegram this dventry was found. - DifVifKey dif_vif_key; - MeasurementType measurement_type; - Vif vif; - std::set combinable_vifs; - std::set combinable_vifs_raw; - StorageNr storage_nr; - TariffNr tariff_nr; - SubUnitNr subunit_nr; - std::string value; - - DVEntry(int off, - DifVifKey dvk, - MeasurementType mt, - Vif vi, - std::set vc, - std::set vc_raw, - StorageNr st, - TariffNr ta, - SubUnitNr su, - std::string &val) : - offset(off), - dif_vif_key(dvk), - measurement_type(mt), - vif(vi), - combinable_vifs(vc), - combinable_vifs_raw(vc_raw), - storage_nr(st), - tariff_nr(ta), - subunit_nr(su), - value(val) - { - } - - DVEntry() : - offset(999999), - dif_vif_key("????"), - measurement_type(MeasurementType::Instantaneous), - vif(0), - storage_nr(0), - tariff_nr(0), - subunit_nr(0), - value("x") - { - } - - bool extractDouble(double *out, bool auto_scale, bool force_unsigned); - bool extractLong(uint64_t *out); - bool extractDate(struct tm *out); - bool extractReadableString(std::string *out); - void addFieldInfo(FieldInfo *fi) { field_infos_.insert(fi); } - bool hasFieldInfo(FieldInfo *fi) { return field_infos_.count(fi) > 0; } - std::string str(); - - double getCounter(DVEntryCounterType ct); - -private: - std::set field_infos_; // The field infos selected to decode this entry. -}; - -struct FieldMatcher -{ - // If not actually used, this remains false. - bool active = false; - - // Exact difvif hex std::string matching all other checks are ignored. - bool match_dif_vif_key = false; - DifVifKey dif_vif_key { "" }; - - // Match the measurement_type. - bool match_measurement_type = false; - MeasurementType measurement_type { MeasurementType::Instantaneous }; - - // Match the value information range. See dvparser.h - bool match_vif_range = false; - VIFRange vif_range { VIFRange::Any }; - - // Match the vif exactly, used for manufacturer specific vifs. - bool match_vif_raw = false; - uint16_t vif_raw {}; - - // Match any vif combinables. - std::set vif_combinables; - std::set vif_combinables_raw; - - // Match the storage nr. If no storage is specified, default to match only 0. - bool match_storage_nr = true; - StorageNr storage_nr_from { 0 }; - StorageNr storage_nr_to { 0 }; - - // Match the tariff nr. If no tariff is specified, default to match only 0. - bool match_tariff_nr = true; - TariffNr tariff_nr_from { 0 }; - TariffNr tariff_nr_to { 0 }; - - // Match the subunit. If no subunit is specified, default to match only 0. - bool match_subunit_nr = true; - SubUnitNr subunit_nr_from { 0 }; - SubUnitNr subunit_nr_to { 0 }; - - // If the telegram has multiple identical difvif entries matching this field - // and you want to catch the second matching entry, then set the index nr to 2. - // The default is 1. - IndexNr index_nr { 1 }; - - FieldMatcher() : active(false) { } - FieldMatcher(bool act) : active(act) { } - static FieldMatcher build() { return FieldMatcher(true); } - static FieldMatcher noMatcher() { return FieldMatcher(false); } - FieldMatcher &set(DifVifKey k) { - dif_vif_key = k; - match_dif_vif_key = (k.str() != ""); return *this; } - FieldMatcher &set(MeasurementType mt) { - measurement_type = mt; - match_measurement_type = (mt != MeasurementType::Any); - return *this; } - FieldMatcher &set(VIFRange v) { - vif_range = v; - match_vif_range = (v != VIFRange::Any); - return *this; } - FieldMatcher &set(VIFRaw v) { - vif_raw = v.value; - match_vif_raw = true; - return *this; } - FieldMatcher &add(VIFCombinable v) { - vif_combinables.insert(v); - return *this; } - FieldMatcher &add(VIFCombinableRaw v) { - vif_combinables_raw.insert(v.value); - return *this; } - FieldMatcher &set(StorageNr s) { - storage_nr_from = storage_nr_to = s; - match_storage_nr = (s != AnyStorageNr); - return *this; } - FieldMatcher &set(StorageNr from, StorageNr to) { - storage_nr_from = from; - storage_nr_to = to; - match_storage_nr = true; - return *this; } - FieldMatcher &set(TariffNr s) { - tariff_nr_from = tariff_nr_to = s; - match_tariff_nr = (s != AnyTariffNr); - return *this; } - FieldMatcher &set(TariffNr from, TariffNr to) { - tariff_nr_from = from; - tariff_nr_to = to; - match_tariff_nr = true; - return *this; } - FieldMatcher &set(SubUnitNr s) { - subunit_nr_from = subunit_nr_to = s; - match_subunit_nr = true; - return *this; } - FieldMatcher &set(SubUnitNr from, SubUnitNr to) { - subunit_nr_from = from; - subunit_nr_to = to; - match_subunit_nr = true; return *this; } - - FieldMatcher &set(IndexNr i) { index_nr = i; return *this; } - - bool matches(DVEntry &dv_entry); - - // Returns true of there is any range for storage, tariff, subunit nrs. - // I.e. this matcher is expected to match against multiple dv entries! - bool expectedToMatchAgainstMultipleEntries() - { - return (match_storage_nr && storage_nr_from != storage_nr_to) - || (match_tariff_nr && tariff_nr_from != tariff_nr_to) - || (match_subunit_nr && subunit_nr_from != subunit_nr_to); - } - - std::string str(); -}; - -bool loadFormatBytesFromSignature(uint16_t format_signature, std::vector *format_bytes); - -struct Telegram; - -bool parseDV(Telegram *t, - std::vector &databytes, - std::vector::iterator data, - size_t data_len, - std::map> *dv_entries, - std::vector::iterator *format = NULL, - size_t format_len = 0, - uint16_t *format_hash = NULL); - -// Instead of using a hardcoded difvif as key in the extractDV... below, -// find an existing difvif entry in the values based on the desired value information type. -// Like: Volume, VolumeFlow, FlowTemperature, ExternalTemperature etc -// in combination with the storagenr. (Later I will add tariff/subunit) -bool findKey(MeasurementType mt, VIFRange vi, StorageNr storagenr, TariffNr tariffnr, - std::string *key, std::map> *values); -// Some meters have multiple identical DIF/VIF values! Meh, they are not using storage nrs or tariff nrs. -// So here we can pick for example nr 2 of an identical set if DIF/VIF values. -// Nr 1 means the first found value. -bool findKeyWithNr(MeasurementType mt, VIFRange vi, StorageNr storagenr, TariffNr tariffnr, int indexnr, - std::string *key, std::map> *values); - -bool hasKey(std::map> *values, std::string key); - -bool extractDVuint8(std::map> *values, - std::string key, - int *offset, - uchar *value); - -bool extractDVuint16(std::map> *values, - std::string key, - int *offset, - uint16_t *value); - -bool extractDVuint24(std::map> *values, - std::string key, - int *offset, - uint32_t *value); - -bool extractDVuint32(std::map> *values, - std::string key, - int *offset, - uint32_t *value); - -// All values are scaled according to the vif and wmbusmeters scaling defaults. -bool extractDVdouble(std::map> *values, - std::string key, - int *offset, - double *value, - bool auto_scale = true, - bool force_unsigned = false); - -// Extract a value without scaling. Works for 8bits to 64 bits, binary and bcd. -bool extractDVlong(std::map> *values, - std::string key, - int *offset, - uint64_t *value); - -// Just copy the raw hex data into the std::string, not reversed or anything. -bool extractDVHexString(std::map> *values, - std::string key, - int *offset, - std::string *value); - -// Read the content and attempt to reverse and transform it into a readble std::string -// based on the dif information. -bool extractDVReadableString(std::map> *values, - std::string key, - int *offset, - std::string *value); - -bool extractDVdate(std::map> *values, - std::string key, - int *offset, - struct tm *value); - - -const std::string &availableVIFRanges(); -const std::string &availableVIFCombinables(); - -#endif diff --git a/components/wmbus_common/formula.cpp b/components/wmbus_common/formula.cpp deleted file mode 100644 index 3e898dc0..00000000 --- a/components/wmbus_common/formula.cpp +++ /dev/null @@ -1,1295 +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"formula.h" -#include"formula_implementation.h" -#include"meters.h" -#include"units.h" - -#include -#include -#include - -NumericFormula::~NumericFormula() { } -NumericFormulaConstant::~NumericFormulaConstant() { } -NumericFormulaMeterField::~NumericFormulaMeterField() { } -NumericFormulaDVEntryField::~NumericFormulaDVEntryField() { } -NumericFormulaPair::~NumericFormulaPair() { } -NumericFormulaAddition::~NumericFormulaAddition() { } -NumericFormulaSubtraction::~NumericFormulaSubtraction() { } -NumericFormulaMultiplication::~NumericFormulaMultiplication() { } -NumericFormulaDivision::~NumericFormulaDivision() { } -NumericFormulaExponentiation::~NumericFormulaExponentiation() { } -NumericFormulaSquareRoot::~NumericFormulaSquareRoot() { } - -double NumericFormulaConstant::calculate(SIUnit to) -{ - double r {}; - siunit().convertTo(constant_, to, &r); - return r; -} - -double NumericFormulaMeterField::calculate(SIUnit to_si_unit) -{ - if (formula()->meter() == NULL) return std::numeric_limits::quiet_NaN(); - - FieldInfo *fi = formula()->meter()->findFieldInfo(vname_, quantity_); - - Unit field_unit = fi->displayUnit(); - double val = formula()->meter()->getNumericValue(fi, field_unit); - - const SIUnit& field_si_unit = toSIUnit(field_unit); - - double r {}; - field_si_unit.convertTo(val, to_si_unit, &r); - return r; -} - -double NumericFormulaDVEntryField::calculate(SIUnit to_si_unit) -{ - if (formula()->dventry() == NULL) return std::numeric_limits::quiet_NaN(); - - double val = formula()->dventry()->getCounter(counter_); - - const SIUnit& counter_si_unit = toSIUnit(Unit::COUNTER); - - double r {}; - counter_si_unit.convertTo(val, to_si_unit, &r); - return r; -} - -double NumericFormulaAddition::calculate(SIUnit to_siunit) -{ - double l = left_->calculate(left_->siunit()); - double r = right_->calculate(right_->siunit()); - - double v {}; - SIUnit v_siunit(Unit::COUNTER); - left_->siunit().mathOpTo(MathOp::ADD, l, r, right_->siunit(), &v_siunit, &v); - - double result {}; - v_siunit.convertTo(v, to_siunit, &result); - - debug("(formula) ADD %g (%s) %g (%s) --> %g %s --> %g %s\n", - l, left_->siunit().info().c_str(), - r, right_->siunit().info().c_str(), - v, v_siunit.info().c_str(), - result, siunit().info().c_str()); - - return result; -} - -double NumericFormulaSubtraction::calculate(SIUnit to_siunit) -{ - double l = left_->calculate(left_->siunit()); - double r = right_->calculate(right_->siunit()); - - double v {}; - SIUnit v_siunit(Unit::COUNTER); - left_->siunit().mathOpTo(MathOp::SUB, l, r, right_->siunit(), &v_siunit, &v); - - double result {}; - v_siunit.convertTo(v, to_siunit, &result); - - debug("(formula) SUB %g (%s) %g (%s) --> %g %s --> %g %s\n", - l, left_->siunit().info().c_str(), - r, right_->siunit().info().c_str(), - v, v_siunit.info().c_str(), - result, siunit().info().c_str()); - - return result; -} - -double NumericFormulaMultiplication::calculate(SIUnit to_siunit) -{ - double l = left_->calculate(left_->siunit()); - double r = right_->calculate(right_->siunit()); - double m = l*r; - double v {}; - siunit().convertTo(m, to_siunit, &v); - - debug("(formula) MUL %g (%s) %g (%s) --> %g --> %g %s\n", - l, left_->siunit().info().c_str(), - r, right_->siunit().info().c_str(), - m, - v, to_siunit.info().c_str()); - - return v; -} - -double NumericFormulaDivision::calculate(SIUnit to_siunit) -{ - double l = left_->calculate(left_->siunit()); - double r = right_->calculate(right_->siunit()); - double d = l/r; - double v {}; - siunit().convertTo(d, to_siunit, &v); - - debug("(formula) DIV %g (%s) %g (%s) --> %g --> %g %s\n", - l, left_->siunit().info().c_str(), - r, right_->siunit().info().c_str(), - d, - v, - to_siunit.info().c_str()); - - return v; -} - -double NumericFormulaExponentiation::calculate(SIUnit to_siunit) -{ - double l = left_->calculate(to_siunit); - double r = right_->calculate(to_siunit); - double p = pow(l,r); - double v {}; - siunit().convertTo(p, to_siunit, &v); - - debug("(formula) %g <-- %g <-- pow %g ^ %g\n", v, p, l, r); - return v; -} - -double NumericFormulaSquareRoot::calculate(SIUnit to_siunit) -{ - double i = inner_->calculate(inner_->siunit()); - double s = sqrt(i); - double v {}; - siunit().convertTo(s, to_siunit, &v); - - debug("(formula) SQRT %g (%s) --> %g --> %g %s\n", - i, inner_->siunit().info().c_str(), - s, - v, - to_siunit.info().c_str()); - - return v; -} - -const char *toString(TokenType tt) -{ - switch (tt) { - case TokenType::SPACE: return "SPACE"; - case TokenType::NUMBER: return "NUMBER"; - case TokenType::DATETIME: return "DATETIME"; - case TokenType::TIME: return "TIME"; - case TokenType::LPAR: return "LPAR"; - case TokenType::RPAR: return "RPAR"; - case TokenType::PLUS: return "PLUS"; - case TokenType::MINUS: return "MINUS"; - case TokenType::TIMES: return "TIMES"; - case TokenType::DIV: return "DIV"; - case TokenType::EXP: return "EXP"; - case TokenType::SQRT: return "SQRT"; - case TokenType::UNIT: return "UNIT"; - case TokenType::FIELD: return "FIELD"; - } - return "?"; -} - -std::string Token::str(const std::string &s) -{ - std::string v = s.substr(start, len); - return tostrprintf("%s(%s)", toString(type), v.c_str()); -} - -double Token::val(const std::string &s) -{ - std::string v = s.substr(start, len); - if (type == TokenType::NUMBER) - { - return atof(v.c_str()); - } - else if (type == TokenType::DATETIME) - { - struct tm time {}; - char *ok = strptime(v.c_str(), "'%Y-%m-%d %H:%M:%S'", &time); - if (!ok) ok = strptime(v.c_str(), "'%Y-%m-%dT%H:%M:%SZ'", &time); - time_t epoch = mktime(&time); - double result = (double)epoch; - return result; - } - else if (type == TokenType::TIME) - { - int h = 0; - int m = 0; - int s = 0; - sscanf(v.c_str(), "'%02d:%02d:%02d'", &h, &m, &s); - double result = h*3600+m*60+s; - return result; - } - - assert(0); - return 0; -} - -std::string Token::vals(const std::string &s) -{ - return s.substr(start, len); -} - -Unit Token::unit(const std::string &s) -{ - std::string v = s.substr(start, len); - return toUnit(v); -} - -void FormulaImplementation::clear() -{ - valid_ = true; - op_stack_.clear(); - tokens_.clear(); - formula_ = ""; - dventry_ = NULL; - meter_ = NULL; -} - -bool is_letter(char c) -{ - return c >= 'a' && c <= 'z'; -} - -bool is_letter_or_underscore(char c) -{ - return c == '_' || (c >= 'a' && c <= 'z'); -} - -bool is_letter_digit_or_underscore(char c) -{ - return c == '_' || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9'); -} - -size_t FormulaImplementation::findSpace(size_t i) -{ - size_t len = 0; - while(isspace(formula_[i])) - { - i++; - len++; - } - return len; -} - -size_t FormulaImplementation::findNumber(size_t i) -{ - size_t len = 0; - size_t start = i; - int num_dots = 0; - while (true) - { - if (i >= formula_.length()) break; - char c = formula_[i]; - if ((c < '0' || c > '9') && c != '.') break; - if (c == '.') - { - if (start == i) return 0; // Numbers do not start with a dot. - num_dots++; - } - i++; - len++; - } - - if (num_dots > 1) return 0; // More than one decimal dot is an error. - - return len; -} - -size_t FormulaImplementation::findDateTime(size_t i) -{ - // A datetime is converted into a unix timestamp. - // Patterns: '2022-12-30T09:32:45Z' - // '2222-22-22 11:11:00' - // '2222-22-22 11:11' - // '2222-22-22' - - struct tm time {}; - const char *start = &formula_[i]; - const char *end; - - memset(&time, 0, sizeof(time)); - if (i+21 < formula_.length()) - { - end = strptime(start, "'%Y-%m-%dT%H:%M:%SZ'", &time); - if (std::distance(start, end) == 22) return 22; - } - - if (i+20 < formula_.length()) - { - end = strptime(start, "'%Y-%m-%d %H:%M:%S'", &time); - if (std::distance(start, end) == 21) return 21; - } - - if (i+17 < formula_.length()) - { - end = strptime(start, "'%Y-%m-%d %H:%M'", &time); - if (std::distance(start, end) == 18) return 18; - } - - if (i+11 < formula_.length()) - { - end = strptime(start, "'%Y-%m-%d'", &time); - if (std::distance(start, end) == 12) return 12; - } - - return 0; -} - -size_t FormulaImplementation::findTime(size_t i) -{ - // A time is converted into seconds 10:11:12 is 10*3600+11*60+12 seconds. - // Patterns: '11:22:15' - // '11:22' - - if (i+9 < formula_.length() && - '\'' == formula_[i+0] && - isdigit(formula_[i+1]) && - isdigit(formula_[i+2]) && - ':' == formula_[i+3] && - isdigit(formula_[i+4]) && - isdigit(formula_[i+5]) && - ':' == formula_[i+6] && - isdigit(formula_[i+7]) && - isdigit(formula_[i+8]) && - '\'' == formula_[i+9]) - { - return 10; - } - - if (i+6 < formula_.length() && - '\'' == formula_[i+0] && - isdigit(formula_[i+1]) && - isdigit(formula_[i+2]) && - ':' == formula_[i+3] && - isdigit(formula_[i+4]) && - isdigit(formula_[i+5]) && - '\'' == formula_[i+6]) - { - return 7; - } - - return 0; -} - - -size_t FormulaImplementation::findPlus(size_t i) -{ - if (i >= formula_.length()) return 0; - - char c = formula_[i]; - if (c == '+') return 1; - - return 0; -} - -size_t FormulaImplementation::findMinus(size_t i) -{ - if (i >= formula_.length()) return 0; - - char c = formula_[i]; - if (c == '-') return 1; - - return 0; -} - -size_t FormulaImplementation::findTimes(size_t i) -{ - if (i >= formula_.length()) return 0; - - char c = formula_[i]; - if (c == '*') return 1; - - return 0; -} - -size_t FormulaImplementation::findDiv(size_t i) -{ - if (i >= formula_.length()) return 0; - - char c = formula_[i]; - if (c == '/') return 1; - - return 0; -} - -size_t FormulaImplementation::findExp(size_t i) -{ - return 0; - /* - if (i >= formula_.length()) return 0; - - char c = formula_[i]; - if (c == '^') return 1; - - return 0; - */ -} - -size_t FormulaImplementation::findSqrt(size_t i) -{ - if (i+4 >= formula_.length()) return 0; - - if (!strncmp(&formula_[i], "sqrt", 4) && !is_letter(formula_[i+4])) - { - return 4; - } - - return 0; -} - -size_t FormulaImplementation::findLPar(size_t i) -{ - if (i >= formula_.length()) return 0; - - char c = formula_[i]; - if (c == '(') return 1; - - return 0; -} - -size_t FormulaImplementation::findRPar(size_t i) -{ - if (i >= formula_.length()) return 0; - - char c = formula_[i]; - if (c == ')') return 1; - - return 0; -} - -size_t FormulaImplementation::findUnit(size_t i) -{ - if (i >= formula_.length()) return 0; - - size_t len = formula_.length(); - char c = formula_[i]; - - // All units start with a lower case a-z, followed by more letters and _ underscores. - if (!is_letter(c)) return 0; - - size_t longest_unit = 0; - -#define X(cname,lcname,hrname,quantity,explanation) \ - if ( (i+sizeof(#lcname)-1 <= len) && \ - !is_letter_digit_or_underscore(formula_[i+sizeof(#lcname)-1]) && \ - !strncmp(#lcname, formula_.c_str()+i, sizeof(#lcname)-1) && \ - longest_unit < (sizeof(#lcname)-1)) { longest_unit = (sizeof(#lcname)-1); } -LIST_OF_UNITS -#undef X - - return longest_unit; -} - -size_t FormulaImplementation::findField(size_t i) -{ - size_t len = 0; - size_t start = i; - - // All field names are lower case a-z. - if (!is_letter(formula_[start])) return 0; - - while (true) - { - if (i >= formula_.length()) break; - char c = formula_[i]; - // After first letter field names can contain more letters digits and underscores. - if (!is_letter_digit_or_underscore(c)) break; - i++; - len++; - } - - return len; -} - -bool FormulaImplementation::tokenize() -{ - size_t i = 0; - - while (i 0) { i+=len; continue; } // No token added for whitespace. - - len = findDateTime(i); - if (len > 0) { tokens_.push_back(Token(TokenType::DATETIME, i, len)); i+=len; continue; } - - len = findTime(i); - if (len > 0) { tokens_.push_back(Token(TokenType::TIME, i, len)); i+=len; continue; } - - len = findNumber(i); - if (len > 0) { tokens_.push_back(Token(TokenType::NUMBER, i, len)); i+=len; continue; } - - len = findLPar(i); - if (len > 0) { tokens_.push_back(Token(TokenType::LPAR, i, len)); i+=len; continue; } - - len = findRPar(i); - if (len > 0) { tokens_.push_back(Token(TokenType::RPAR, i, len)); i+=len; continue; } - - len = findPlus(i); - if (len > 0) { tokens_.push_back(Token(TokenType::PLUS, i, len)); i+=len; continue; } - - len = findMinus(i); - if (len > 0) { tokens_.push_back(Token(TokenType::MINUS, i, len)); i+=len; continue; } - - len = findTimes(i); - if (len > 0) { tokens_.push_back(Token(TokenType::TIMES, i, len)); i+=len; continue; } - - len = findDiv(i); - if (len > 0) { tokens_.push_back(Token(TokenType::DIV, i, len)); i+=len; continue; } - - len = findExp(i); - if (len > 0) { tokens_.push_back(Token(TokenType::EXP, i, len)); i+=len; continue; } - - len = findSqrt(i); - if (len > 0) { tokens_.push_back(Token(TokenType::SQRT, i, len)); i+=len; continue; } - - len = findUnit(i); - if (len > 0) { tokens_.push_back(Token(TokenType::UNIT, i, len)); i+=len; continue; } - - len = findField(i); - if (len > 0) { tokens_.push_back(Token(TokenType::FIELD, i, len)); i+=len; continue; } - - break; - } - - // Interrupted early, thus there was an error tokenizing. - if (i < formula_.length()) - { - Token tok(TokenType::SPACE, i, 0); - std::string s = std::string("Unknown token!\n")+tok.withMarker(formula_); - errors_.push_back(s); - valid_ = false; - return false; - } - - return true; -} - -size_t FormulaImplementation::parseOps(size_t i) -{ - Token *tok = LA(i); - Token *next= LA(i+1); - if (tok == NULL) return i; - - if (tok->type == TokenType::FIELD) - { - handleField(tok); - return i+1; - } - - if (tok->type == TokenType::DATETIME) - { - handleUnixTimestamp(tok); - return i+1; - } - - if (tok->type == TokenType::TIME) - { - handleSeconds(tok); - return i+1; - } - - if (tok->type == TokenType::PLUS) - { - size_t next = parseOps(i+1); - if (!valid_) return next; - handleAddition(tok); - return next; - } - - if (tok->type == TokenType::MINUS) - { - size_t next = parseOps(i+1); - if (!valid_) return next; - handleSubtraction(tok); - return next; - } - - if (tok->type == TokenType::TIMES) - { - size_t next = parseOps(i+1); - if (!valid_) return next; - handleMultiplication(tok); - return next; - } - - if (tok->type == TokenType::DIV) - { - size_t next = parseOps(i+1); - if (!valid_) return next; - handleDivision(tok); - return next; - } - - if (tok->type == TokenType::EXP) - { - size_t next = parseOps(i+1); - if (!valid_) return next; - handleExponentiation(tok); - return next; - } - - if (tok->type == TokenType::SQRT) - { - size_t next = parseOps(i+1); - if (!valid_) return next; - handleSquareRoot(tok); - return next; - } - - if (tok->type == TokenType::LPAR) - { - return parsePar(i); - } - - if (tok->type == TokenType::RPAR) - { - return i; - } - - if (tok->type == TokenType::NUMBER) - { - if (next == NULL || next->type != TokenType::UNIT) - { - errors_.push_back(tostrprintf("Constant number %s lacks a valid unit!\n%s", - tok->vals(formula_).c_str(), - tok->withMarker(formula_).c_str())); - valid_ = false; - return i; - } - handleConstant(tok, next); - return i+2; - } - - return i; -} - -size_t FormulaImplementation::parsePar(size_t i) -{ - Token *tok = LA(i); - assert(tok->type == TokenType::LPAR); - - i++; - for (;;) - { - tok = LA(i); - if (tok == NULL) break; - if (tok->type == TokenType::RPAR) break; - size_t next = parseOps(i); - if (!valid_ || next == i) break; - i = next; - } - - if (valid_ && tok == NULL) - { - errors_.push_back("Missing closing parenthesis at end of formula!\n"); - valid_ = false; - return i; - } - - if (valid_ && tok->type != TokenType::RPAR) - { - errors_.push_back("Expected closing parenthesis!\n"+tok->withMarker(formula_)); - valid_ = false; - return i; - } - return i+1; -} - -void FormulaImplementation::handleConstant(Token *number, Token *unit) -{ - double c = number->val(formula_); - Unit u = unit->unit(formula_); - - if (u == Unit::Unknown) - { - errors_.push_back(tostrprintf("Unknown unit \"%s\"!\n%s", - unit->vals(formula_).c_str(), - unit->withMarker(formula_).c_str())); - valid_ = false; - return; - } - - doConstant(u, c); -} - -void FormulaImplementation::handleUnixTimestamp(Token *number) -{ - double c = number->val(formula_); - - doConstant(Unit::UnixTimestamp, c); -} - -void FormulaImplementation::handleSeconds(Token *number) -{ - double c = number->val(formula_); - Unit u = Unit::Second; - - doConstant(u, c); -} - -void FormulaImplementation::handleAddition(Token *tok) -{ - SIUnit right_siunit = topOp()->siunit(); - SIUnit left_siunit = top2Op()->siunit(); - SIUnit to_siunit(Unit::COUNTER); - - bool ok = left_siunit.mathOpTo(MathOp::ADD, 0, 0, right_siunit, &to_siunit, NULL); - - if (!ok) - { - std::string lsis = left_siunit.str(); - std::string rsis = right_siunit.str(); - errors_.push_back(tostrprintf("Cannot add %s to %s!\n%s", - left_siunit.info().c_str(), - right_siunit.info().c_str(), - tok->withMarker(formula_).c_str())); - valid_ = false; - return; - } - - doAddition(to_siunit); -} - -void FormulaImplementation::handleSubtraction(Token *tok) -{ - SIUnit right_siunit = topOp()->siunit(); - SIUnit left_siunit = top2Op()->siunit(); - - SIUnit v_siunit(Unit::COUNTER); - - bool ok = left_siunit.mathOpTo(MathOp::SUB, 0, 0, right_siunit, &v_siunit, NULL); - - if (!ok) - { - std::string lsis = left_siunit.str(); - std::string rsis = right_siunit.str(); - errors_.push_back(tostrprintf("Cannot subtract %s from %s!\n%s", - left_siunit.info().c_str(), - right_siunit.info().c_str(), - tok->withMarker(formula_).c_str())); - valid_ = false; - return; - } - - doSubtraction(v_siunit); -} - -void FormulaImplementation::handleMultiplication(Token *tok) -{ - // Any two units can be multiplied! You might not like the answer thought.... - doMultiplication(); -} - -void FormulaImplementation::handleDivision(Token *tok) -{ - // Any two units can be divided! You might not like the answer thought.... - doDivision(); -} - -void FormulaImplementation::handleExponentiation(Token *tok) -{ - // You can only exponentiate to a number. - doExponentiation(); -} - -void FormulaImplementation::handleSquareRoot(Token *tok) -{ - doSquareRoot(); -} - -void FormulaImplementation::handleField(Token *field) -{ - std::string field_name = field->vals(formula_); // Full field: total_m3 - std::string vname; // Without unit: total - Unit named_unit; // The extracted unit: m3 - bool ok = extractUnit(field_name, &vname, &named_unit); - - if (!ok) - { - errors_.push_back("Cannot extra a valid unit from field name \""+field_name+"\"\n"+field->withMarker(formula_)); - return; - } - - DVEntryCounterType dve = toDVEntryCounterType(field_name); - - if (dve != DVEntryCounterType::UNKNOWN) - { - debug("(formula) handle dventry field %s into %s %s\n", field_name.c_str(), vname.c_str(), - unitToStringLowerCase(named_unit).c_str()); - - doDVEntryField(named_unit, dve); - } - else - { - debug("(formula) handle meter field %s into %s %s\n", field_name.c_str(), vname.c_str(), - unitToStringLowerCase(named_unit).c_str()); - - Quantity q = toQuantity(named_unit); - FieldInfo *f = meter_->findFieldInfo(vname, q); - - if (f == NULL) - { - errors_.push_back("No such field found \""+field_name+"\"\n"+field->withMarker(formula_)); - valid_ = false; - return; - } - - doMeterField(named_unit, f); - } -} - -bool FormulaImplementation::go() -{ - size_t i = 0; - - for (;;) - { - size_t next = parseOps(i); - if (!valid_ || next == i) break; - i = next; - } - - return i == tokens_.size(); -} - -Token *FormulaImplementation::LA(size_t i) -{ - if (i < 0 || i >= tokens_.size()) return NULL; - return &tokens_[i]; -} - -bool FormulaImplementation::parse(Meter *m, const std::string &f) -{ - bool ok; - - clear(); - - meter_ = m; - formula_ = f; - - debug("(formula) parsing \"%s\"\n", formula_.c_str()); - - ok = tokenize(); - if (!ok) return false; - - - debug("(formula) tokens: "); - for (Token &t : tokens_) - { - debug("%s ", t.str(formula_).c_str()); - } - debug("\n"); - - - ok = go(); - if (!ok) return false; - - debug("(formula) %s\n", tree().c_str()); - - return valid_; -} - -bool FormulaImplementation::valid() -{ - return valid_ == true && op_stack_.size() == 1; -} - -std::string FormulaImplementation::errors() -{ - std::string s; - for (std::string& e : errors_) s += e; - return s; -} - -double FormulaImplementation::calculate(Unit to, DVEntry *dve, Meter *m) -{ - if (dve != NULL) dventry_ = dve; - if (m != NULL) meter_ = m; - - if (!valid_) - { - std::string t = tree(); - warning("Warning! Formula is not valid! Returning nan!\n%s\n", t.c_str()); - return std::nan(""); - } - - if (op_stack_.size() != 1) - { - std::string t = tree(); - warning("Warning! Formula is not valid! Multiple ops on stack! Returning nan!\n%s\n", t.c_str()); - return std::nan(""); - } - - return topOp()->calculate(toSIUnit(to)); -} - -void FormulaImplementation::doConstant(Unit u, double c) -{ - pushOp(new NumericFormulaConstant(this, u, c)); -} - -void FormulaImplementation::doAddition(const SIUnit &to_siunit) -{ - assert(op_stack_.size() >= 2); - - std::unique_ptr right_node = popOp(); - std::unique_ptr left_node = popOp(); - - pushOp(new NumericFormulaAddition(this, to_siunit, left_node, right_node)); -} - -void FormulaImplementation::doSubtraction(const SIUnit &to_siunit) -{ - assert(op_stack_.size() >= 2); - - std::unique_ptr right_node = popOp(); - std::unique_ptr left_node = popOp(); - - pushOp(new NumericFormulaSubtraction(this, to_siunit, left_node, right_node)); -} - -void FormulaImplementation::doMultiplication() -{ - assert(op_stack_.size() >= 2); - - SIUnit right_siunit = topOp()->siunit(); - - std::unique_ptr right_node = popOp(); - - SIUnit left_siunit = topOp()->siunit(); - - std::unique_ptr left_node = popOp(); - - SIUnit mul_siunit = left_siunit.mul(right_siunit); - - std::string lsis = left_siunit.info(); - std::string rsis = right_siunit.info(); - std::string msis = mul_siunit.info(); - - pushOp(new NumericFormulaMultiplication(this, mul_siunit, left_node, right_node)); -} - -void FormulaImplementation::doDivision() -{ - assert(op_stack_.size() >= 2); - - SIUnit right_siunit = topOp()->siunit(); - - std::unique_ptr right_node = popOp(); - - SIUnit left_siunit = topOp()->siunit(); - - std::unique_ptr left_node = popOp(); - - SIUnit div_siunit = left_siunit.div(right_siunit); - - std::string lsis = left_siunit.info(); - std::string rsis = right_siunit.info(); - std::string dsis = div_siunit.info(); - - debug("(formula) unit %s DIV %s ==> %s\n", lsis.c_str(), rsis.c_str(), dsis.c_str()); - - pushOp(new NumericFormulaDivision(this, div_siunit, left_node, right_node)); -} - -void FormulaImplementation::doExponentiation() -{ - assert(op_stack_.size() >= 2); - -// SIUnit right_siunit = topOp()->siunit(); - - std::unique_ptr right_node = popOp(); - - SIUnit left_siunit = topOp()->siunit(); - - std::unique_ptr left_node = popOp(); - - pushOp(new NumericFormulaDivision(this, left_siunit, left_node, right_node)); - -// assert(canConvert(left_siunit, right_siunit)); -} - -void FormulaImplementation::doSquareRoot() -{ - assert(op_stack_.size() >= 1); - - SIUnit inner_siunit = topOp()->siunit(); - - SIUnit siunit = inner_siunit.sqrt(); - - std::unique_ptr inner_node = popOp(); - - pushOp(new NumericFormulaSquareRoot(this, siunit, inner_node)); -} - - -void FormulaImplementation::doMeterField(Unit u, FieldInfo *fi) -{ - SIUnit from_si_unit = toSIUnit(fi->displayUnit()); - SIUnit to_si_unit = toSIUnit(u); - assert(from_si_unit.convertTo(0, to_si_unit, NULL)); - - pushOp(new NumericFormulaMeterField(this, u, fi->vname(), fi->xuantity())); -} - -void FormulaImplementation::doDVEntryField(Unit u, DVEntryCounterType ct) -{ - pushOp(new NumericFormulaDVEntryField(this, Unit::COUNTER, ct)); -} - -Formula *newFormula() -{ - return new FormulaImplementation(); -} - -Formula::~Formula() -{ -} - -FormulaImplementation::~FormulaImplementation() -{ -} - -std::string FormulaImplementation::str() -{ - return topOp()->str(); -} - -std::string FormulaImplementation::tree() -{ - std::string s; - - for (auto &op : op_stack_) - { - if (s.length() > 0) s += " | "; - s += op->tree(); - if (s.back() == ' ') s.pop_back(); - } - return s; -} - -void FormulaImplementation::setMeter(Meter *m) -{ - meter_ = m; -} - -void FormulaImplementation::setDVEntry(DVEntry *dve) -{ - dventry_ = dve; -} - -std::string NumericFormulaConstant::str() -{ - return tostrprintf("%.17g %s", constant_, siunit()); -} - -std::string NumericFormulaConstant::tree() -{ - Quantity q = siunit().quantity(); - Unit nearest = siunit().asUnit(q); - std::string sis = siunit().str(); - - return tostrprintf(" ", constant_, unitToStringLowerCase(nearest).c_str(), sis.c_str(), toString(q)); -} - -std::string NumericFormulaPair::str() -{ - std::string left = left_->tree(); - std::string right = right_->tree(); - return left+" "+op_+" "+right; -} - -std::string NumericFormulaPair::tree() -{ - std::string left = left_->tree(); - std::string right = right_->tree(); - return "<"+name_+" "+left+right+"> "; -} - -std::string NumericFormulaSquareRoot::str() -{ - std::string inner = inner_->str(); - return "sqrt("+inner+")"; -} - -std::string NumericFormulaSquareRoot::tree() -{ - std::string inner = inner_->tree(); - return " "; -} - -std::string NumericFormulaMeterField::str() -{ - if (formula()->meter() == NULL) return ""; - FieldInfo *fi = formula()->meter()->findFieldInfo(vname_, quantity_); - - return fi->vname()+"_"+unitToStringLowerCase(fi->displayUnit()); -} - -std::string NumericFormulaMeterField::tree() -{ - if (formula()->meter() == NULL) return ""; - FieldInfo *fi = formula()->meter()->findFieldInfo(vname_, quantity_); - return "vname()+"_"+unitToStringLowerCase(fi->displayUnit())+"> "; -} - -std::string NumericFormulaDVEntryField::str() -{ - return toString(counter_); -} - -std::string NumericFormulaDVEntryField::tree() -{ - return std::string(" "; -} - -void FormulaImplementation::pushOp(NumericFormula *nf) -{ - op_stack_.push_back(std::unique_ptr(nf)); -} - -std::unique_ptr FormulaImplementation::popOp() -{ - assert(op_stack_.size() > 0); - std::unique_ptr nf = std::move(op_stack_.back()); - op_stack_.pop_back(); - return nf; -} - -NumericFormula * FormulaImplementation::topOp() -{ - assert(op_stack_.size() > 0); - return op_stack_.back().get(); -} - -NumericFormula * FormulaImplementation::top2Op() -{ - assert(op_stack_.size() > 1); - return op_stack_[op_stack_.size()-2].get(); -} - -SIUnit &FormulaImplementation::siUnit() -{ - return topOp()->siunit(); -} - -std::string Token::withMarker(const std::string& formula) -{ - std::string indent; - size_t n = start; - while (n > 0) - { - indent += " "; - n--; - } - return formula+"\n"+indent+"^~~~~\n"; -} - -StringInterpolator *newStringInterpolator() -{ - return new StringInterpolatorImplementation(); -} - -StringInterpolator::~StringInterpolator() -{ -} - -StringInterpolatorImplementation::~StringInterpolatorImplementation() -{ -} - -bool StringInterpolatorImplementation::parse(Meter *m, const std::string &f) -{ - strings_.clear(); - formulas_.clear(); - - size_t prev_string_start = 0; - size_t next_start_brace = f.find('{', prev_string_start); - - while (next_start_brace != std::string::npos) - { - // Push the std::string up to the brace. - std::string part = f.substr(prev_string_start, next_start_brace - prev_string_start); - strings_.push_back(part); - - // Find the end of the formula. - size_t next_end_brace = f.find('}', next_start_brace); - if (next_end_brace == std::string::npos) return false; // Oups, missing closing } - - std::string formula = f.substr(next_start_brace+1, next_end_brace - next_start_brace - 1); - - formulas_.push_back(std::unique_ptr(newFormula())); - bool ok = formulas_.back()->parse(m, formula); - if (!ok) return false; - - prev_string_start = next_end_brace+1; - - next_start_brace = f.find('{', prev_string_start); - } - - // Add any remaining std::string segment after the last formula. - if (prev_string_start < f.length()) - { - std::string part = f.substr(prev_string_start); - strings_.push_back(part); - } - - return true; -} - -std::string StringInterpolatorImplementation::apply(Meter *m, DVEntry *dve) -{ - std::string result; - size_t s = 0; - size_t f = 0; - - for (;;) - { - if (s >= strings_.size() && f >= formulas_.size()) break; - - if (s < strings_.size()) - { - result += strings_[s]; - s++; - } - - if (f < formulas_.size()) - { - Formula *ff = formulas_[f].get(); - if (dve != NULL) - { - Unit u = ff->siUnit().asUnit(Quantity::PointInTime); - if (u == Unit::UnixTimestamp) - { - double pt = ff->calculate(Unit::UnixTimestamp, dve, m); - // Print as DateLT - result += strdate(pt); - } - else - { - result += tostrprintf("%g", ff->calculate(Unit::COUNTER, dve, m)); - } - } - else - { - result += "{NULL}"; - } - f++; - } - } - - return result; -} diff --git a/components/wmbus_common/formula.h b/components/wmbus_common/formula.h deleted file mode 100644 index 72356604..00000000 --- a/components/wmbus_common/formula.h +++ /dev/null @@ -1,82 +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 . -*/ - -#ifndef FORMULA_H -#define FORMULA_H - -#include -#include - -#include"units.h" - -struct Meter; -struct FieldInfo; -struct DVEntry; - -struct Formula -{ - // Parse and return false if parse failed. - virtual bool parse(Meter *m, const std::string &f) = 0; - // Returns false if the parse has failed and the formula is invalid. - virtual bool valid() = 0; - // Returns lines of error messages. - virtual std::string errors() = 0; - // Calculate the formula. Returns nan if it could not be calculated. - // If a dventry is supplied, then the values storage_counter, tariff_counter, subunit_counter are available. - // If a meter is supplied it overrides the meter supplied when parsing. - virtual double calculate(Unit to, DVEntry *dve = NULL, Meter *m = NULL) = 0; - // Clear the formula, ie drop any parsed tree. - virtual void clear() = 0; - // Return a regenerated formula std::string. - virtual std::string str() = 0; - // Return the formula in a format where the tree structure is explicit. - virtual std::string tree() = 0; - // Return the final type of the formula calculation. - virtual SIUnit &siUnit() = 0; - // Specify which meter to read the meter fields from. - virtual void setMeter(Meter *m) = 0; - // Specify which dventry to read counter fields from. - virtual void setDVEntry(DVEntry *dve) = 0; - - virtual ~Formula() = 0; -}; - -Formula *newFormula(); - -struct StringInterpolator -{ - /** - parse: Parse a field name like: "historic_{storage_counter / - 12 counter}_value" - @f: String to parse and find {...} subformulas inside. - - Parse and prepare it for being applied to an actual dve. - E.g. for a dventry with storage 13 the formula above will generate "historic_1_value" - */ - virtual bool parse(Meter *m, const std::string &f) = 0; - /** - apply: Insert inline formulas in field names like "total_{subunit_nr}" insert the result of the formula. - @dve: A pointer to a telegram entry to use (ie containing storagenr/tariffnr/subunitnr etc.) Can be NULL. - - */ - virtual std::string apply(Meter *m, DVEntry *dve) = 0; - - virtual ~StringInterpolator() = 0; -}; - -StringInterpolator *newStringInterpolator(); - -#endif diff --git a/components/wmbus_common/formula_implementation.h b/components/wmbus_common/formula_implementation.h deleted file mode 100644 index 174d5ee3..00000000 --- a/components/wmbus_common/formula_implementation.h +++ /dev/null @@ -1,334 +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 . -*/ - -#ifndef FORMULA_IMPLEMENTATION -#define FORMULA_IMPLEMENTATION - -#include"formula.h" -#include"meters.h" - -#include - -struct FormulaImplementation; - -struct NumericFormula -{ - NumericFormula(FormulaImplementation *f, SIUnit u) : formula_(f), siunit_(u) { } - SIUnit &siunit() { return siunit_; } - // Calculate the formula and return the value in the given "to" unit. - virtual double calculate(SIUnit to) = 0; - virtual std::string str() = 0; - virtual std::string tree() = 0; - virtual ~NumericFormula() = 0; - - FormulaImplementation *formula() { return formula_; } - - private: - - FormulaImplementation *formula_; - SIUnit siunit_; -}; - -struct NumericFormulaConstant : public NumericFormula -{ - NumericFormulaConstant(FormulaImplementation *f, Unit u, double c) : NumericFormula(f, u), constant_(c) {} - double calculate(SIUnit to); - std::string str(); - std::string tree(); - ~NumericFormulaConstant(); - - private: - - double constant_; -}; - -struct NumericFormulaMeterField : public NumericFormula -{ - NumericFormulaMeterField(FormulaImplementation *f, Unit u, std::string v, Quantity q) - : NumericFormula(f, u), vname_(v), quantity_(q) {} - - double calculate(SIUnit to); - std::string str(); - std::string tree(); - ~NumericFormulaMeterField(); - - private: - - std::string vname_; - Quantity quantity_; -}; - -struct NumericFormulaDVEntryField : public NumericFormula -{ - NumericFormulaDVEntryField(FormulaImplementation *f, Unit u, DVEntryCounterType ct) : NumericFormula(f, u), counter_(ct) {} - - double calculate(SIUnit to); - std::string str(); - std::string tree(); - ~NumericFormulaDVEntryField(); - - private: - - DVEntryCounterType counter_; -}; - -struct NumericFormulaPair : public NumericFormula -{ - NumericFormulaPair(FormulaImplementation *f, SIUnit siu, - std::unique_ptr &a, - std::unique_ptr &b, - std::string name, std::string op) - : NumericFormula(f, siu), - left_(std::move(a)), - right_(std::move(b)), - name_(name), - op_(op) - {} - - std::string str(); - std::string tree(); - ~NumericFormulaPair(); - -protected: - - std::unique_ptr left_; - std::unique_ptr right_; - std::string name_; - std::string op_; -}; - -struct NumericFormulaAddition : public NumericFormulaPair -{ - NumericFormulaAddition(FormulaImplementation *f, SIUnit siu, - std::unique_ptr &a, - std::unique_ptr &b) - : NumericFormulaPair(f, siu, a, b, "ADD", "+") {} - - double calculate(SIUnit to); - - ~NumericFormulaAddition(); -}; - -struct NumericFormulaSubtraction : public NumericFormulaPair -{ - NumericFormulaSubtraction(FormulaImplementation *f, SIUnit siu, - std::unique_ptr &a, - std::unique_ptr &b) - : NumericFormulaPair(f, siu, a, b, "SUB", "-") {} - - double calculate(SIUnit to); - - ~NumericFormulaSubtraction(); -}; - -struct NumericFormulaMultiplication : public NumericFormulaPair -{ - NumericFormulaMultiplication(FormulaImplementation *f, SIUnit siu, - std::unique_ptr &a, - std::unique_ptr &b) - : NumericFormulaPair(f, siu, a, b, "TIMES", "×") {} - - double calculate(SIUnit to); - - ~NumericFormulaMultiplication(); -}; - -struct NumericFormulaDivision : public NumericFormulaPair -{ - NumericFormulaDivision(FormulaImplementation *f, SIUnit siu, - std::unique_ptr &a, - std::unique_ptr &b) - : NumericFormulaPair(f, siu, a, b, "DIV", "÷") {} - - double calculate(SIUnit to); - - ~NumericFormulaDivision(); -}; - -struct NumericFormulaExponentiation : public NumericFormulaPair -{ - NumericFormulaExponentiation(FormulaImplementation *f, SIUnit siu, - std::unique_ptr &a, - std::unique_ptr &b) - : NumericFormulaPair(f, siu, a, b, "EXP", "^") {} - - double calculate(SIUnit to); - - ~NumericFormulaExponentiation(); -}; - -struct NumericFormulaSquareRoot : public NumericFormula -{ - NumericFormulaSquareRoot(FormulaImplementation *f, SIUnit siu, - std::unique_ptr &inner) - : NumericFormula(f, siu), inner_(std::move(inner)) {} - - double calculate(SIUnit to); - std::string str(); - std::string tree(); - - ~NumericFormulaSquareRoot(); - -private: - - std::unique_ptr inner_; -}; - -enum class TokenType -{ - SPACE, - LPAR, - RPAR, - NUMBER, - DATETIME, - TIME, - PLUS, - MINUS, - TIMES, - DIV, - EXP, - SQRT, - UNIT, - FIELD -}; - -const char *toString(TokenType tt); - -struct Token -{ - Token(TokenType t, size_t s, size_t l) : type(t), start(s), len(l) {} - - TokenType type; - size_t start; - size_t len; - - std::string str(const std::string &s); - std::string vals(const std::string &s); - double val(const std::string &s); - Unit unit(const std::string &s); - - std::string withMarker(const std::string &s); -}; - -struct FormulaImplementation : public Formula -{ - bool parse(Meter *m, const std::string &f); - bool valid(); - std::string errors(); - double calculate(Unit to, DVEntry *dve = NULL, Meter *m = NULL); - void clear(); - std::string str(); - std::string tree(); - SIUnit &siUnit(); - void setMeter(Meter *m); - void setDVEntry(DVEntry *dve); - - // Pushes a constant on the formula builder stack. - void doConstant(Unit u, double c); - // Pushes a meter field read on the formula builder stack. - void doMeterField(Unit u, FieldInfo *fi); - // Pushes a dve entry field read on the formula builder stack. - void doDVEntryField(Unit u, DVEntryCounterType ct); - // Pops the two top nodes of the formula builder stack and pushes an addition on the formula builder stack. - // The target unit will be the supplied to unit. - void doAddition(const SIUnit &to); - // Pops the two top nodes of the formula builder stack and pushes a subtraction on the formula builder stack. - // The target unit will be the supplied to unit. - void doSubtraction(const SIUnit &to); - // Pops the two top nodes of the formula builder stack and pushes a multiplication on the formula builder stack. - // The target unit will be multiplication of the SI Units. - void doMultiplication(); - // Pops the two top nodes of the formula builder stack and pushes a division on the formula builder stack. - // The target unit will be first SIUnit divided by the second SIUnit. - void doDivision(); - // Pops the two top nodes of the formula builder stack and pushes an exponentiation on the formula builder stack. - // The target unit will be first SIUnit exponentiated. - void doExponentiation(); - // Pops the single top node of the formula builder stack and pushes an squareroot on the formula builder stack. - // The target unit will be SIUnit square rooted. - void doSquareRoot(); - - ~FormulaImplementation(); - - bool tokenize(); - bool go(); - size_t findSpace(size_t i); - size_t findNumber(size_t i); - size_t findDateTime(size_t i); - size_t findTime(size_t i); - size_t findUnit(size_t i); - size_t findPlus(size_t i); - size_t findMinus(size_t i); - size_t findTimes(size_t i); - size_t findDiv(size_t i); - size_t findExp(size_t i); - size_t findSqrt(size_t i); - size_t findLPar(size_t i); - size_t findRPar(size_t i); - size_t findField(size_t i); - - Token *LA(size_t i); - size_t parseOps(size_t i); - size_t parsePar(size_t i); - - void handleConstant(Token *number, Token *unit); - void handleSeconds(Token *number); - void handleUnixTimestamp(Token *number); - void handleAddition(Token *add); - void handleSubtraction(Token *add); - void handleMultiplication(Token *add); - void handleDivision(Token *add); - void handleExponentiation(Token *add); - void handleSquareRoot(Token *add); - void handleField(Token *field); - - void pushOp(NumericFormula *nf); - std::unique_ptr popOp(); - NumericFormula *topOp(); - NumericFormula *top2Op(); - - Meter *meter() { return meter_; } - DVEntry *dventry() { return dventry_; } - -private: - - bool valid_ = true; - std::vector> op_stack_; - std::vector tokens_; - std::string formula_; // To be parsed. - Meter *meter_; // To be referenced when parsing and calculating. - DVEntry *dventry_; // To be referenced when calculating. - - // Any errors during parsing are store here. - std::vector errors_; -}; - -struct StringInterpolatorImplementation : public StringInterpolator -{ - // Create a std::string interpolation from for example: "historic_{storage_counter / - 12 counter}_value" - // Which for a dventry with storage 13 will "generate historic_1_value" - bool parse(Meter *m, const std::string &f); - std::string apply(Meter *m, DVEntry *dve); - ~StringInterpolatorImplementation(); - - // The std::strings store "historic_" "_value" - std::vector strings_; - // The formula stores the parsed "storage_counter / - 12 counter" formula. - std::vector> formulas_; -}; - -#endif diff --git a/components/wmbus_common/ixml.cpp b/components/wmbus_common/ixml.cpp new file mode 100644 index 00000000..fa32ed4e --- /dev/null +++ b/components/wmbus_common/ixml.cpp @@ -0,0 +1,160 @@ +#include "ixml.h" + +namespace wmbus { + +namespace { + +// Backtracking, longest repetition first. No recursion limits or index bounds: +// a grammar is part of a driver, not data off the radio. +constexpr size_t NO_MATCH = static_cast(-1); + +// The bytes no deleted term consumed; `start` is NO_MATCH until the first such term. +struct Content { + size_t start = NO_MATCH; + size_t end = 0; + + size_t from(size_t fallback) const { return start == NO_MATCH ? fallback : start; } +}; + +struct SequenceMatch { + size_t end; + Content content; +}; + +struct Matcher { + const IxmlGrammar &grammar; + std::span input; + std::vector &matches; + + Matcher(const IxmlGrammar &g, std::span in, std::vector &out) + : grammar(g), input(in), matches(out) {} + + size_t match_pattern(const IxmlTerm &term, size_t position) const { + if (position + term.length > input.size()) + return NO_MATCH; + + for (uint16_t i = 0; i < term.length; ++i) { + const IxmlBytePattern &pattern = grammar.patterns[term.arg + i]; + const uint8_t byte = input[position + i]; + if ((pattern.high & (1u << (byte >> 4))) == 0) + return NO_MATCH; + if ((pattern.low & (1u << (byte & 0x0F))) == 0) + return NO_MATCH; + } + return position + term.length; + } + + std::span key_of(const IxmlRule &rule) const { + return grammar.keys.subspan(rule.first_key_byte, rule.key_length); + } + + size_t match_once(const IxmlTerm &term, size_t position) { + if (term.kind == IxmlTermKind::Pattern) + return match_pattern(term, position); + return match_rule(term.arg, position); + } + + SequenceMatch match_sequence(const IxmlAlternative &alternative, uint16_t term_index, size_t position, + Content content) { + if (term_index == alternative.term_count) + return {position, content}; + + const IxmlTerm &term = grammar.terms[alternative.first_term + term_index]; + const size_t emitted = matches.size(); + + const bool many = term.repeat == IxmlRepeat::Star || term.repeat == IxmlRepeat::Plus; + const size_t min_repeats = (term.repeat == IxmlRepeat::Once || term.repeat == IxmlRepeat::Plus) ? 1 : 0; + + size_t available = 0; + size_t at = position; + while (many || available == 0) { + const size_t next = match_once(term, at); + if (next == NO_MATCH) + break; + ++available; + // A rule may match the empty string; repeating it would not move forward. + if (next == at) + break; + at = next; + } + + for (size_t taken = available + 1; taken-- > min_repeats;) { + // Drop what the longer attempts emitted. + matches.resize(emitted); + + Content taken_content = content; + size_t cursor = position; + for (size_t i = 0; i < taken; ++i) + cursor = match_once(term, cursor); + + if (taken > 0 && !term.deleted) { + taken_content.start = taken_content.from(position); + taken_content.end = cursor; + } + + const SequenceMatch rest = match_sequence(alternative, term_index + 1, cursor, taken_content); + if (rest.end != NO_MATCH) + return rest; + } + + matches.resize(emitted); + return {NO_MATCH, content}; + } + + // A later alternative may be the one that covers the whole input. + bool match_whole_input(uint16_t rule_index) { + const IxmlRule &rule = grammar.rules[rule_index]; + for (uint16_t i = 0; i < rule.alternative_count; ++i) { + matches.clear(); + + const SequenceMatch m = match_sequence(grammar.alternatives[rule.first_alternative + i], 0, 0, {}); + if (m.end != input.size()) + continue; + + if (rule.key_length != 0) { + const size_t from = m.content.from(0); + matches.push_back(IxmlMatch{key_of(rule), input.subspan(from, m.content.end - from)}); + } + return true; + } + + matches.clear(); + return false; + } + + size_t match_rule(uint16_t rule_index, size_t position) { + const IxmlRule &rule = grammar.rules[rule_index]; + const size_t emitted = matches.size(); + + for (uint16_t i = 0; i < rule.alternative_count; ++i) { + const IxmlAlternative &alternative = grammar.alternatives[rule.first_alternative + i]; + + const SequenceMatch m = match_sequence(alternative, 0, position, {NO_MATCH, position}); + if (m.end == NO_MATCH) { + matches.resize(emitted); + continue; + } + + if (rule.key_length != 0) { + const size_t from = m.content.from(position); + matches.push_back(IxmlMatch{key_of(rule), input.subspan(from, m.content.end - from)}); + } + return m.end; + } + + matches.resize(emitted); + return NO_MATCH; + } +}; + +} // namespace + +std::optional> IxmlGrammar::decode(std::span bytes) const { + std::vector collected; + Matcher matcher(*this, bytes, collected); + if (!matcher.match_whole_input(0)) + return {}; + return collected; +} + +} // namespace wmbus diff --git a/components/wmbus_common/ixml.h b/components/wmbus_common/ixml.h new file mode 100644 index 00000000..5558ebb8 --- /dev/null +++ b/components/wmbus_common/ixml.h @@ -0,0 +1,60 @@ +#pragma once + +#include +#include +#include +#include + +namespace wmbus { + +enum class IxmlTermKind : uint8_t { Pattern, Rule }; + +enum class IxmlRepeat : uint8_t { Once, Star, Plus, Optional }; + +// Every value one half of a byte may take. +constexpr uint16_t IXML_ANY = 0xFFFF; + +// A bit per value the high and the low nibble may take. +struct IxmlBytePattern { + uint16_t high; + uint16_t low; +}; + +struct IxmlTerm { + IxmlTermKind kind; + IxmlRepeat repeat; + bool deleted; // a deleted term does not enter the node's content + uint8_t length; // pattern bytes; 0 for a rule + uint16_t arg; // the pattern's first byte, or the index of the rule called +}; + +struct IxmlAlternative { + uint16_t first_term; + uint16_t term_count; +}; + +struct IxmlRule { + uint16_t first_alternative; + uint16_t alternative_count; + uint16_t first_key_byte; + uint16_t key_length; // 0 when the rule carries no DIF/VIF key +}; + +struct IxmlMatch { + std::span key; + std::span data; +}; + +struct IxmlGrammar { + // Rule 0 is the start rule. + std::span rules; + std::span alternatives; + std::span terms; + std::span patterns; + std::span keys; + + // Nothing unless the start rule covers the whole input; empty when no key was recognised. + std::optional> decode(std::span bytes) const; +}; + +} // namespace wmbus diff --git a/components/wmbus_common/manufacturer_specificities.cpp b/components/wmbus_common/manufacturer_specificities.cpp deleted file mode 100644 index 29485275..00000000 --- a/components/wmbus_common/manufacturer_specificities.cpp +++ /dev/null @@ -1,375 +0,0 @@ -/* - Copyright (C) 2019 Jacek Tomasiak (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 -#include"manufacturers.h" -#include"manufacturer_specificities.h" -#include"meters.h" - -std::set diehl_manufacturers = { - MANUFACTURER_DME, - MANUFACTURER_EWT, - MANUFACTURER_HYD, - MANUFACTURER_SAP, - MANUFACTURER_SPL -}; - -// Default keys for Izar/PRIOS and Sharky meters -#define PRIOS_DEFAULT_KEY1 "39BC8A10E66D83F8" -#define PRIOS_DEFAULT_KEY2 "51728910E66D83F8" - -// Diehl: Is "A field" coded differently from standard? -DiehlAddressTransformMethod mustTransformDiehlAddress(DiehlFrameInterpretation interpretation) -{ - switch (interpretation) { - case DiehlFrameInterpretation::PRIOS: - case DiehlFrameInterpretation::PRIOS_SCR: - case DiehlFrameInterpretation::REAL_DATA: - return DiehlAddressTransformMethod::SWAPPING; - - case DiehlFrameInterpretation::SAP_PRIOS: - return DiehlAddressTransformMethod::SAP_PRIOS; - - case DiehlFrameInterpretation::SAP_PRIOS_STD: - return DiehlAddressTransformMethod::SAP_PRIOS_STANDARD; - - case DiehlFrameInterpretation::RESERVED: - case DiehlFrameInterpretation::OMS: - case DiehlFrameInterpretation::NA: - default: - return DiehlAddressTransformMethod::NONE; - } -} - -// Diehl: Determines how to interpret frame -DiehlFrameInterpretation detectDiehlFrameInterpretation(uchar c_field, int m_field, uchar ci_field, int tpl_cfg) -{ - if (diehl_manufacturers.find(m_field) != diehl_manufacturers.end()) - { - if (c_field == 0x44 || c_field == 0x46) // SND_NR / SND_IR - { - switch (ci_field) { - case 0x71: // Alarm - return DiehlFrameInterpretation::REAL_DATA; - case 0x7A: // EN 13757-3 Application Layer (short tplh) - if (((tpl_cfg >> 8) & 0x10) == 0x10) // Bit 12 from MMMMM bits of CFG field - { - return DiehlFrameInterpretation::REAL_DATA; - } - return DiehlFrameInterpretation::OMS; - case 0xA0: // Manufacturer specific - case 0xA1: // Manufacturer specific - case 0xA2: // Manufacturer specific - case 0xA3: // Manufacturer specific - case 0xA4: // Manufacturer specific - case 0xA5: // Manufacturer specific - case 0xA6: // Manufacturer specific - case 0xA7: // Manufacturer specific - if (m_field == MANUFACTURER_SAP) - { - return DiehlFrameInterpretation::SAP_PRIOS; - } - return DiehlFrameInterpretation::PRIOS; - case 0xB0: // Manufacturer specific - if (m_field == MANUFACTURER_SAP) - { - return DiehlFrameInterpretation::SAP_PRIOS_STD; - } - return DiehlFrameInterpretation::RESERVED; - case 0xA8: // Manufacturer specific - case 0xA9: // Manufacturer specific - case 0xAA: // Manufacturer specific - case 0xAB: // Manufacturer specific - case 0xAC: // Manufacturer specific - case 0xAD: // Manufacturer specific - case 0xAE: // Manufacturer specific - case 0xAF: // Manufacturer specific - case 0xB4: // Manufacturer specific - case 0xB5: // Manufacturer specific - case 0xB6: // Manufacturer specific - case 0xB7: // Manufacturer specific - return DiehlFrameInterpretation::RESERVED; - case 0xB1: // Manufacturer specific - case 0xB2: // Manufacturer specific - case 0xB3: // Manufacturer specific - return DiehlFrameInterpretation::PRIOS_SCR; - default: - return DiehlFrameInterpretation::OMS; - } - } - } - return DiehlFrameInterpretation::NA; -} - -// Diehl: Determines how to interpret frame -DiehlFrameInterpretation detectDiehlFrameInterpretation(const std::vector& frame) -{ - if (frame.size() < 15) - return DiehlFrameInterpretation::NA; - - uchar c_field = frame[1]; - int m_field = frame[3] <<8 | frame[2]; - uchar ci_field = frame[10]; - int tpl_cfg = frame[14] <<8 | frame[13]; - return detectDiehlFrameInterpretation(c_field, m_field, ci_field, tpl_cfg); -} - -// Diehl: Is "A field" coded differently from standard? -DiehlAddressTransformMethod mustTransformDiehlAddress(const std::vector& frame) -{ - return mustTransformDiehlAddress(detectDiehlFrameInterpretation(frame)); -} - -// Diehl: transform "A field" to make it compliant to standard -void transformDiehlAddress (std::vector& frame, DiehlAddressTransformMethod transform_method) -{ - if (transform_method == DiehlAddressTransformMethod::SWAPPING) - { - debug("(diehl) Pre-processing: swapping address field\n"); - uchar version = frame[4]; - uchar type = frame[5]; - for (int i = 4; i < 8; i++) - { - frame[i] = frame[i+2]; - } - frame[8] = version; - frame[9] = type; - } - else if (transform_method == DiehlAddressTransformMethod::SAP_PRIOS) - { - debug("(diehl) Pre-processing: setting device type to water meter for SAP PRIOS\n"); - frame[8] = 0x00; // version field is used by IZAR as part of meter id on 5 bytes instead of 4 - frame[9] = 0x07; // water meter - } - else if (transform_method == DiehlAddressTransformMethod::SAP_PRIOS_STANDARD) - { - warning("(diehl) Pre-processing: SAP PRIOS STANDARD transformation not implemented!\n"); // TODO - } -} - -// Diehl: decode LFSR encrypted data used in Izar/PRIOS and Sharky meters -std::vector decodeDiehlLfsr(const std::vector &origin, const std::vector &frame, uint32_t key, DiehlLfsrCheckMethod check_method, uint32_t check_value) -{ - // modify seed key with header values - key ^= uint32FromBytes(origin, 2); // manufacturer + address[0-1] - key ^= uint32FromBytes(origin, 6); // address[2-3] + version + type - key ^= uint32FromBytes(frame, 10); // ci + some more bytes from the telegram... - - int size = frame.size() - 15; - std::vector decoded(size); - - for (int i = 0; i < size; ++i) { - // calculate new key (LFSR) - // https://en.wikipedia.org/wiki/Linear-feedback_shift_register - for (int j = 0; j < 8; ++j) { - // calculate new bit value (xor of selected bits from previous key) - uchar bit = ((key & 0x2) != 0) ^ ((key & 0x4) != 0) ^ ((key & 0x800) != 0) ^ ((key & 0x80000000) != 0); - // shift key bits and add new one at the end - key = (key << 1) | bit; - } - // decode i-th content byte with fresh/last 8-bits of key - decoded[i] = frame[i + 15] ^ (key & 0xFF); - - if (check_method == DiehlLfsrCheckMethod::HEADER_1_BYTE) - { - // check-byte doesn't match? - if (decoded[0] != 0x4B) { - decoded.clear(); - return decoded; - } - } - else if (check_method == DiehlLfsrCheckMethod::CHECKSUM_AND_0XEF) - { - if (i == size - 1) { - uint32_t checksum = 0; - for (int index = 0; index < size; index++) { - checksum += (decoded[index] & 0xFF); - } - if ((checksum & 0xEF) != check_value) { - decoded.clear(); - return decoded; - } - } - } - } - - return decoded; -} - -uint32_t uint32FromBytes(const std::vector &data, int offset, bool reverse) -{ - if (reverse) - return ((uint32_t)data[offset + 3] << 24) | - ((uint32_t)data[offset + 2] << 16) | - ((uint32_t)data[offset + 1] << 8) | - (uint32_t)data[offset]; - else - return ((uint32_t)data[offset] << 24) | - ((uint32_t)data[offset + 1] << 16) | - ((uint32_t)data[offset + 2] << 8) | - (uint32_t)data[offset + 3]; -} - -uint32_t convertKey(const std::vector &bytes) -{ - uint32_t key1 = uint32FromBytes(bytes, 0); - uint32_t key2 = uint32FromBytes(bytes, 4); - uint32_t key = key1 ^ key2; - return key; -} - -uint32_t convertKey(const char *hex) -{ - std::vector bytes; - hex2bin(hex, &bytes); - return convertKey(bytes); -} - -// Common: add default manufacturers key if none specified and we know one for the given frame -void addDefaultManufacturerKeyIfAny(const std::vector &frame, TPLSecurityMode tpl_sec_mode, MeterKeys *meter_keys) -{ - if (!meter_keys->hasConfidentialityKey() - && tpl_sec_mode == TPLSecurityMode::AES_CBC_IV - && detectDiehlFrameInterpretation(frame) == DiehlFrameInterpretation::OMS) - { - std::vector half; - hex2bin(PRIOS_DEFAULT_KEY2, &half); - meter_keys->confidentiality_key = std::vector(half.begin(), half.end()); - meter_keys->confidentiality_key.insert(meter_keys->confidentiality_key.end(), half.begin(), half.end()); - debug("(mfct) added default key\n"); - } -} - -// Diehl: initialize support of default keys in a meter -void initializeDiehlDefaultKeySupport(const std::vector &confidentiality_key, std::vector& keys) -{ - if (!confidentiality_key.empty()) - keys.push_back(convertKey(confidentiality_key)); - - // fallback to default keys if no custom key provided - if (keys.empty()) - { - keys.push_back(convertKey(PRIOS_DEFAULT_KEY1)); - keys.push_back(convertKey(PRIOS_DEFAULT_KEY2)); - } -} - -// Diehl: Is payload real data crypted (LFSR)? -bool mustDecryptDiehlRealData(const std::vector& frame) -{ - DiehlFrameInterpretation fi = detectDiehlFrameInterpretation(frame); - debug("(diehl) frame %s\n", toString(fi)); - return fi == DiehlFrameInterpretation::REAL_DATA; -} - -// Diehl: decrypt real data payload (LFSR) -bool decryptDielhRealData(Telegram *t, std::vector &frame, std::vector::iterator &pos, const std::vector &confidentiality_key) -{ - std::vector keys; - initializeDiehlDefaultKeySupport(confidentiality_key, keys); - - std::vector decoded_content; - for (auto& key : keys) { - decoded_content = decodeDiehlLfsr(t->original.empty() ? frame : t->original, frame, key, DiehlLfsrCheckMethod::CHECKSUM_AND_0XEF, frame[14] & 0xEF); - if (!decoded_content.empty()) - break; - } - - if (decoded_content.empty()) - { - if (!t->isSimulated() && t->parserWarns()) - { - // Do not warn when decryption is not needed since we are running from a simulated file. - // Or if the telegram cannot be decrypted when listening to all. - warning("(diehl) Decoding LFSR real data failed.\n"); - } - else - { - if (t->isSimulated()) - { - debug("(diehl) Decoding LFSR real data failed, probably already decoded since telegram is simulated.\n"); - } - else - { - debug("(diehl) Decoding LFSR real data failed.\n"); - } - } - return false; - } - - debug("(diehl) Decoded LFSR real data: %s\n", bin2hex(decoded_content).c_str()); - - frame.erase(pos, frame.end()); - frame.insert(frame.end(), decoded_content.begin(), decoded_content.end()); - - return true; -} - -const char *toString(DiehlFrameInterpretation i) -{ - switch (i) - { - case DiehlFrameInterpretation::NA: return "N/A"; - case DiehlFrameInterpretation::REAL_DATA: return "REAL_DATA"; - case DiehlFrameInterpretation::OMS: return "OMS"; - case DiehlFrameInterpretation::PRIOS: return "PRIOS"; - case DiehlFrameInterpretation::SAP_PRIOS: return "SAP_PRIOS"; - case DiehlFrameInterpretation::SAP_PRIOS_STD: return "SAP_PRIOS_STD"; - case DiehlFrameInterpretation::PRIOS_SCR: return "PRIOS_SCR"; - case DiehlFrameInterpretation::RESERVED: return "RESERVED"; - } - return "?"; -} - -const char *toString(DiehlAddressTransformMethod m) -{ - switch (m) - { - case DiehlAddressTransformMethod::NONE: return "NONE"; - case DiehlAddressTransformMethod::SWAPPING: return "SWAPPING"; - case DiehlAddressTransformMethod::SAP_PRIOS: return "SAP_PRIOS"; - case DiehlAddressTransformMethod::SAP_PRIOS_STANDARD: return "SAP_PRIOS_STANDARD"; - } - return "?"; -} - -void qdsExtractWalkByField(Telegram *t, Meter *driver, DVEntry &mfctEntry, int pos, int n, const std::string &key_s, const std::string &fieldName, Quantity quantity) { - std::string bytes = mfctEntry.value.substr(pos, n); - - DifVifKey key(key_s); - DVEntry fieldEntry(0, - key, - MeasurementType::Instantaneous, - key.vif(), - std::set(), - std::set(), - AnyStorageNr, - AnyTariffNr, - SubUnitNr(0), - bytes); - - FieldInfo *fieldInfo = driver->findFieldInfo(fieldName, quantity); - if (fieldInfo == nullptr) { - error("(qds) field info not found: %s\n", fieldName.c_str()); - } - - fieldInfo->performExtraction(driver, t, &fieldEntry); - std::string info = "*** " + bytes + " (" + fieldInfo->renderJson(driver, &fieldEntry) + ")"; - - t->addSpecialExplanation(mfctEntry.offset + pos / 2, n / 2, KindOfData::CONTENT, Understanding::FULL, info.c_str()); -} diff --git a/components/wmbus_common/manufacturer_specificities.h b/components/wmbus_common/manufacturer_specificities.h deleted file mode 100644 index 7058c5f3..00000000 --- a/components/wmbus_common/manufacturer_specificities.h +++ /dev/null @@ -1,85 +0,0 @@ -/* - Copyright (C) 2019 Jacek Tomasiak (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 . -*/ - -#ifndef MANUFACTURER_SPECIFICITIES_H -#define MANUFACTURER_SPECIFICITIES_H - -#include"util.h" -#include"wmbus.h" - -using namespace std; - -// Common: add default manufacturers key if none specified and we know one for the given frame -void addDefaultManufacturerKeyIfAny(const std::vector &frame, TPLSecurityMode tpl_sec_mode, MeterKeys *meter_keys); - -uint32_t uint32FromBytes(const std::vector &data, int offset, bool reverse = false); - -// Diehl: initialize support of default keys in a meter -void initializeDiehlDefaultKeySupport(const std::vector &confidentiality_key, std::vector& keys); - -// Diehl: check method of LFSR decryption algorithm -enum class DiehlLfsrCheckMethod { - CHECKSUM_AND_0XEF, - HEADER_1_BYTE -}; - -// Diehl: decode LFSR encrypted data used in Izar/PRIOS and Sharky meters -std::vector decodeDiehlLfsr(const std::vector &origin, const std::vector &frame, uint32_t key, DiehlLfsrCheckMethod check_method, uint32_t check_value); - -// Diehl: frame interpretation -enum class DiehlFrameInterpretation { - NA, // N/A: not a Diehl frame - REAL_DATA, - OMS, - PRIOS, - SAP_PRIOS, - SAP_PRIOS_STD, - PRIOS_SCR, - RESERVED -}; - -const char *toString(DiehlFrameInterpretation i); - -// Diehl: address transformation method -enum class DiehlAddressTransformMethod { - NONE, // "A field" coded as per standard - SWAPPING, // "A field" coded as version/type/serialnumber instead of standard serialnumber/version/type - SAP_PRIOS, // Version and type not included in telegram. Must be hardcoded to 0 and 7 - SAP_PRIOS_STANDARD // ? -}; - -const char *toString(DiehlAddressTransformMethod m); - -// Diehl: Determines how to interpret frame -DiehlFrameInterpretation detectDiehlFrameInterpretation(const std::vector& frame); - -// Diehl: Is "A field" coded differently from standard? -DiehlAddressTransformMethod mustTransformDiehlAddress(const std::vector& frame); - -// Diehl: transform "A field" to make it compliant to standard -void transformDiehlAddress (std::vector& frame, DiehlAddressTransformMethod method); - -// Diehl: Is payload real data crypted (LFSR)? -bool mustDecryptDiehlRealData(const std::vector& frame); - -// Diehl: decrypt real data payload (LFSR) -bool decryptDielhRealData(Telegram *t, std::vector &frame, std::vector::iterator &pos, const std::vector &meterkey); - -void qdsExtractWalkByField(Telegram *t, Meter *driver, DVEntry &mfctEntry, int pos, int n, const std::string &key_s, const std::string &fieldName, Quantity quantity); - -#endif diff --git a/components/wmbus_common/manufacturers.h b/components/wmbus_common/manufacturers.h deleted file mode 100644 index 7bf6ce97..00000000 --- a/components/wmbus_common/manufacturers.h +++ /dev/null @@ -1,3088 +0,0 @@ -// Copyright (C) 2025 Fredrik Öhrström (CC0) -#ifndef MANUFACTURERS_H -#define MANUFACTURERS_H -#define MANFCODE(a,b,c) ((a-64)*1024+(b-64)*32+(c-64)) -#define LIST_OF_MANUFACTURERS \ -X(AAA,MANFCODE('A','A','A'),"Aventies, Germany")\ -X(AAB,MANFCODE('A','A','B'),"Milas")\ -X(ABB,MANFCODE('A','B','B'),"ABB, Sweden")\ -X(ABC,MANFCODE('A','B','C'),"Paya Energy")\ -X(ABN,MANFCODE('A','B','N'),"ABN Braun, Germany")\ -X(ABR,MANFCODE('A','B','R'),"ABB, Czech Republic")\ -X(ACA,MANFCODE('A','C','A'),"Acean, France")\ -X(ACB,MANFCODE('A','C','B'),"AcBel Polytech, Taiwan")\ -X(ACC,MANFCODE('A','C','C'),"Accurate, Pakistan")\ -X(ACE,MANFCODE('A','C','E'),"Actaris Electricity, France")\ -X(ACG,MANFCODE('A','C','G'),"Actaris Gas, France")\ -X(ACH,MANFCODE('A','C','H'),"Acantho, Italy")\ -X(ACL,MANFCODE('A','C','L'),"Aclara Meters UK, United Kingdom")\ -X(ACN,MANFCODE('A','C','N'),"ACN Advanced Communications Networks, Switzerland")\ -X(ACS,MANFCODE('A','C','S'),"Tianjin Chuangshi Technology Developing, China")\ -X(ACT,MANFCODE('A','C','T'),"Activis Metering, Germany")\ -X(ACW,MANFCODE('A','C','W'),"Actaris Water and Heat, France")\ -X(ADA,MANFCODE('A','D','A'),"Andrea Informatique, France")\ -X(ADD,MANFCODE('A','D','D'),"ADD Production, Moldova")\ -X(ADE,MANFCODE('A','D','E'),"Yantai Aerospace Delu Energy saving Technology, China")\ -X(ADF,MANFCODE('A','D','F'),"ADFweb com, Italy")\ -X(ADN,MANFCODE('A','D','N'),"Aidon, Finland")\ -X(ADR,MANFCODE('A','D','R'),"Adrem Engineering, Romania")\ -X(ADU,MANFCODE('A','D','U'),"Adunos, Germany")\ -X(ADV,MANFCODE('A','D','V'),"Advant")\ -X(ADX,MANFCODE('A','D','X'),"ADD Production, Moldova")\ -X(AEA,MANFCODE('A','E','A'),"Abam Electronic Alborz, Iran")\ -X(AEC,MANFCODE('A','E','C'),"Advance Electronics Company, Saudi Arabia")\ -X(AEE,MANFCODE('A','E','E'),"Atlas Electronics, Serbia")\ -X(AEG,MANFCODE('A','E','G'),"AEG")\ -X(AEI,MANFCODE('A','E','I'),"Ajay Electrical Industries, India")\ -X(AEL,MANFCODE('A','E','L'),"Kohler, Turkey")\ -X(AEM,MANFCODE('A','E','M'),"S C AEM, Romania")\ -X(AEN,MANFCODE('A','E','N'),"ADVANCED ENERGY MANAGEMENT SOLUTIONS, Nigeria")\ -X(AER,MANFCODE('A','E','R'),"Aerzener Maschinenfabrik, Germany")\ -X(AES,MANFCODE('A','E','S'),"ALFANAR ELECTRICAL SYSTEM, Saudi Arabia")\ -X(AFS,MANFCODE('A','F','S'),"AFSONA INVEST, Uzbekistan")\ -X(AFX,MANFCODE('A','F','X'),"Alflex Products, Netherlands")\ -X(AGE,MANFCODE('A','G','E'),"AccessGate, Sweden")\ -X(AGI,MANFCODE('A','G','I'),"PT Armitec Electrical Systems, Indonesia")\ -X(AGT,MANFCODE('A','G','T'),"Agnitio Technologies, India")\ -X(AHV,MANFCODE('A','H','V'),"aventies, Germany")\ -X(AHX,MANFCODE('A','H','X'),"Sepanta Electronic Tabarestan, Iran")\ -X(AIC,MANFCODE('A','I','C'),"Avastu Creations, India")\ -X(AIM,MANFCODE('A','I','M'),"Ningbo Aimei Meter Manufacture, China")\ -X(AKK,MANFCODE('A','K','K'),"MAximum, Moldova")\ -X(AKU,MANFCODE('A','K','U'),"Akanksha Power and Infrastructure, India")\ -X(ALC,MANFCODE('A','L','C'),"Zenner Gas, Italy")\ -X(ALE,MANFCODE('A','L','E'),"AEROSPACE LIANGLI ELECTRIC, China")\ -X(ALF,MANFCODE('A','L','F'),"Alfatech Elektromed Elektronik, Turkey")\ -X(ALG,MANFCODE('A','L','G'),"Algodue Elettronica, Italy")\ -X(ALH,MANFCODE('A','L','H'),"ALFANAR ELECTRICAL SYSTEM, Saudi Arabia")\ -X(ALK,MANFCODE('A','L','K'),"Actislink, Poland")\ -X(ALL,MANFCODE('A','L','L'),"Allied Engineering Works, India")\ -X(ALP,MANFCODE('A','L','P'),"Alphion, India")\ -X(ALR,MANFCODE('A','L','R'),"Algorab, Italy")\ -X(ALT,MANFCODE('A','L','T'),"Amplitec, Germany")\ -X(ALV,MANFCODE('A','L','V'),"Alvicom, Hungary")\ -X(AMB,MANFCODE('A','M','B'),"Wurth Elektronik eiSos, Germany")\ -X(AMC,MANFCODE('A','M','C'),"Arch Meter Corporation, Taiwan")\ -X(AMD,MANFCODE('A','M','D'),"KEPID AMSTECH, South Korea")\ -X(AME,MANFCODE('A','M','E'),"Avon Meters, India")\ -X(AMG,MANFCODE('A','M','G'),"ANTE METER GROUP, China")\ -X(AMH,MANFCODE('A','M','H'),"AmiHo, United Kingdom")\ -X(AMI,MANFCODE('A','M','I'),"AMI Tech I, India")\ -X(AML,MANFCODE('A','M','L'),"Eon Electric, India")\ -X(AMP,MANFCODE('A','M','P'),"Ampy Automation Digilog")\ -X(AMR,MANFCODE('A','M','R'),"Actislink, Poland")\ -X(AMS,MANFCODE('A','M','S'),"Zhejiang Joy Electronic Technology, China")\ -X(AMT,MANFCODE('A','M','T'),"INTEGRA METERING")\ -X(AMX,MANFCODE('A','M','X'),"APATOR METRIX, Poland")\ -X(ANA,MANFCODE('A','N','A'),"Anacle Systems, Singapore")\ -X(AND,MANFCODE('A','N','D'),"Andis, Slovakia")\ -X(ANE,MANFCODE('A','N','E'),"AN electronic, Czech Republic")\ -X(ANZ,MANFCODE('A','N','Z'),"Anhui Nari Zenith Electricity Electronic")\ -X(AOM,MANFCODE('A','O','M'),"Aquametro Oil Marine, Switzerland")\ -X(AON,MANFCODE('A','O','N'),"ASTRON, Slovenia")\ -X(APA,MANFCODE('A','P','A'),"Apator, Poland")\ -X(APL,MANFCODE('A','P','L'),"APLI, Slovakia")\ -X(APO,MANFCODE('A','P','O'),"Apolis, Germany")\ -X(APR,MANFCODE('A','P','R'),"Apronecs, Bulgaria")\ -X(APS,MANFCODE('A','P','S'),"Apsis Kontrol Sistemleri, Turkey")\ -X(APT,MANFCODE('A','P','T'),"Apator, Poland")\ -X(APX,MANFCODE('A','P','X'),"Amplex, Denmark")\ -X(AQA,MANFCODE('A','Q','A'),"ACTAQUA, Germany")\ -X(AQL,MANFCODE('A','Q','L'),"Aqualoc, South Africa")\ -X(AQM,MANFCODE('A','Q','M'),"INTEGRA METERING, Switzerland")\ -X(AQN,MANFCODE('A','Q','N'),"Aquantia, Italy")\ -X(AQT,MANFCODE('A','Q','T'),"Aquatherm PPH, Poland")\ -X(AQU,MANFCODE('A','Q','U'),"Aquamess, Germany")\ -X(ARA,MANFCODE('A','R','A'),"Aranha, United Kingdom")\ -X(ARC,MANFCODE('A','R','C'),"Arcelik, Turkey")\ -X(ARD,MANFCODE('A','R','D'),"Arad Group, Israel")\ -X(ARF,MANFCODE('A','R','F'),"ADEUNIS RF, France")\ -X(ARG,MANFCODE('A','R','G'),"ARGO group of companies, Russian Federation")\ -X(ARH,MANFCODE('A','R','H'),"PT Armitec Electrical Systems, Indonesia")\ -X(ARK,MANFCODE('A','R','K'),"Arkon Flow Systems, Czech Republic")\ -X(ARM,MANFCODE('A','R','M'),"Arivus metering, Germany")\ -X(ARQ,MANFCODE('A','R','Q'),"ARQIVA, United Kingdom")\ -X(ARS,MANFCODE('A','R','S'),"ADD, Bulgaria")\ -X(ART,MANFCODE('A','R','T'),"Electrotecnica Arteche Smart Grid, Spain")\ -X(ARW,MANFCODE('A','R','W'),"Arrow Electronics, India")\ -X(ARX,MANFCODE('A','R','X'),"Aras Mercu Sdn Bhd, Malaysia")\ -X(ASA,MANFCODE('A','S','A'),"Asac, Italy")\ -X(ASC,MANFCODE('A','S','C'),"A2A Smart City, Italy")\ -X(ASD,MANFCODE('A','S','D'),"AS DAQ SYSTEMS, India")\ -X(ASG,MANFCODE('A','S','G'),"Meter technology Company, Saudi Arabia")\ -X(ASH,MANFCODE('A','S','H'),"Basic Electronics Company, Saudi Arabia")\ -X(ASJ,MANFCODE('A','S','J'),"ACCUMET SOLUTIONS, India")\ -X(ASM,MANFCODE('A','S','M'),"Adya Wifi Metering, India")\ -X(ASR,MANFCODE('A','S','R'),"Erelsan Elektrik ve Elektronik, Turkey")\ -X(AST,MANFCODE('A','S','T'),"ASTINCO, Canada")\ -X(ASY,MANFCODE('A','S','Y'),"Arvato Systems Digital, Germany")\ -X(ATA,MANFCODE('A','T','A'),"Alcansar OElcum ve Otomasyon Sistemleri, Turkey")\ -X(ATE,MANFCODE('A','T','E'),"ADVANCE TECHNOLOGY AND ELECTRICS COMPANY LIMTED, Thailand")\ -X(ATF,MANFCODE('A','T','F'),"AKTIF OTOMASYON STI, Turkey")\ -X(ATI,MANFCODE('A','T','I'),"Analogics Tech, India")\ -X(ATL,MANFCODE('A','T','L'),"Atlas Elektronik, Turkey")\ -X(ATM,MANFCODE('A','T','M'),"Atmel SAU, Spain")\ -X(ATS,MANFCODE('A','T','S'),"Atlas Sayac Sanayi, Turkey")\ -X(AUR,MANFCODE('A','U','R'),"Aurum Europe, Netherlands")\ -X(AUT,MANFCODE('A','U','T'),"Aiut, Poland")\ -X(AUX,MANFCODE('A','U','X'),"Ningbo Sanxing Smart Electric, China")\ -X(AVA,MANFCODE('A','V','A'),"Avangard JSC, Russian Federation")\ -X(AVE,MANFCODE('A','V','E'),"ASHTAVINAYAKA ENERGY INFRA, India")\ -X(AXI,MANFCODE('A','X','I'),"Axioma Metering UAB, Lithuania")\ -X(AXS,MANFCODE('A','X','S'),"AXSEM, Switzerland")\ -X(AYS,MANFCODE('A','Y','S'),"Euromet Stl, Turkey")\ -X(AZE,MANFCODE('A','Z','E'),"AZEL Electronics, Turkey")\ -X(BAM,MANFCODE('B','A','M'),"Bachmann, Germany")\ -X(BAR,MANFCODE('B','A','R'),"Baer Industrie Elektronik, Germany")\ -X(BAS,MANFCODE('B','A','S'),"BASIC INTELLIGENCE TECHNOLOGY, China")\ -X(BBE,MANFCODE('B','B','E'),"Bentec Bhartia Engineers, India")\ -X(BBS,MANFCODE('B','B','S'),"BBS Electronics, Singapore")\ -X(BCE,MANFCODE('B','C','E'),"ShenZhen B C Electronic, China")\ -X(BCR,MANFCODE('B','C','R'),"Jiangsu Bingchen Electronics, China")\ -X(BEA,MANFCODE('B','E','A'),"Baer Energy Automation, Germany")\ -X(BEE,MANFCODE('B','E','E'),"Bentec, India")\ -X(BEF,MANFCODE('B','E','F'),"BEFEGA, Germany")\ -X(BEG,MANFCODE('B','E','G'),"begcomm Communication, Sweden")\ -X(BEL,MANFCODE('B','E','L'),"Belassitsa AD, Bulgaria")\ -X(BEN,MANFCODE('B','E','N'),"BEENIC, Germany")\ -X(BER,MANFCODE('B','E','R'),"Bernina Electronic, Switzerland")\ -X(BFW,MANFCODE('B','F','W'),"BFW Buero fuer Waermemesstechnik OHG, Germany")\ -X(BHG,MANFCODE('B','H','G'),"Brunata, Denmark")\ -X(BIM,MANFCODE('B','I','M'),"BEAM INFOTECH, India")\ -X(BJY,MANFCODE('B','J','Y'),"Beijing JOYO smart water meter, China")\ -X(BKB,MANFCODE('B','K','B'),"Boendekomfort, Sweden")\ -X(BKO,MANFCODE('B','K','O'),"Beko Elektronik, Turkey")\ -X(BKT,MANFCODE('B','K','T'),"Bekto Precisa, Bosnia and Herzegovina")\ -X(BLM,MANFCODE('B','L','M'),"BELIMO Automation, Switzerland")\ -X(BLU,MANFCODE('B','L','U'),"Bluering, Italy")\ -X(BME,MANFCODE('B','M','E'),"Beifeng, Germany")\ -X(BMI,MANFCODE('B','M','I'),"Badger Meter, United States of America")\ -X(BMP,MANFCODE('B','M','P'),"BMETERS Polska, Poland")\ -X(BMT,MANFCODE('B','M','T'),"BMETERS, Italy")\ -X(BNR,MANFCODE('B','N','R'),"Beijing Banner Electric, China")\ -X(BOV,MANFCODE('B','O','V'),"JiaXing Bove Intelligent Technology, China")\ -X(BPM,MANFCODE('B','P','M'),"Aadharshila Mobility Solutions, India")\ -X(BRA,MANFCODE('B','R','A'),"Brandes, Germany")\ -X(BRC,MANFCODE('B','R','C'),"BRICK4U, Germany")\ -X(BRN,MANFCODE('B','R','N'),"Bruntab Energy, Iran")\ -X(BRT,MANFCODE('B','R','T'),"BEREKET TEKNOLOJIK OELCUM SISTEMLERI BIL ELK INS ITH IHR SAN VE TIC STI, Turkey")\ -X(BRU,MANFCODE('B','R','U'),"Klaus Bruchmann, Germany")\ -X(BSC,MANFCODE('B','S','C'),"Sanaye Sanjesh Energy Behineh Sazan Toos, Iran")\ -X(BSD,MANFCODE('B','S','D'),"BSED, Germany")\ -X(BSE,MANFCODE('B','S','E'),"Basari Elektronik, Turkey")\ -X(BSI,MANFCODE('B','S','I'),"Bundesamt fur Sicherheit in der Informationstechnik BSI, Germany")\ -X(BSM,MANFCODE('B','S','M'),"Bluestar Electrical Meter Research Institute, China")\ -X(BSP,MANFCODE('B','S','P'),"Byucksan Power, South Korea")\ -X(BSS,MANFCODE('B','S','S'),"Baylan Su Sayaclari, Turkey")\ -X(BST,MANFCODE('B','S','T'),"BESTAS Elektronik Optik, Turkey")\ -X(BSX,MANFCODE('B','S','X'),"BS Messtechnik, Germany")\ -X(BSY,MANFCODE('B','S','Y'),"Aktif Enerji AE nsaat Sanayi ve Ticaret Sirketi, Turkey")\ -X(BTA,MANFCODE('B','T','A'),"Buta Elektromekanik, Turkey")\ -X(BTL,MANFCODE('B','T','L'),"BIT LAB")\ -X(BTM,MANFCODE('B','T','M'),"Bharatech Smart Industrial, India")\ -X(BTR,MANFCODE('B','T','R'),"RIA BTR Produktions, Germany")\ -X(BTS,MANFCODE('B','T','S'),"Basari Teknolojik Sistemler, Turkey")\ -X(BUR,MANFCODE('B','U','R'),"Bopp und Reuther Messtechnik, Germany")\ -X(BUY,MANFCODE('B','U','Y'),"BUYI TECHNOLOGOY, China")\ -X(BXC,MANFCODE('B','X','C'),"Beijing Fuxing Xiao Cheng Electronic Technology Stock Xisanhuan Beilu, China")\ -X(BYD,MANFCODE('B','Y','D'),"BYD Company, China")\ -X(BYL,MANFCODE('B','Y','L'),"Baylan, Turkey")\ -X(BYW,MANFCODE('B','Y','W'),"Baylan Water Meters, Turkey")\ -X(BZR,MANFCODE('B','Z','R'),"Gebr Bauer, Germany")\ -X(CAB,MANFCODE('C','A','B'),"Cabtronix, Switzerland")\ -X(CAD,MANFCODE('C','A','D'),"CADIS, Belgium")\ -X(CAH,MANFCODE('C','A','H'),"MAEC GROUPE CAHORS, France")\ -X(CAL,MANFCODE('C','A','L'),"Caleffi, Italy")\ -X(CAN,MANFCODE('C','A','N'),"PT CANNET ELEKTRIK, Indonesia")\ -X(CAR,MANFCODE('C','A','R'),"CARI Electronic, France")\ -X(CAT,MANFCODE('C','A','T'),"Cubes And Tubes, Finland")\ -X(CAV,MANFCODE('C','A','V'),"CaveTronic, Poland")\ -X(CBF,MANFCODE('C','B','F'),"ELECTRICAL CIRCUIT BREAKER FACTORY FOR INDUSTRY ECB, Saudi Arabia")\ -X(CBI,MANFCODE('C','B','I'),"Circuit Breaker Industries, South Africa")\ -X(CBS,MANFCODE('C','B','S'),"CB Svendsen, Denmark")\ -X(CCP,MANFCODE('C','C','P'),"1334816, Canada")\ -X(CCS,MANFCODE('C','C','S'),"Chetas Control Systems, India")\ -X(CDA,MANFCODE('C','D','A'),"Centre for Development of Advanced Computing, India")\ -X(CDL,MANFCODE('C','D','L'),"Customised Data, United Kingdom")\ -X(CDT,MANFCODE('C','D','T'),"CENTRE FOR DEVELOPMENT OF TELEMATICS, India")\ -X(CEB,MANFCODE('C','E','B'),"Cebyc, Norway")\ -X(CED,MANFCODE('C','E','D'),"CEDARNET, France")\ -X(CEG,MANFCODE('C','E','G'),"Consolinno Energy, Germany")\ -X(CEH,MANFCODE('C','E','H'),"Tosee Garan Sanjesh Energy Hoshmand, Iran")\ -X(CEL,MANFCODE('C','E','L'),"Creative Electronics, Pakistan")\ -X(CEM,MANFCODE('C','E','M'),"YAVUZ METAL SANAYI VE TICARET, Turkey")\ -X(CET,MANFCODE('C','E','T'),"Cetinkaya Aydinlatma, Turkey")\ -X(CGC,MANFCODE('C','G','C'),"Contor Group, Romania")\ -X(CGO,MANFCODE('C','G','O'),"Cavagna Group, Italy")\ -X(CHA,MANFCODE('C','H','A'),"Charles Technology Research, Taiwan")\ -X(CHC,MANFCODE('C','H','C'),"CHUN IL INSTRUMENT, South Korea")\ -X(CHE,MANFCODE('C','H','E'),"Shandong Chenhui Electronic Technology, China")\ -X(CHM,MANFCODE('C','H','M'),"Chung Hsin Electric and Machinery Manufacturing, Taiwan")\ -X(CHS,MANFCODE('C','H','S'),"Smart Services International, Switzerland")\ -X(CIC,MANFCODE('C','I','C'),"CHALLENGE INDUSTRIAL, Taiwan")\ -X(CIR,MANFCODE('C','I','R'),"Circutor, Spain")\ -X(CJS,MANFCODE('C','J','S'),"CJS Technology, United Kingdom")\ -X(CKT,MANFCODE('C','K','T'),"CKT Elektrik Elektronik Sanayi ve Ticaret, Turkey")\ -X(CLA,MANFCODE('C','L','A'),"3g celllabs, India")\ -X(CLB,MANFCODE('C','L','B'),"CMC, Italy")\ -X(CLE,MANFCODE('C','L','E'),"Shen Zhen Clou Electronics, China")\ -X(CLO,MANFCODE('C','L','O'),"Clorius Raab Karcher Energi Service")\ -X(CLS,MANFCODE('C','L','S'),"Celsa Messgeraete, Germany")\ -X(CLT,MANFCODE('C','L','T'),"Zhuhai S E Z Calintech Electric, China")\ -X(CLX,MANFCODE('C','L','X'),"CLK IPEKYOLU LOJISTIK VE TICARET A AA, Turkey")\ -X(CLY,MANFCODE('C','L','Y'),"Clayster, Sweden")\ -X(CMC,MANFCODE('C','M','C'),"CMC EKOCON, Slovenia")\ -X(CMP,MANFCODE('C','M','P'),"CM Partner, South Korea")\ -X(CMS,MANFCODE('C','M','S'),"CMS Computers")\ -X(CMT,MANFCODE('C','M','T'),"CMOSTEK MICROELECTRONICS, China")\ -X(CMV,MANFCODE('C','M','V'),"Comverge, United States of America")\ -X(CNM,MANFCODE('C','N','M'),"COSTEL, South Korea")\ -X(CNU,MANFCODE('C','N','U'),"CNU Global, South Korea")\ -X(COH,MANFCODE('C','O','H'),"COHEAT Future Business Centre, United Kingdom")\ -X(COM,MANFCODE('C','O','M'),"COMMON, Poland")\ -X(CON,MANFCODE('C','O','N'),"Conlog")\ -X(CPG,MANFCODE('C','P','G'),"CentraPlus, Germany")\ -X(CPL,MANFCODE('C','P','L'),"CPL CONCORDIA Soc Coop, Italy")\ -X(CPO,MANFCODE('C','P','O'),"C3PO, Spain")\ -X(CPS,MANFCODE('C','P','S'),"CAPITAL POWER SYSTEMS, India")\ -X(CQC,MANFCODE('C','Q','C'),"Yueqing Qicheng Electric, China")\ -X(CRD,MANFCODE('C','R','D'),"CRDM DEVELOPPEMENTS, France")\ -X(CRT,MANFCODE('C','R','T'),"CRDM Technology, France")\ -X(CRW,MANFCODE('C','R','W'),"Xuzhou Runwu Science and Technology Development, China")\ -X(CRX,MANFCODE('C','R','X'),"Corinex Communications, Canada")\ -X(CRY,MANFCODE('C','R','Y'),"Crystal Power, India")\ -X(CSC,MANFCODE('C','S','C'),"CHUBUSEIKI, Japan")\ -X(CSL,MANFCODE('C','S','L'),"Comsel System, Finland")\ -X(CSP,MANFCODE('C','S','P'),"CSP Innovazione nelle ICT, Italy")\ -X(CSW,MANFCODE('C','S','W'),"Critical Software, Portugal")\ -X(CTE,MANFCODE('C','T','E'),"COSTER T E, Italy")\ -X(CTL,MANFCODE('C','T','L'),"Cyan Technology, United Kingdom")\ -X(CTP,MANFCODE('C','T','P'),"Cabcon Technologies, India")\ -X(CTQ,MANFCODE('C','T','Q'),"Control Q, Netherlands")\ -X(CTR,MANFCODE('C','T','R'),"Contar Electronica Industrial, Portugal")\ -X(CTT,MANFCODE('C','T','T'),"DLMS User Association Conformance, Switzerland")\ -X(CTX,MANFCODE('C','T','X'),"Contronix, Germany")\ -X(CUB,MANFCODE('C','U','B'),"Cubic Sensor and Instrument, China")\ -X(CUC,MANFCODE('C','U','C'),"Cuculus, Germany")\ -X(CUR,MANFCODE('C','U','R'),"CURRENT Group, United States of America")\ -X(CWA,MANFCODE('C','W','A'),"CompWell, Sweden")\ -X(CWI,MANFCODE('C','W','I'),"Cewe Instrument, Sweden")\ -X(CWV,MANFCODE('C','W','V'),"CMEC Electric Import Export, China")\ -X(CYE,MANFCODE('C','Y','E'),"Quanzhou Chiyoung Electronics Technology, China")\ -X(CYI,MANFCODE('C','Y','I'),"QUANZHOU CHIYOUNG INSTRUMENT, China")\ -X(CYN,MANFCODE('C','Y','N'),"Cynox Weinart Engineering, Germany")\ -X(CZA,MANFCODE('C','Z','A'),"Contadores de Agua de Zaragoza, Spain")\ -X(CZM,MANFCODE('C','Z','M'),"Cazzaniga")\ -X(DAE,MANFCODE('D','A','E'),"DAE, Italy")\ -X(DAF,MANFCODE('D','A','F'),"Daf Enerji Sanayi ve Ticaret, Turkey")\ -X(DAN,MANFCODE('D','A','N'),"Danubia")\ -X(DBD,MANFCODE('D','B','D'),"DBD Water Systems, India")\ -X(DBE,MANFCODE('D','B','E'),"Decibels Electronics P Decibels Electronics, India")\ -X(DCD,MANFCODE('D','C','D'),"Delhi Control Devices, India")\ -X(DCO,MANFCODE('D','C','O'),"diaeco wave company, Iran")\ -X(DCZ,MANFCODE('D','C','Z'),"decarbon, Germany")\ -X(DDE,MANFCODE('D','D','E'),"D D Elettronica, Italy")\ -X(DDI,MANFCODE('D','D','I'),"DD Infraventures, India")\ -X(DDL,MANFCODE('D','D','L'),"DONGDO LMS, South Korea")\ -X(DEA,MANFCODE('D','E','A'),"Dea HT, Italy")\ -X(DEC,MANFCODE('D','E','C'),"DECODE Data Communications, Serbia")\ -X(DEI,MANFCODE('D','E','I'),"DevSys Embedded Technology, India")\ -X(DEL,MANFCODE('D','E','L'),"DELTAMESS DWWF, Germany")\ -X(DEN,MANFCODE('D','E','N'),"Ducati Energia, Italy")\ -X(DES,MANFCODE('D','E','S'),"Desi Alarms, Turkey")\ -X(DEV,MANFCODE('D','E','V'),"Develco Products, Denmark")\ -X(DFE,MANFCODE('D','F','E'),"Dongfang Electronics, China")\ -X(DFS,MANFCODE('D','F','S'),"Danfoss")\ -X(DGC,MANFCODE('D','G','C'),"Digicom, Italy")\ -X(DGI,MANFCODE('D','G','I'),"Digi International")\ -X(DGM,MANFCODE('D','G','M'),"Diehl Metering, Germany")\ -X(DGY,MANFCODE('D','G','Y'),"Discovergy, Germany")\ -X(DHA,MANFCODE('D','H','A'),"Shanghai dahua measuring auto control instruments manufactory, China")\ -X(DHQ,MANFCODE('D','H','Q'),"YIDU Smart Technology Beijing, China")\ -X(DIE,MANFCODE('D','I','E'),"Dielen, Germany")\ -X(DIG,MANFCODE('D','I','G'),"Digutal Request, Germany")\ -X(DIL,MANFCODE('D','I','L'),"DECCAN INFRATECH LIMETED, India")\ -X(DJA,MANFCODE('D','J','A'),"De Jaeger Automation, Belgium")\ -X(DKY,MANFCODE('D','K','Y'),"Electric Power Research Institute of Guangdong Power Grid Corporation, China")\ -X(DLX,MANFCODE('D','L','X'),"Delixi Group Instruments Meters, China")\ -X(DMC,MANFCODE('D','M','C'),"DMC International, United Arab Emirates")\ -X(DME,MANFCODE('D','M','E'),"DIEHL Metering, Germany")\ -X(DMG,MANFCODE('D','M','G'),"DIINNO, Germany")\ -X(DMI,MANFCODE('D','M','I'),"Dualmicro Measurement Technologies, Netherlands")\ -X(DMO,MANFCODE('D','M','O'),"BEENIC, Germany")\ -X(DMP,MANFCODE('D','M','P'),"DM Power, South Korea")\ -X(DMS,MANFCODE('D','M','S'),"Digilog Micro Solutions, India")\ -X(DNB,MANFCODE('D','N','B'),"Dana Sanjesh o Bargh Danaab, Iran")\ -X(DNC,MANFCODE('D','N','C'),"Dongnam, South Korea")\ -X(DNO,MANFCODE('D','N','O'),"DENO, Croatia")\ -X(DNT,MANFCODE('D','N','T'),"Dr Neuhaus Telekommunikation, Germany")\ -X(DNV,MANFCODE('D','N','V'),"DNV GL, Netherlands")\ -X(DOJ,MANFCODE('D','O','J'),"Suzhou Dojustek Intelligent Technology, China")\ -X(DOM,MANFCODE('D','O','M'),"Domat Control System, Czech Republic")\ -X(DOS,MANFCODE('D','O','S'),"Metsec Cables, Kenya")\ -X(DPM,MANFCODE('D','P','M'),"DELTA PRIMA METERING SDN BHD")\ -X(DPP,MANFCODE('D','P','P'),"DECCAN POWER PRODUCTS, India")\ -X(DRC,MANFCODE('D','R','C'),"DURECOM, South Korea")\ -X(DRP,MANFCODE('D','R','P'),"DBD WATER SYSTEMS, India")\ -X(DRT,MANFCODE('D','R','T'),"DRESSER Italia, Italy")\ -X(DSE,MANFCODE('D','S','E'),"Digitech Systems and Engineering, India")\ -X(DSM,MANFCODE('D','S','M'),"Desimal Elektronik Muhendislik San ve Tic, Turkey")\ -X(DST,MANFCODE('D','S','T'),"Shenzhen Donsun Technology, China")\ -X(DTE,MANFCODE('D','T','E'),"DanTaet, Denmark")\ -X(DUA,MANFCODE('D','U','A'),"DLMS User Association, Switzerland")\ -X(DUS,MANFCODE('D','U','S'),"Dresser Utility Solutions, Germany")\ -X(DVE,MANFCODE('D','V','E'),"DEV ENERGY, Morocco")\ -X(DVG,MANFCODE('D','V','G'),"Device, Germany")\ -X(DVL,MANFCODE('D','V','L'),"devolo, Germany")\ -X(DWY,MANFCODE('D','W','Y'),"Dwyer Instruments, United States of America")\ -X(DWZ,MANFCODE('D','W','Z'),"Lorenz, Germany")\ -X(DZG,MANFCODE('D','Z','G'),"DZG Metering, Germany")\ -X(EAA,MANFCODE('E','A','A'),"Electronic Afzar Azma, Iran")\ -X(EAD,MANFCODE('E','A','D'),"EAD Energieabrechnungssysteme")\ -X(EAH,MANFCODE('E','A','H'),"Endress Hauser, Germany")\ -X(EAS,MANFCODE('E','A','S'),"EAS Elektronik San Tic, Turkey")\ -X(EBE,MANFCODE('E','B','E'),"Ebeco, Sweden")\ -X(EBK,MANFCODE('E','B','K'),"Biesenthal, Germany")\ -X(EBV,MANFCODE('E','B','V'),"EBV Elektronik, Germany")\ -X(EBZ,MANFCODE('E','B','Z'),"eBZ, Germany")\ -X(ECA,MANFCODE('E','C','A'),"ECO ADAPT, France")\ -X(ECB,MANFCODE('E','C','B'),"ELECTRICAL CIRCUITT BREAKER FACTORY FOR INDUSTRY, Saudi Arabia")\ -X(ECC,MANFCODE('E','C','C'),"Energycare Company, Saudi Arabia")\ -X(ECH,MANFCODE('E','C','H'),"Networked Energy Services Corporation, United States of America")\ -X(ECI,MANFCODE('E','C','I'),"ECIL, India")\ -X(ECL,MANFCODE('E','C','L'),"Electronics Corporation of, India")\ -X(ECM,MANFCODE('E','C','M'),"Ecomess, Poland")\ -X(ECO,MANFCODE('E','C','O'),"Engie BtoC Smart program, France")\ -X(ECS,MANFCODE('E','C','S'),"Herholdt Controls, Italy")\ -X(EDI,MANFCODE('E','D','I'),"Enel Distribuzione Via Ombrone, Italy")\ -X(EDM,MANFCODE('E','D','M'),"EDMI")\ -X(EDP,MANFCODE('E','D','P'),"Electro Digital Meter Project Management Services, United Arab Emirates")\ -X(EEC,MANFCODE('E','E','C'),"ESCO, Poland")\ -X(EEE,MANFCODE('E','E','E'),"3E, Italy")\ -X(EEI,MANFCODE('E','E','I'),"ELSOURCE, India")\ -X(EEM,MANFCODE('E','E','M'),"Electrify Energy, India")\ -X(EEO,MANFCODE('E','E','O'),"Eppeltone Engineers, India")\ -X(EEP,MANFCODE('E','E','P'),"ELECTRONET EQUIPMENT, India")\ -X(EES,MANFCODE('E','E','S'),"Eletra Industria e Comercio de Medidores Eletricos, Brazil")\ -X(EFA,MANFCODE('E','F','A'),"EFACEC Engenharia e Sistemas, Portugal")\ -X(EFE,MANFCODE('E','F','E'),"Engelmann Sensor, Germany")\ -X(EFI,MANFCODE('E','F','I'),"Efimatic Iberica, Spain")\ -X(EFN,MANFCODE('E','F','N'),"EFEN, Germany")\ -X(EFR,MANFCODE('E','F','R'),"Europaeische Funk Rundsteuerung, Germany")\ -X(EFS,MANFCODE('E','F','S'),"EFSYS, France")\ -X(EGA,MANFCODE('E','G','A'),"eGain International, Sweden")\ -X(EGC,MANFCODE('E','G','C'),"Ekbatan Gas Control, Iran")\ -X(EGD,MANFCODE('E','G','D'),"EcoGuard, Sweden")\ -X(EGM,MANFCODE('E','G','M'),"Elgama Elektronika, Lithuania")\ -X(EGT,MANFCODE('E','G','T'),"Enegate, Japan")\ -X(EGW,MANFCODE('E','G','W'),"Enermess Energie Mess und Servicedienste, Germany")\ -X(EGY,MANFCODE('E','G','Y'),"ian Smart Meter Company, Egypt")\ -X(EHL,MANFCODE('E','H','L'),"Secure Meters")\ -X(EHS,MANFCODE('E','H','S'),"Endress Hauser SICK, Germany")\ -X(EIE,MANFCODE('E','I','E'),"Ei Electronics, Ireland")\ -X(EIG,MANFCODE('E','I','G'),"Eigensonne, Germany")\ -X(EIP,MANFCODE('E','I','P'),"Elmeasure, India")\ -X(EIT,MANFCODE('E','I','T'),"EnergyICT, Belgium")\ -X(EKA,MANFCODE('E','K','A'),"Eka Systems, United States of America")\ -X(EKF,MANFCODE('E','K','F'),"EKF, Russian Federation")\ -X(EKO,MANFCODE('E','K','O'),"EKOLIS, France")\ -X(EKS,MANFCODE('E','K','S'),"DICLE KOEK ENERJI YATIRIM")\ -X(EKT,MANFCODE('E','K','T'),"PA KVANT J S, Russian Federation")\ -X(ELC,MANFCODE('E','L','C'),"CJSC EMIS, Russian Federation")\ -X(ELD,MANFCODE('E','L','D'),"Elektromed Elektronik, Turkey")\ -X(ELE,MANFCODE('E','L','E'),"Elster Electricity, United States of America")\ -X(ELG,MANFCODE('E','L','G'),"Elgas, Czech Republic")\ -X(ELL,MANFCODE('E','L','L'),"Energy Plus, Russian Federation")\ -X(ELM,MANFCODE('E','L','M'),"Elektromed Elektronik, Turkey")\ -X(ELO,MANFCODE('E','L','O'),"ELO Sistemas Eletronicos, Brazil")\ -X(ELP,MANFCODE('E','L','P'),"ElectroPlus, Iran")\ -X(ELQ,MANFCODE('E','L','Q'),"ELEQ, Germany")\ -X(ELR,MANFCODE('E','L','R'),"Elster Metering, United Kingdom")\ -X(ELS,MANFCODE('E','L','S'),"Elster, Germany")\ -X(ELT,MANFCODE('E','L','T'),"ELTAKO, Germany")\ -X(ELV,MANFCODE('E','L','V'),"Elvaco, Sweden")\ -X(ELY,MANFCODE('E','L','Y'),"Elektroniksystem i Umeaa, Sweden")\ -X(EMB,MANFCODE('E','M','B'),"Embit, Italy")\ -X(EMC,MANFCODE('E','M','C'),"Embedded Communication Systems, Switzerland")\ -X(EME,MANFCODE('E','M','E'),"SC Electromagnetica, Romania")\ -X(EMF,MANFCODE('E','M','F'),"IT Beratung Energiemanagement Flammang, Germany")\ -X(EMH,MANFCODE('E','M','H'),"EMH metering formerly EMH Elektrizitatszahler")\ -X(EMI,MANFCODE('E','M','I'),"Entorno Inteligente de Medicion, Spain")\ -X(EML,MANFCODE('E','M','L'),"Emlite, United Kingdom")\ -X(EMM,MANFCODE('E','M','M'),"Email Metering, Australia")\ -X(EMO,MANFCODE('E','M','O'),"Enermet")\ -X(EMR,MANFCODE('E','M','R'),"CJSC Energomera, Russian Federation")\ -X(EMS,MANFCODE('E','M','S'),"EMS PATVAG, Switzerland")\ -X(EMT,MANFCODE('E','M','T'),"Elster Messtechnik, Germany")\ -X(EMU,MANFCODE('E','M','U'),"EMU Elektronik, Switzerland")\ -X(ENC,MANFCODE('E','N','C'),"ENSYCO ETHIOPIA ELECTRICITY METER MANUFACTURING, China")\ -X(END,MANFCODE('E','N','D'),"ENDYS")\ -X(ENE,MANFCODE('E','N','E'),"ENERDIS, France")\ -X(ENG,MANFCODE('E','N','G'),"ENER G Switch, United Kingdom")\ -X(ENH,MANFCODE('E','N','H'),"ENMAS, Germany")\ -X(ENI,MANFCODE('E','N','I'),"entec innovations, Germany")\ -X(ENL,MANFCODE('E','N','L'),"ENEL, Serbia")\ -X(ENN,MANFCODE('E','N','N'),"Enernet, South Korea")\ -X(ENO,MANFCODE('E','N','O'),"ennovatis, Germany")\ -X(ENP,MANFCODE('E','N','P'),"Kiev Polytechnical Scientific Research")\ -X(ENR,MANFCODE('E','N','R'),"Energisme")\ -X(ENS,MANFCODE('E','N','S'),"ENSO NETZ, Germany")\ -X(ENT,MANFCODE('E','N','T'),"ENTES Elektronik, Turkey")\ -X(ENX,MANFCODE('E','N','X'),"Enetronx, Germany")\ -X(EPI,MANFCODE('E','P','I'),"EPI USE AFRICA, South Africa")\ -X(EPL,MANFCODE('E','P','L'),"Escorts, Pakistan")\ -X(EPT,MANFCODE('E','P','T'),"Energy Prospect Technologies, Spain")\ -X(ERE,MANFCODE('E','R','E'),"Enermatics Energy, South Africa")\ -X(ERI,MANFCODE('E','R','I'),"Easun Reyrolle, India")\ -X(ERL,MANFCODE('E','R','L'),"Erelsan Elektrik ve Elektronik, Turkey")\ -X(ERN,MANFCODE('E','R','N'),"Ericsson Telecomunicazioni, Italy")\ -X(ERS,MANFCODE('E','R','S'),"Eurosys, Bulgaria")\ -X(ERW,MANFCODE('E','R','W'),"Everrenew Energy, India")\ -X(ESA,MANFCODE('E','S','A'),"ESAC, Italy")\ -X(ESC,MANFCODE('E','S','C'),"EDMI Shenzhen, China")\ -X(ESD,MANFCODE('E','S','D'),"Edelstrom Electronic Devices and Designing, United Arab Emirates")\ -X(ESE,MANFCODE('E','S','E'),"ESE Nordic, Sweden")\ -X(ESG,MANFCODE('E','S','G'),"ESG, South Korea")\ -X(ESH,MANFCODE('E','S','H'),"EMSYCON, Germany")\ -X(ESI,MANFCODE('E','S','I'),"Monosan Monofaze Elektrik Motorlari, Turkey")\ -X(ESL,MANFCODE('E','S','L'),"Esyasoft Landis and Gyr, India")\ -X(ESM,MANFCODE('E','S','M'),"Monosan Monofaze Elektrik Motorlari, Turkey")\ -X(ESN,MANFCODE('E','S','N'),"Zhejiang Eastron Electronic, China")\ -X(ESO,MANFCODE('E','S','O'),"Monosan Monofaze Elektrik Motorlari, Turkey")\ -X(ESP,MANFCODE('E','S','P'),"EFICAA ENSMART SOLUTIONS, India")\ -X(ESR,MANFCODE('E','S','R'),"Ensor, Switzerland")\ -X(ESS,MANFCODE('E','S','S'),"Energy Saving Systems, Ukraine")\ -X(ESV,MANFCODE('E','S','V'),"ESKA VALVE, Turkey")\ -X(ESY,MANFCODE('E','S','Y'),"EasyMeter")\ -X(ETD,MANFCODE('E','T','D'),"Energy Team, Italy")\ -X(ETK,MANFCODE('E','T','K'),"ELTEK BROJILA, Croatia")\ -X(ETO,MANFCODE('E','T','O'),"Sphere of economical technologies, Russian Federation")\ -X(ETP,MANFCODE('E','T','P'),"Energotechnica")\ -X(EUE,MANFCODE('E','U','E'),"E E Electronic, Austria")\ -X(EUR,MANFCODE('E','U','R'),"Eurometers")\ -X(EUS,MANFCODE('E','U','S'),"Ebeling und Sohn, Germany")\ -X(EVD,MANFCODE('E','V','D'),"DREWAG NETZ, Germany")\ -X(EVK,MANFCODE('E','V','K'),"EV KUR ELEKTRIK, Turkey")\ -X(EVL,MANFCODE('E','V','L'),"Geothermique, United Kingdom")\ -X(EVT,MANFCODE('E','V','T'),"EVThru, South Korea")\ -X(EWA,MANFCODE('E','W','A'),"EWATTCH, France")\ -X(EWE,MANFCODE('E','W','E'),"EWE, Germany")\ -X(EWG,MANFCODE('E','W','G'),"EWG DOO, Serbia")\ -X(EWS,MANFCODE('E','W','S'),"ENLESS WIRELESS")\ -X(EWT,MANFCODE('E','W','T'),"Diehl Metering, Germany")\ -X(EXS,MANFCODE('E','X','S'),"exceeding solutions, Germany")\ -X(EXT,MANFCODE('E','X','T'),"LOTRIC Metrology, Slovenia")\ -X(EYE,MANFCODE('E','Y','E'),"Eco eye, United Kingdom")\ -X(EYT,MANFCODE('E','Y','T'),"Enerlyt Potsdam")\ -X(FAC,MANFCODE('F','A','C'),"Fireangel Safety Technology, United Kingdom")\ -X(FAE,MANFCODE('F','A','E'),"Fae Technology Company, Brazil")\ -X(FAN,MANFCODE('F','A','N'),"Fantini Cosmi, Italy")\ -X(FAR,MANFCODE('F','A','R'),"FARAB, Iran")\ -X(FAS,MANFCODE('F','A','S'),"FAST, Italy")\ -X(FED,MANFCODE('F','E','D'),"Federal Elektrik, Turkey")\ -X(FEI,MANFCODE('F','E','I'),"FUJI ELECTRIC, India")\ -X(FEM,MANFCODE('F','E','M'),"Fortune Electric, Taiwan")\ -X(FFD,MANFCODE('F','F','D'),"Fast Forward, Germany")\ -X(FID,MANFCODE('F','I','D'),"Fi Muhendislik, Turkey")\ -X(FIM,MANFCODE('F','I','M'),"Frodexim, Bulgaria")\ -X(FIN,MANFCODE('F','I','N'),"Finder, Germany")\ -X(FIO,MANFCODE('F','I','O'),"Pietro Fiorentini, Italy")\ -X(FLA,MANFCODE('F','L','A'),"FLASH ELECTRONICS, India")\ -X(FLD,MANFCODE('F','L','D'),"Fludia, France")\ -X(FLE,MANFCODE('F','L','E'),"XIAN FLAG ELECTRONIC, China")\ -X(FLG,MANFCODE('F','L','G'),"FLOMAG, Czech Republic")\ -X(FLO,MANFCODE('F','L','O'),"Flonidan, Denmark")\ -X(FLS,MANFCODE('F','L','S'),"FLASH o s, Turkey")\ -X(FLU,MANFCODE('F','L','U'),"SHANDONG FEILONG INSTRUMENT, China")\ -X(FLW,MANFCODE('F','L','W'),"Fluidwell, Netherlands")\ -X(FLX,MANFCODE('F','L','X'),"FLEXIM Flexible Industriemesstechnik, Germany")\ -X(FMG,MANFCODE('F','M','G'),"Flow Meter Group, Netherlands")\ -X(FML,MANFCODE('F','M','L'),"Siemens Measurements Formerly FML")\ -X(FMM,MANFCODE('F','M','M'),"F IMM, Italy")\ -X(FND,MANFCODE('F','N','D'),"findy IoT Solutions, Bulgaria")\ -X(FNF,MANFCODE('F','N','F'),"FlowSystem, Poland")\ -X(FNX,MANFCODE('F','N','X'),"Flownetix, United Kingdom")\ -X(FOX,MANFCODE('F','O','X'),"FOXYTECH, Poland")\ -X(FPL,MANFCODE('F','P','L'),"fifthplay, Belgium")\ -X(FPR,MANFCODE('F','P','R'),"F Pribor, Belarus")\ -X(FRC,MANFCODE('F','R','C'),"Shenzhen Friendcom Technology")\ -X(FRE,MANFCODE('F','R','E'),"Frer, Italy")\ -X(FRU,MANFCODE('F','R','U'),"OJSC NRPA FRUNZE, Russian Federation")\ -X(FSE,MANFCODE('F','S','E'),"Flecto Sense, South Africa")\ -X(FSP,MANFCODE('F','S','P'),"Finmek Space, Italy")\ -X(FST,MANFCODE('F','S','T'),"FieldServer Technologies, United States of America")\ -X(FSY,MANFCODE('F','S','Y'),"FlowService, Poland")\ -X(FTL,MANFCODE('F','T','L'),"Tritschler, Germany")\ -X(FUN,MANFCODE('F','U','N'),"Micro Matic Norge, Norway")\ -X(FUS,MANFCODE('F','U','S'),"Fuccesso, China")\ -X(FUT,MANFCODE('F','U','T'),"first utility, United Kingdom")\ -X(FWS,MANFCODE('F','W','S'),"FW Systeme, Germany")\ -X(FZK,MANFCODE('F','Z','K'),"FUNZIN, South Korea")\ -X(GAT,MANFCODE('G','A','T'),"GEPDEC SMART TECHNOLOGY, India")\ -X(GAV,MANFCODE('G','A','V'),"Carlo Gavazzi Controls, Italy")\ -X(GBJ,MANFCODE('G','B','J'),"Grundfoss")\ -X(GBX,MANFCODE('G','B','X'),"GRIDBLOX, United States of America")\ -X(GCE,MANFCODE('G','C','E'),"Genergica, Venezuela")\ -X(GCG,MANFCODE('G','C','G'),"Goldcard Smart Group, China")\ -X(GCX,MANFCODE('G','C','X'),"grandcentrix, Germany")\ -X(GDC,MANFCODE('G','D','C'),"GADICO TECNOLOGIAA")\ -X(GDS,MANFCODE('G','D','S'),"InnoSense, Switzerland")\ -X(GEC,MANFCODE('G','E','C'),"GEC Meters")\ -X(GEE,MANFCODE('G','E','E'),"GE Energy, United Kingdom")\ -X(GEL,MANFCODE('G','E','L'),"Industrial Technology Research Institute, Taiwan")\ -X(GEN,MANFCODE('G','E','N'),"Goerlitz")\ -X(GEO,MANFCODE('G','E','O'),"Green Energy Options, United Kingdom")\ -X(GEP,MANFCODE('G','E','P'),"GEPDEC INFRATECH")\ -X(GET,MANFCODE('G','E','T'),"Genus Electrotech, India")\ -X(GEX,MANFCODE('G','E','X'),"Global Evolution Lighting, Tunisia")\ -X(GFM,MANFCODE('G','F','M'),"GE Fuji Meter, Japan")\ -X(GIL,MANFCODE('G','I','L'),"Genus Innovations, India")\ -X(GIN,MANFCODE('G','I','N'),"Gineers, Bulgaria")\ -X(GIO,MANFCODE('G','I','O'),"G GIOANOLA, Italy")\ -X(GKA,MANFCODE('G','K','A'),"G K Assembly, Thailand")\ -X(GKM,MANFCODE('G','K','M'),"George Kent Bhd, Malaysia")\ -X(GLM,MANFCODE('G','L','M'),"GETRALINE, France")\ -X(GLO,MANFCODE('G','L','O'),"NetLand, Poland")\ -X(GLX,MANFCODE('G','L','X'),"Electrical Equipment Joint Stock Corporation, Vietnam")\ -X(GMC,MANFCODE('G','M','C'),"GMC I Messtechnik, Germany")\ -X(GME,MANFCODE('G','M','E'),"Global Metering Electronics, Netherlands")\ -X(GMM,MANFCODE('G','M','M'),"Gamma International, Egypt")\ -X(GMS,MANFCODE('G','M','S'),"Gity Madara Sepahan, Iran")\ -X(GMT,MANFCODE('G','M','T'),"GMT, Germany")\ -X(GNP,MANFCODE('G','N','P'),"GEPDEC ENERGY, India")\ -X(GNY,MANFCODE('G','N','Y'),"JiangSu GuoNeng Instrument Technology, China")\ -X(GOE,MANFCODE('G','O','E'),"Genus Power Infrastructures, India")\ -X(GOM,MANFCODE('G','O','M'),"Zhejiang Gomelong Meter, China")\ -X(GPM,MANFCODE('G','P','M'),"Polaris Smart Metering, India")\ -X(GPT,MANFCODE('G','P','T'),"GINTAI POWER TECHNOLOGY, China")\ -X(GRA,MANFCODE('G','R','A'),"Graesslin, Germany")\ -X(GRD,MANFCODE('G','R','D'),"Greenradio")\ -X(GRE,MANFCODE('G','R','E'),"GE2 Green Energy Electronics, Portugal")\ -X(GRI,MANFCODE('G','R','I'),"Grinpal Energy Management, South Africa")\ -X(GRN,MANFCODE('G','R','N'),"GREEN ENS, South Korea")\ -X(GRP,MANFCODE('G','R','P'),"GlobalRnD, Poland")\ -X(GRS,MANFCODE('G','R','S'),"Garderos, Germany")\ -X(GRU,MANFCODE('G','R','U'),"G GURU, South Korea")\ -X(GRV,MANFCODE('G','R','V'),"Gridventure Arge ve Teknoloji Pazarlama A AA, Turkey")\ -X(GRX,MANFCODE('G','R','X'),"Gurux, Finland")\ -X(GSD,MANFCODE('G','S','D'),"Genesis Smart Devices, India")\ -X(GSE,MANFCODE('G','S','E'),"Gridspertise, Italy")\ -X(GSP,MANFCODE('G','S','P'),"Ingenieurbuero Gasperowicz")\ -X(GSS,MANFCODE('G','S','S'),"R D Gran System S, Belarus")\ -X(GST,MANFCODE('G','S','T'),"Shenzhen Golden Square Technology, China")\ -X(GTE,MANFCODE('G','T','E'),"GREATech, Germany")\ -X(GTM,MANFCODE('G','T','M'),"G2 misuratori, Italy")\ -X(GTR,MANFCODE('G','T','R'),"Globaltronics for Electronics E, Egypt")\ -X(GTS,MANFCODE('G','T','S'),"GIGATRONIK Stuttgart, Germany")\ -X(GUH,MANFCODE('G','U','H'),"ShenZhen GuangNing Industrial, China")\ -X(GVL,MANFCODE('G','V','L'),"gAvilar, Netherlands")\ -X(GWF,MANFCODE('G','W','F'),"Gas u Wassermesserfabrik, Switzerland")\ -X(GWI,MANFCODE('G','W','I'),"George Wilson Industries Aldermans Green Industrial Estate, United Kingdom")\ -X(HAG,MANFCODE('H','A','G'),"Hager Electro, Germany")\ -X(HAK,MANFCODE('H','A','K'),"Hako, Slovakia")\ -X(HBY,MANFCODE('H','B','Y'),"HENAN HUABIAO INSTRUMENT TECHNOLOGY, China")\ -X(HCE,MANFCODE('H','C','E'),"Hsiang Cheng Electric, China")\ -X(HCM,MANFCODE('H','C','M'),"Hidroconta U, Spain")\ -X(HDA,MANFCODE('H','D','A'),"Hidria, Slovenia")\ -X(HDK,MANFCODE('H','D','K'),"Hydroko, Belgium")\ -X(HDX,MANFCODE('H','D','X'),"Beijing TianRuiXiangDe Measuring Technology, China")\ -X(HDY,MANFCODE('H','D','Y'),"Dalian hundao instrument, China")\ -X(HEG,MANFCODE('H','E','G'),"Hamburger Elektronik Gesellschaft")\ -X(HEI,MANFCODE('H','E','I'),"Hydro Eco Invest, Poland")\ -X(HEK,MANFCODE('H','E','K'),"Hexing Technology Company, Kenya")\ -X(HEL,MANFCODE('H','E','L'),"Heliowatt")\ -X(HEM,MANFCODE('H','E','M'),"Hokkaido Electric Meter Industry, Japan")\ -X(HER,MANFCODE('H','E','R'),"Hermes Systems, Australia")\ -X(HEW,MANFCODE('H','E','W'),"Honeywell Integrated Technology, China")\ -X(HEX,MANFCODE('H','E','X'),"Shenzhen Hexcell Electronics Technology, China")\ -X(HFI,MANFCODE('H','F','I'),"Nanjing Hu Measurement Control Technology, China")\ -X(HFL,MANFCODE('H','F','L'),"HFIL Technologies")\ -X(HFR,MANFCODE('H','F','R'),"SAERI HEAT METERING TECHNOLOGY, China")\ -X(HGM,MANFCODE('H','G','M'),"HG meter, Denmark")\ -X(HGP,MANFCODE('H','G','P'),"Hangzhou GreenPalm Technology, China")\ -X(HHE,MANFCODE('H','H','E'),"HAUSHELD, Germany")\ -X(HHM,MANFCODE('H','H','M'),"Chongqing Huahong Metering, China")\ -X(HIE,MANFCODE('H','I','E'),"Shenzhen Holley South Electronics Technology, China")\ -X(HIN,MANFCODE('H','I','N'),"huebner Informationselektronik, Germany")\ -X(HIT,MANFCODE('H','I','T'),"ABB, Poland")\ -X(HJM,MANFCODE('H','J','M'),"Huaihua Jiannan Machinery Factory, China")\ -X(HKK,MANFCODE('H','K','K'),"Hokuriku Instrumentation, Japan")\ -X(HKT,MANFCODE('H','K','T'),"Heklatech, Sweden")\ -X(HLD,MANFCODE('H','L','D'),"Hildebrand Technology, United Kingdom")\ -X(HLT,MANFCODE('H','L','T'),"Hangzhou Laison Technology, China")\ -X(HLY,MANFCODE('H','L','Y'),"Holley Metering")\ -X(HMI,MANFCODE('H','M','I'),"HMI Energy, Taiwan")\ -X(HML,MANFCODE('H','M','L'),"Holley Technology, China")\ -X(HMS,MANFCODE('H','M','S'),"Hermes Systems, Australia")\ -X(HMT,MANFCODE('H','M','T'),"Hangzhou Meteronic Technology, China")\ -X(HMU,MANFCODE('H','M','U'),"Hugo Muller, Germany")\ -X(HNA,MANFCODE('H','N','A'),"Houshmand Namayeh Afraz, Iran")\ -X(HND,MANFCODE('H','N','D'),"Shenzhen Hemei Group, China")\ -X(HNN,MANFCODE('H','N','N'),"Houshmand Namayeh Afraz, Iran")\ -X(HOE,MANFCODE('H','O','E'),"HOENTZSCH, Germany")\ -X(HOL,MANFCODE('H','O','L'),"Holosys, Croatia")\ -X(HON,MANFCODE('H','O','N'),"Honeywell Technologies, Switzerland")\ -X(HOY,MANFCODE('H','O','Y'),"Holley Meters, India")\ -X(HPL,MANFCODE('H','P','L'),"HPL Socomec, India")\ -X(HPM,MANFCODE('H','P','M'),"HANGZHOU HPM COMPANY, China")\ -X(HQL,MANFCODE('H','Q','L'),"H Q LAMPS MANUFACTURING, India")\ -X(HRM,MANFCODE('H','R','M'),"Hefei Runa Metering, China")\ -X(HRS,MANFCODE('H','R','S'),"HomeRider, France")\ -X(HRZ,MANFCODE('H','R','Z'),"HERZ Messtechnik, Germany")\ -X(HSD,MANFCODE('H','S','D'),"Ningbo Histar Meter Technology, China")\ -X(HST,MANFCODE('H','S','T'),"HST Equipamentos Electronicosa")\ -X(HSX,MANFCODE('H','S','X'),"HST, Germany")\ -X(HTC,MANFCODE('H','T','C'),"Horstmann Timers and Controls")\ -X(HTE,MANFCODE('H','T','E'),"Hexing Technologies Europe, Romania")\ -X(HTI,MANFCODE('H','T','I'),"Shandong Hetong Information Technology, China")\ -X(HTL,MANFCODE('H','T','L'),"Ernst Heitland Erlenstr, Germany")\ -X(HTS,MANFCODE('H','T','S'),"HTS Elektronik")\ -X(HUK,MANFCODE('H','U','K'),"Helbeck Kusemann, Germany")\ -X(HUM,MANFCODE('H','U','M'),"Humanlitech, Ghana")\ -X(HVG,MANFCODE('H','V','G'),"Hekatron Vertriebs, Germany")\ -X(HVT,MANFCODE('H','V','T'),"Helvatron, Switzerland")\ -X(HWC,MANFCODE('H','W','C'),"Qingdao Hiwits Meter, China")\ -X(HWM,MANFCODE('H','W','M'),"Beijing Hongwei Chaoda Instrument Manufacturing, China")\ -X(HWT,MANFCODE('H','W','T'),"Huawei Technologies, China")\ -X(HXD,MANFCODE('H','X','D'),"Beijing HongHaoXingDa Meters, China")\ -X(HXE,MANFCODE('H','X','E'),"Hexing Electrical, China")\ -X(HXT,MANFCODE('H','X','T'),"HEXIN HONGKONG TECHNOLOGY, China")\ -X(HXW,MANFCODE('H','X','W'),"Hangzhou Xili Watthour Meter Manufacture, China")\ -X(HYC,MANFCODE('H','Y','C'),"Zhejiang Hengye Electronics, China")\ -X(HYD,MANFCODE('H','Y','D'),"Diehl Metering, Germany")\ -X(HYE,MANFCODE('H','Y','E'),"Zhejiang Hyayi Electronic Industry, China")\ -X(HYG,MANFCODE('H','Y','G'),"Diehl Metering, Germany")\ -X(HYM,MANFCODE('H','Y','M'),"Zhejiang Hengye Electronics, China")\ -X(HYS,MANFCODE('H','Y','S'),"Hydrelis")\ -X(HYT,MANFCODE('H','Y','T'),"Zhejiang Hengye Electronics, China")\ -X(HZC,MANFCODE('H','Z','C'),"TANGSHAN HUIZHONG INSTRUMENTATION, China")\ -X(HZI,MANFCODE('H','Z','I'),"TANGSHAN HUIZHONG INSTRUMENTATION, China")\ -X(HZZ,MANFCODE('H','Z','Z'),"Huizhou Zhongcheng Electronic Technology, China")\ -X(IAC,MANFCODE('I','A','C'),"INHEMETER AFRICA COMPANY, Kenya")\ -X(IBE,MANFCODE('I','B','E'),"IBOT ENERGY SYSTEMS, India")\ -X(IBG,MANFCODE('I','B','G'),"ITI BANGALORE PLANT, India")\ -X(IBT,MANFCODE('I','B','T'),"Ikom Bilisim Teknolojileri, Turkey")\ -X(ICB,MANFCODE('I','C','B'),"Inscobee, South Korea")\ -X(ICE,MANFCODE('I','C','E'),"Icoal incorporated, United States of America")\ -X(ICK,MANFCODE('I','C','K'),"I C Technology, South Korea")\ -X(ICM,MANFCODE('I','C','M'),"Intracom, Greece")\ -X(ICP,MANFCODE('I','C','P'),"PT Comnets Plus, Indonesia")\ -X(ICS,MANFCODE('I','C','S'),"ICSA, India")\ -X(ICT,MANFCODE('I','C','T'),"International Control Metering Technologies, Germany")\ -X(ICU,MANFCODE('I','C','U'),"i cube, Switzerland")\ -X(IDE,MANFCODE('I','D','E'),"IMIT")\ -X(IDS,MANFCODE('I','D','S'),"IDS, Germany")\ -X(IEC,MANFCODE('I','E','C'),"leonnardo Corporation, Ukraine")\ -X(IEE,MANFCODE('I','E','E'),"I E Electromatic, Spain")\ -X(IEI,MANFCODE('I','E','I'),"Informage Energy, India")\ -X(IEN,MANFCODE('I','E','N'),"Institute of Power Engineering, Poland")\ -X(IES,MANFCODE('I','E','S'),"Integrated Electronic Systems Lab, China")\ -X(IFC,MANFCODE('I','F','C'),"infocon, Montenegro")\ -X(IFX,MANFCODE('I','F','X'),"Infineon Technologies, Germany")\ -X(IGE,MANFCODE('I','G','E'),"INDKOSQUARE GLOBAL ENGINEERING, India")\ -X(IGM,MANFCODE('I','G','M'),"ITALGAS RETI, Italy")\ -X(IGR,MANFCODE('I','G','R'),"iGridS Hebelweg, Germany")\ -X(IGS,MANFCODE('I','G','S'),"I G S DATAFLOW, Italy")\ -X(IHM,MANFCODE('I','H','M'),"Shenzhen Inhemeter, China")\ -X(IID,MANFCODE('I','I','D'),"PT INDUSTRI TELEKOMUNIKASI Persero, Indonesia")\ -X(IIF,MANFCODE('I','I','F'),"Westenergie Metering, Germany")\ -X(IJE,MANFCODE('I','J','E'),"ILJIN Electric, South Korea")\ -X(IJK,MANFCODE('I','J','K'),"IJENKO, France")\ -X(IKE,MANFCODE('I','K','E'),"IK Elektronik Friedrichsgruener Str, Germany")\ -X(IKM,MANFCODE('I','K','M'),"Ikom, Croatia")\ -X(IKO,MANFCODE('I','K','O'),"AE KOM BiliAA im Teknolojileri A AA, Turkey")\ -X(IKS,MANFCODE('I','K','S'),"IKASIAN, Spain")\ -X(IMC,MANFCODE('I','M','C'),"madar, Iran")\ -X(IME,MANFCODE('I','M','E'),"Fellows, Poland")\ -X(IMR,MANFCODE('I','M','R'),"iMeter, Ukraine")\ -X(IMS,MANFCODE('I','M','S'),"IMST, Germany")\ -X(IMT,MANFCODE('I','M','T'),"INTEGRA METERING, Switzerland")\ -X(IMZ,MANFCODE('I','M','Z'),"iMezr Innovations, India")\ -X(INC,MANFCODE('I','N','C'),"Incotex, Russian Federation")\ -X(IND,MANFCODE('I','N','D'),"INDRA SISTEMAS, Spain")\ -X(INE,MANFCODE('I','N','E'),"INNOTAS Elektronik, Germany")\ -X(INF,MANFCODE('I','N','F'),"Infometric, Sweden")\ -X(INI,MANFCODE('I','N','I'),"Altero, Sweden")\ -X(INM,MANFCODE('I','N','M'),"Inepro Metering, Netherlands")\ -X(INN,MANFCODE('I','N','N'),"INNEXIV, United States of America")\ -X(INO,MANFCODE('I','N','O'),"Indotech Switchgear Controls Delhi, India")\ -X(INP,MANFCODE('I','N','P'),"INNOTAS Produktions, Germany")\ -X(INS,MANFCODE('I','N','S'),"INSYS MICROELECTRONICS, Germany")\ -X(INT,MANFCODE('I','N','T'),"Infranet Technologies, Germany")\ -X(INV,MANFCODE('I','N','V'),"Sensus Metering Systems, Germany")\ -X(INX,MANFCODE('I','N','X'),"Innolex Engineering, Netherlands")\ -X(IOM,MANFCODE('I','O','M'),"IOmeter, Germany")\ -X(ION,MANFCODE('I','O','N'),"Ionics EMS, Philippines")\ -X(IOT,MANFCODE('I','O','T'),"Trilliant Networks, Canada")\ -X(IPD,MANFCODE('I','P','D'),"IPD Industrial Products, Australia")\ -X(IQO,MANFCODE('I','Q','O'),"Kwhiq, Netherlands")\ -X(IQS,MANFCODE('I','Q','S'),"IQ Systems, South Africa")\ -X(IRE,MANFCODE('I','R','E'),"IREN Energia, Italy")\ -X(ISA,MANFCODE('I','S','A'),"Isabellenhutte Heusler, Germany")\ -X(ISE,MANFCODE('I','S','E'),"INESH SMART ENERGY P, India")\ -X(ISF,MANFCODE('I','S','F'),"Investcom Szymon Fiedorowicz, Poland")\ -X(ISI,MANFCODE('I','S','I'),"Akcionarsko Drustvo Insa Industrija Satova, Serbia")\ -X(ISK,MANFCODE('I','S','K'),"Iskraemeco, Slovenia")\ -X(ISO,MANFCODE('I','S','O'),"Isoil Industria, Italy")\ -X(ISS,MANFCODE('I','S','S'),"Iskra, Slovenia")\ -X(IST,MANFCODE('I','S','T'),"Ista")\ -X(ISW,MANFCODE('I','S','W'),"iSAtech water, Germany")\ -X(ITA,MANFCODE('I','T','A'),"iTrona, Switzerland")\ -X(ITB,MANFCODE('I','T','B'),"Itron Solucoes para Energia e Aguaa, Brazil")\ -X(ITC,MANFCODE('I','T','C'),"INTECH TUNISIE, Tunisia")\ -X(ITE,MANFCODE('I','T','E'),"ITRON Electricity, France")\ -X(ITF,MANFCODE('I','T','F'),"ITF Froeschl, Germany")\ -X(ITG,MANFCODE('I','T','G'),"ITRON Gas, France")\ -X(ITH,MANFCODE('I','T','H'),"INTELTEH, Croatia")\ -X(ITI,MANFCODE('I','T','I'),"ITRON Asia, Indonesia")\ -X(ITK,MANFCODE('I','T','K'),"Itron, Germany")\ -X(ITL,MANFCODE('I','T','L'),"Indriya Technologies, India")\ -X(ITM,MANFCODE('I','T','M'),"Intelligent Metering Systems, Pakistan")\ -X(ITP,MANFCODE('I','T','P'),"M s ITI, India")\ -X(ITR,MANFCODE('I','T','R'),"Itron")\ -X(ITS,MANFCODE('I','T','S'),"ITRON Australasia, Australia")\ -X(ITU,MANFCODE('I','T','U'),"ITRON United States Itron, United States of America")\ -X(ITW,MANFCODE('I','T','W'),"ITRON Water, France")\ -X(ITX,MANFCODE('I','T','X'),"IDEAL TECHNICAL SOLUTIONS, United Arab Emirates")\ -X(ITZ,MANFCODE('I','T','Z'),"ITRON, South Africa")\ -X(IUS,MANFCODE('I','U','S'),"IUSA DE CV, Mexico")\ -X(IVY,MANFCODE('I','V','Y'),"IVY metering, China")\ -X(IWD,MANFCODE('I','W','D'),"Inkwell Data, Ireland")\ -X(IWK,MANFCODE('I','W','K'),"IWK Regler und Kompensatoren")\ -X(IYI,MANFCODE('I','Y','I'),"KAYI ENERGY Hacettepe Universitesi, Turkey")\ -X(IZE,MANFCODE('I','Z','E'),"iZenze, Sweden")\ -X(JAC,MANFCODE('J','A','C'),"Jay Autocomponents, India")\ -X(JAN,MANFCODE('J','A','N'),"Janitza electronics, Germany")\ -X(JBF,MANFCODE('J','B','F'),"Beijing jingyibeifang instrument company")\ -X(JCE,MANFCODE('J','C','E'),"Janz Contadores de Energia, Portugal")\ -X(JED,MANFCODE('J','E','D'),"JED, South Korea")\ -X(JGD,MANFCODE('J','G','D'),"tianjin guangdaweiye measuring instrument technology, China")\ -X(JGF,MANFCODE('J','G','F'),"Janz Contagem e Gestao de Fluidos, Portugal")\ -X(JGH,MANFCODE('J','G','H'),"ITSQUARE, South Korea")\ -X(JHA,MANFCODE('J','H','A'),"Zhejiang JiaHe Instrument, China")\ -X(JHM,MANFCODE('J','H','M'),"Changzhou Jianhu Intelligentize Meter, China")\ -X(JJN,MANFCODE('J','J','N'),"BeiJing Fine Clean Enery Technology, China")\ -X(JKW,MANFCODE('J','K','W'),"QingdaoJakewill Energy Technology, China")\ -X(JMA,MANFCODE('J','M','A'),"Huizhou JMAO Intelligent Technology, China")\ -X(JMT,MANFCODE('J','M','T'),"JM TRONIC, Poland")\ -X(JNC,MANFCODE('J','N','C'),"NINGBO JIANAN ELECTRONICS, China")\ -X(JND,MANFCODE('J','N','D'),"J D Electronics, South Korea")\ -X(JNJ,MANFCODE('J','N','J'),"JNJ POWERCOM SYSTEMS, India")\ -X(JOY,MANFCODE('J','O','Y'),"Zhejiang Joy Electronic Technology, China")\ -X(JPM,MANFCODE('J','P','M'),"JPM INDUSTRIES, India")\ -X(JSM,MANFCODE('J','S','M'),"Jining Goldwater Science Technology, China")\ -X(JSO,MANFCODE('J','S','O'),"Schloesser Armaturen, Germany")\ -X(JST,MANFCODE('J','S','T'),"JSTech, South Korea")\ -X(JUM,MANFCODE('J','U','M'),"JUMO, Germany")\ -X(JWH,MANFCODE('J','W','H'),"TIANJIN WANHUA, China")\ -X(JWR,MANFCODE('J','W','R'),"Shandong jingwei telecommunications equipment, China")\ -X(JWS,MANFCODE('J','W','S'),"Wesson energy saving technology tianjin, China")\ -X(JYS,MANFCODE('J','Y','S'),"Beijing Jingyuan instrument company, China")\ -X(KAA,MANFCODE('K','A','A'),"Kamstrup, Denmark")\ -X(KAC,MANFCODE('K','A','C'),", Russian Federation")\ -X(KAL,MANFCODE('K','A','L'),"Kallenbach Elektronik")\ -X(KAM,MANFCODE('K','A','M'),"Kamstrup Energi")\ -X(KAR,MANFCODE('K','A','R'),"Keskar Vana Metal Mak Sti, Turkey")\ -X(KAS,MANFCODE('K','A','S'),"Kamstrup, Denmark")\ -X(KAT,MANFCODE('K','A','T'),"KATHREIN Werke, Germany")\ -X(KAW,MANFCODE('K','A','W'),"Kamstrup, Denmark")\ -X(KBH,MANFCODE('K','B','H'),"K Biesinger, Germany")\ -X(KBK,MANFCODE('K','B','K'),"KBK ELECTRONICS, Pakistan")\ -X(KBL,MANFCODE('K','B','L'),"Kimbal, India")\ -X(KBN,MANFCODE('K','B','N'),"Alpamis IT, Turkey")\ -X(KDN,MANFCODE('K','D','N'),"KADEN VODOMERY, Czech Republic")\ -X(KDS,MANFCODE('K','D','S'),"Kedros, Slovakia")\ -X(KEE,MANFCODE('K','E','E'),"Keewe, Bulgaria")\ -X(KEL,MANFCODE('K','E','L'),"KELEMINIC, Croatia")\ -X(KER,MANFCODE('K','E','R'),"KERMS haftungsbeschraenkt, Germany")\ -X(KES,MANFCODE('K','E','S'),"Kavosh Electronic Sepahan, Iran")\ -X(KEY,MANFCODE('K','E','Y'),"KEYTECH ELEKTRONAE K ANONAE M AA AE RKETAE, Turkey")\ -X(KFM,MANFCODE('K','F','M'),"Shenzhen Kaifa Technology Chengdu, China")\ -X(KFM,MANFCODE('K','F','M'),"Shenzhen Kaifa Technology, China")\ -X(KGE,MANFCODE('K','G','E'),"Guangzhou Keli General Electric, China")\ -X(KHL,MANFCODE('K','H','L'),"Kohler, Turkey")\ -X(KIG,MANFCODE('K','I','G'),"Kiwigrid Kleiststrasse, Germany")\ -X(KIT,MANFCODE('K','I','T'),"KONCAR Instrument transformers, Croatia")\ -X(KJN,MANFCODE('K','J','N'),"Radniroo Industrial")\ -X(KKE,MANFCODE('K','K','E'),"KK Electronic")\ -X(KKG,MANFCODE('K','K','G'),"Katronic, Germany")\ -X(KLE,MANFCODE('K','L','E'),"SHIJIAZHUANG KELIN ELECTRIC, China")\ -X(KLK,MANFCODE('K','L','K'),"Kerlink, France")\ -X(KMB,MANFCODE('K','M','B'),"Kamstrup, Denmark")\ -X(KMT,MANFCODE('K','M','T'),"Krohne Messtechnik, Germany")\ -X(KNI,MANFCODE('K','N','I'),"zhangzhou keneng electrical equipment, China")\ -X(KNR,MANFCODE('K','N','R'),"ISCIENTIFIC TECHSOLUTIONS LABS, India")\ -X(KNX,MANFCODE('K','N','X'),"KONNEX based users Siemens Regensburg")\ -X(KPD,MANFCODE('K','P','D'),"Korea Electric Power Industrial Development, South Korea")\ -X(KPL,MANFCODE('K','P','L'),"KPLC Power, Kenya")\ -X(KRO,MANFCODE('K','R','O'),"Kromschroder")\ -X(KRT,MANFCODE('K','R','T'),"Karat, Russian Federation")\ -X(KSM,MANFCODE('K','S','M'),"Korea Testing Certification, South Korea")\ -X(KST,MANFCODE('K','S','T'),"Kundo SystemTechnik")\ -X(KSY,MANFCODE('K','S','Y'),"KUNDO SystemTechnik, Germany")\ -X(KTC,MANFCODE('K','T','C'),"Kerman Tablo, Iran")\ -X(KTL,MANFCODE('K','T','L'),"Korea Testing Laboratory, South Korea")\ -X(KUK,MANFCODE('K','U','K'),"KYRGYZ UNAA KURUUSH, Kyrgyzstan")\ -X(KVS,MANFCODE('K','V','S'),"Klepsan Klape Vana oelcu aletleri San Tic Sti, Turkey")\ -X(KWH,MANFCODE('K','W','H'),"Zhejiang Songxia Electric Meter, China")\ -X(KZK,MANFCODE('K','Z','K'),"KRIZIK, Czech Republic")\ -X(LAC,MANFCODE('L','A','C'),"Heinz Lackmann, Germany")\ -X(LAI,MANFCODE('L','A','I'),"Hangzhou LAISON Technology, China")\ -X(LAN,MANFCODE('L','A','N'),"Langmatz, Germany")\ -X(LAS,MANFCODE('L','A','S'),"Lansen Systems, Sweden")\ -X(LCG,MANFCODE('L','C','G'),"Landis Gyr Meter System Zhuhai, China")\ -X(LCR,MANFCODE('L','C','R'),"ShanDong LiChuang Science and Technology, China")\ -X(LDE,MANFCODE('L','D','E'),"Shenzhen Londian Electrics, China")\ -X(LEC,MANFCODE('L','E','C'),"Lectrotek Systems, India")\ -X(LEI,MANFCODE('L','E','I'),"PCP International, India")\ -X(LEM,MANFCODE('L','E','M'),"LEM HEME, United Kingdom")\ -X(LES,MANFCODE('L','E','S'),"Lakshmi Electrical Control Systems, India")\ -X(LET,MANFCODE('L','E','T'),"LAN ENGINEERING TECHNOLOGIES, India")\ -X(LFS,MANFCODE('L','F','S'),"Payolcer Metering Devices, Turkey")\ -X(LFX,MANFCODE('L','F','X'),"LoggerFlex Smart Devices, Canada")\ -X(LGA,MANFCODE('L','G','A'),"Landis Gyr")\ -X(LGB,MANFCODE('L','G','B'),"Landis Gyr, United Kingdom")\ -X(LGC,MANFCODE('L','G','C'),"Landis Gyr Equipamentos de Medicaoa Brasil, Brazil")\ -X(LGD,MANFCODE('L','G','D'),"Landis Gyr, Germany")\ -X(LGE,MANFCODE('L','G','E'),"Landis Gyr, Singapore")\ -X(LGF,MANFCODE('L','G','F'),"Landis Gyr, Finland")\ -X(LGG,MANFCODE('L','G','G'),"Ningxia LGG Instrument, China")\ -X(LGI,MANFCODE('L','G','I'),"Landis Gyr, India")\ -X(LGS,MANFCODE('L','G','S'),"Landis Gyr, South Africa")\ -X(LGU,MANFCODE('L','G','U'),"LG Uplus, South Korea")\ -X(LGZ,MANFCODE('L','G','Z'),"Landis Gyr, Switzerland")\ -X(LHA,MANFCODE('L','H','A'),"Atlantic Meters, South Africa")\ -X(LIN,MANFCODE('L','I','N'),"Ever Meter, Myanmar")\ -X(LIT,MANFCODE('L','I','T'),"Punos Electronic, Sweden")\ -X(LJP,MANFCODE('L','J','P'),"Lars Jansen Productions, Netherlands")\ -X(LLM,MANFCODE('L','L','M'),"LianYunGang LianLi First Meter, China")\ -X(LMC,MANFCODE('L','M','C'),"Lumacol, Slovakia")\ -X(LMG,MANFCODE('L','M','G'),"Luftmeister, Germany")\ -X(LML,MANFCODE('L','M','L'),"LUMEL, Poland")\ -X(LMP,MANFCODE('L','M','P'),"LIN MAN POWER TECHNOLOGY, Philippines")\ -X(LMS,MANFCODE('L','M','S'),"LMS Services, Germany")\ -X(LNC,MANFCODE('L','N','C'),"Lancier Monitoring, Germany")\ -X(LNK,MANFCODE('L','N','K'),"Loenk F, South Korea")\ -X(LNT,MANFCODE('L','N','T'),"Schneider Electric, India")\ -X(LOB,MANFCODE('L','O','B'),"Lobaro, Germany")\ -X(LOG,MANFCODE('L','O','G'),"LOGAREX Smart Metering, Czech Republic")\ -X(LOV,MANFCODE('L','O','V'),"Lovato Electric, Italy")\ -X(LSC,MANFCODE('L','S','C'),"Lund Science, Sweden")\ -X(LSE,MANFCODE('L','S','E'),"Landis Staefa electronic")\ -X(LSK,MANFCODE('L','S','K'),"LS Industrial Systems, South Korea")\ -X(LSP,MANFCODE('L','S','P'),"Landis Gyr, Germany")\ -X(LSZ,MANFCODE('L','S','Z'),"Siemens Building Technologies")\ -X(LTO,MANFCODE('L','T','O'),"Labtrino, Sweden")\ -X(LUG,MANFCODE('L','U','G'),"Landis Gyr, Germany")\ -X(LUN,MANFCODE('L','U','N'),"LUNA Elektrik Elektronik, Turkey")\ -X(LUP,MANFCODE('L','U','P'),"LUPUS Electronics, Germany")\ -X(LWT,MANFCODE('L','W','T'),"Lotus Wireless Technologies, India")\ -X(LYE,MANFCODE('L','Y','E'),"Jiangsu Linyang Electronics, China")\ -X(LZG,MANFCODE('L','Z','G'),"Lackmann Zaehler Gesellschaft mbH, Germany")\ -X(MAC,MANFCODE('M','A','C'),"RUDNAP Group Meter Control, Serbia")\ -X(MAD,MANFCODE('M','A','D'),"Maddalena, Italy")\ -X(MAE,MANFCODE('M','A','E'),"Mates Elektronik Metin Ates, Turkey")\ -X(MAG,MANFCODE('M','A','G'),"MAGNATECH Smart Grid Solutions, India")\ -X(MAH,MANFCODE('M','A','H'),"MAHAM ELECTRONIC SAMA SANAT")\ -X(MAN,MANFCODE('M','A','N'),"Manthey, Germany")\ -X(MAS,MANFCODE('M','A','S'),"Master Meter Teknoloji, Turkey")\ -X(MAT,MANFCODE('M','A','T'),"Mitsubishi Electric Automation, Thailand")\ -X(MAX,MANFCODE('M','A','X'),"MAXMET, South Korea")\ -X(MBH,MANFCODE('M','B','H'),"MBH POWER, Ghana")\ -X(MBS,MANFCODE('M','B','S'),"MBS, Germany")\ -X(MCI,MANFCODE('M','C','I'),"METERING AND COMMUNICATIONS SOLUTIONS, India")\ -X(MCP,MANFCODE('M','C','P'),"MPSE, Germany")\ -X(MCR,MANFCODE('M','C','R'),"MICRORISC, Czech Republic")\ -X(MCS,MANFCODE('M','C','S'),"MetCom Solutions, Germany")\ -X(MCV,MANFCODE('M','C','V'),"SOCIETE MAROCAINE DES COMPTEURS SMCV, Morocco")\ -X(MDA,MANFCODE('M','D','A'),"PT MELCOINDA, Indonesia")\ -X(MDE,MANFCODE('M','D','E'),"Diehl Metering, Germany")\ -X(MDL,MANFCODE('M','D','L'),"Maddalena, Italy")\ -X(MDT,MANFCODE('M','D','T'),"Minnotra Energy, India")\ -X(MDX,MANFCODE('M','D','X'),"mdex, Germany")\ -X(MEC,MANFCODE('M','E','C'),"Mitsubishi Electric Corporation, Japan")\ -X(MED,MANFCODE('M','E','D'),"MAHARASHTRA STATE ELECTRICITY DISTRIBUTION COMPANY, India")\ -X(MEE,MANFCODE('M','E','E'),"Metron Europe, France")\ -X(MEH,MANFCODE('M','E','H'),"Mueller electronic, Germany")\ -X(MEI,MANFCODE('M','E','I'),"Sensus Metering Systems, Germany")\ -X(MEL,MANFCODE('M','E','L'),"Mikroelektronika a d, Bosnia and Herzegovina")\ -X(MEM,MANFCODE('M','E','M'),"MEMS, Switzerland")\ -X(MES,MANFCODE('M','E','S'),"Electrometer U, Spain")\ -X(MET,MANFCODE('M','E','T'),"METRA Energie Messtechnik, Germany")\ -X(MEX,MANFCODE('M','E','X'),"Metrix Italia, Italy")\ -X(MHT,MANFCODE('M','H','T'),"Mechatronic Meters, Italy")\ -X(MIC,MANFCODE('M','I','C'),"Microtronics Engineering, Austria")\ -X(MID,MANFCODE('M','I','D'),"MiDo, Italy")\ -X(MII,MANFCODE('M','I','I'),"Apator Miitors ApS, Denmark")\ -X(MIJ,MANFCODE('M','I','J'),"MAXWELL, India")\ -X(MIK,MANFCODE('M','I','K'),"Mikroklima, Czech Republic")\ -X(MIL,MANFCODE('M','I','L'),"Milur Smart System liability company, Russian Federation")\ -X(MIM,MANFCODE('M','I','M'),"n Intelligence Meters Sdn Bhd, Malaysia")\ -X(MIO,MANFCODE('M','I','O'),"Metiundo, Germany")\ -X(MIR,MANFCODE('M','I','R'),"MIR Research and Production Association, Russian Federation")\ -X(MIS,MANFCODE('M','I','S'),"Iskra MIS, Slovenia")\ -X(MIT,MANFCODE('M','I','T'),"Sagemcom Energy Telecom, France")\ -X(MIV,MANFCODE('M','I','V'),"MIVO Technology, Sweden")\ -X(MKA,MANFCODE('M','K','A'),"mirakonta, Germany")\ -X(MKE,MANFCODE('M','K','E'),"MKEK Genel Mudurlugu Gazi Fisek ELSA, Turkey")\ -X(MKL,MANFCODE('M','K','L'),"MAKEL Elektrik Malzemeleri, Turkey")\ -X(MKS,MANFCODE('M','K','S'),"MAK SAY Elektrik Elektronik, Turkey")\ -X(MLQ,MANFCODE('M','L','Q'),"MeterLinq, Italy")\ -X(MLR,MANFCODE('M','L','R'),"PKK Milandr, Russian Federation")\ -X(MMC,MANFCODE('M','M','C'),"Modern Meters, Syria")\ -X(MMF,MANFCODE('M','M','F'),"MEMF Electrical Industries, Saudi Arabia")\ -X(MMI,MANFCODE('M','M','I'),"MyMeterInfo, France")\ -X(MMM,MANFCODE('M','M','M'),"MATRICA")\ -X(MMP,MANFCODE('M','M','P'),"MATRIXMETERS, India")\ -X(MMS,MANFCODE('M','M','S'),"Brunswick Bowling and Billiards UK, United Kingdom")\ -X(MMT,MANFCODE('M','M','T'),"METMOTEC, United Kingdom")\ -X(MNS,MANFCODE('M','N','S'),"MANAS Elektronik, Turkey")\ -X(MNW,MANFCODE('M','N','W'),"MENOWATT GE, Italy")\ -X(MOJ,MANFCODE('M','O','J'),"Mojec International, Nigeria")\ -X(MOS,MANFCODE('M','O','S'),"MOMAS SYSTEMS, Nigeria")\ -X(MOT,MANFCODE('M','O','T'),"The Motwane Manufacturing Company, India")\ -X(MPA,MANFCODE('M','P','A'),"Mega Power Automation International, Hong Kong")\ -X(MPL,MANFCODE('M','P','L'),"Metrona Polska Pomiary i Rozliczenia, Poland")\ -X(MPR,MANFCODE('M','P','R'),"Michael Rac, Germany")\ -X(MPS,MANFCODE('M','P','S'),"Multiprocessor Systems, Bulgaria")\ -X(MRN,MANFCODE('M','R','N'),"LLC RPC MEtRON, Ukraine")\ -X(MRT,MANFCODE('M','R','T'),"MIRTEK, Russian Federation")\ -X(MSB,MANFCODE('M','S','B'),"MISA SDN BHD, Malaysia")\ -X(MSE,MANFCODE('M','S','E'),"Mahashakti Energy, India")\ -X(MSI,MANFCODE('M','S','I'),"MIC ELECTRONICS, India")\ -X(MSM,MANFCODE('M','S','M'),"MS M, South Korea")\ -X(MSO,MANFCODE('M','S','O'),"Metiso, Serbia")\ -X(MST,MANFCODE('M','S','T'),"Microsoft Corporation, Taiwan")\ -X(MSY,MANFCODE('M','S','Y'),"MAK SAY Elektrik Elektronik Malzemeleri, Turkey")\ -X(MTC,MANFCODE('M','T','C'),"Metering Technology Corporation")\ -X(MTD,MANFCODE('M','T','D'),"Removed November, United States of America")\ -X(MTE,MANFCODE('M','T','E'),"The ian German for Water Meter Manufacturing Metrotec, Egypt")\ -X(MTH,MANFCODE('M','T','H'),"njmeter, China")\ -X(MTI,MANFCODE('M','T','I'),"Microtech Industries, Pakistan")\ -X(MTL,MANFCODE('M','T','L'),"Advance Metering technology, India")\ -X(MTM,MANFCODE('M','T','M'),"Metrum, Sweden")\ -X(MTN,MANFCODE('M','T','N'),"Merytronic, Spain")\ -X(MTP,MANFCODE('M','T','P'),"Modern Transformers, India")\ -X(MTR,MANFCODE('M','T','R'),"METER, Russian Federation")\ -X(MTS,MANFCODE('M','T','S'),"MeteRSit, Italy")\ -X(MTT,MANFCODE('M','T','T'),"PT Metalindo Teratai Putra, Indonesia")\ -X(MTX,MANFCODE('M','T','X'),"Matrix Energy, India")\ -X(MUK,MANFCODE('M','U','K'),"Meters UK Whitegate White Lund Trading Estate, United Kingdom")\ -X(MWI,MANFCODE('M','W','I'),"Magic Wand Intelligent Systems, India")\ -X(MWU,MANFCODE('M','W','U'),"METRONA Union, Germany")\ -X(MXE,MANFCODE('M','X','E'),"Hangzhou MingX technology, China")\ -X(MXM,MANFCODE('M','X','M'),"Maxim Integrated Circuit Design, India")\ -X(MYS,MANFCODE('M','Y','S'),"MODEMSYS, Spain")\ -X(NAE,MANFCODE('N','A','E'),"Nanjing NengRui Automation Equipment, China")\ -X(NAG,MANFCODE('N','A','G'),"Networked Energy Services, Switzerland")\ -X(NAR,MANFCODE('N','A','R'),"NARI Group Corporation NARI Technology Development, China")\ -X(NAS,MANFCODE('N','A','S'),"NAS Instruments OU")\ -X(NAS,MANFCODE('N','A','S'),"NAS Instruments OU, Estonia")\ -X(NAT,MANFCODE('N','A','T'),"Natural Heat Ahorro Energetico, Spain")\ -X(NCK,MANFCODE('N','C','K'),"NC Korea, South Korea")\ -X(NDF,MANFCODE('N','D','F'),"NUCLEO DURO FELGUERA, Spain")\ -X(NDM,MANFCODE('N','D','M'),"Northern Design, United Kingdom")\ -X(NEE,MANFCODE('N','E','E'),"Northern Electric Equipment Joint Stock Company, Vietnam")\ -X(NEI,MANFCODE('N','E','I'),"NEWLECTRIC INNOVATION, India")\ -X(NEO,MANFCODE('N','E','O'),"NeoVac, Switzerland")\ -X(NER,MANFCODE('N','E','R'),"Nero Electronics, Belarus")\ -X(NES,MANFCODE('N','E','S'),"NORA ELK MALZ SAN ve TIC, Turkey")\ -X(NET,MANFCODE('N','E','T'),"Netatmo, France")\ -X(NEX,MANFCODE('N','E','X'),"Nexelec, France")\ -X(NGP,MANFCODE('N','G','P'),"Ngenic, Poland")\ -X(NIK,MANFCODE('N','I','K'),"NIK ELEKTRONIKA, Ukraine")\ -X(NIS,MANFCODE('N','I','S'),"Nisko Industries, Israel")\ -X(NJC,MANFCODE('N','J','C'),"NAMJUN, South Korea")\ -X(NKS,MANFCODE('N','K','S'),"N ERGIE Kundenservice, Germany")\ -X(NLI,MANFCODE('N','L','I'),"Next Level Integration, Germany")\ -X(NME,MANFCODE('N','M','E'),"SI ART, Russian Federation")\ -X(NMG,MANFCODE('N','M','G'),"NMG, Poland")\ -X(NMS,MANFCODE('N','M','S'),"Nisko Advanced Metering Solutions, Israel")\ -X(NMT,MANFCODE('N','M','T'),"Nova met, Czech Republic")\ -X(NNT,MANFCODE('N','N','T'),"2N Telekomunikace, Czech Republic")\ -X(NOI,MANFCODE('N','O','I'),"NOITAC sp k, Poland")\ -X(NOQ,MANFCODE('N','O','Q'),"NorthQ, Denmark")\ -X(NOV,MANFCODE('N','O','V'),"NovaTech, United States of America")\ -X(NPL,MANFCODE('N','P','L'),"CSIR National Physical Laboratory, India")\ -X(NPS,MANFCODE('N','P','S'),"NEOPIS, South Korea")\ -X(NPT,MANFCODE('N','P','T'),"CJSC PROGTECH, Russian Federation")\ -X(NRM,MANFCODE('N','R','M'),"Norm Elektronik, Turkey")\ -X(NRN,MANFCODE('N','R','N'),"Neuron, Switzerland")\ -X(NRT,MANFCODE('N','R','T'),"Zavod Nartis, Russian Federation")\ -X(NSE,MANFCODE('N','S','E'),"North Smart Equipment, China")\ -X(NSP,MANFCODE('N','S','P'),"M S N S Power, India")\ -X(NTC,MANFCODE('N','T','C'),"Nuri Telecom, South Korea")\ -X(NTM,MANFCODE('N','T','M'),"Netinium, Netherlands")\ -X(NVD,MANFCODE('N','V','D'),"METER")\ -X(NVN,MANFCODE('N','V','N'),"NOVEN ENERGY AND ICT, Turkey")\ -X(NWG,MANFCODE('N','W','G'),"NINGBO WASSER GERAETE, China")\ -X(NWM,MANFCODE('N','W','M'),"Ningbo Water Meter, China")\ -X(NXG,MANFCODE('N','X','G'),"nexgen consultancy, India")\ -X(NXP,MANFCODE('N','X','P'),"NXP Semiconductors, Netherlands")\ -X(NYG,MANFCODE('N','Y','G'),"Ningbo Yonggang Instrument, China")\ -X(NYN,MANFCODE('N','Y','N'),"Nanjing Yuneng Instrument, China")\ -X(NZR,MANFCODE('N','Z','R'),"Nordwestdeutsche Zaehlerrevision Ing Aug Knemeyer, Germany")\ -X(OAS,MANFCODE('O','A','S'),"Omni Agate Systems, India")\ -X(OBC,MANFCODE('O','B','C'),"shandong oubiao information Technology, China")\ -X(OBE,MANFCODE('O','B','E'),"Obeo, Norway")\ -X(OBR,MANFCODE('O','B','R'),"RMZ OBRIY, Ukraine")\ -X(ODI,MANFCODE('O','D','I'),"OAS Digital Infrastructures, India")\ -X(ODK,MANFCODE('O','D','K'),"Okinawa Denki Kogyo, Japan")\ -X(OEE,MANFCODE('O','E','E'),"ONUR Elektrik ve Elektronik, Turkey")\ -X(OJA,MANFCODE('O','J','A'),"ELECTRICAL CIRCUIT BREAKER FACTORY FOR INDUSTRY ECB, Saudi Arabia")\ -X(OKN,MANFCODE('O','K','N'),"be storaged, Germany")\ -X(OLI,MANFCODE('O','L','I'),"Olivetti, Italy")\ -X(OMG,MANFCODE('O','M','G'),"Open Metering System Group OMS Group e V")\ -X(OMS,MANFCODE('O','M','S'),"OMNISYSTEM, South Korea")\ -X(ONE,MANFCODE('O','N','E'),"Omnicore Stratejik Teknolojiler Sti, Turkey")\ -X(ONR,MANFCODE('O','N','R'),"ONUR Elektroteknik, Turkey")\ -X(ONS,MANFCODE('O','N','S'),"ONUR Elektroteknik, Turkey")\ -X(OPT,MANFCODE('O','P','T'),"Optec, Switzerland")\ -X(ORB,MANFCODE('O','R','B'),"ORBIS Tecnologia Electrica, Spain")\ -X(ORM,MANFCODE('O','R','M'),"Ormazabal, Spain")\ -X(OSA,MANFCODE('O','S','A'),"Osaki Electric Europe, Japan")\ -X(OSB,MANFCODE('O','S','B'),"Ocea Smart Building, France")\ -X(OSC,MANFCODE('O','S','C'),"Oriental Sales Corporation, India")\ -X(OSK,MANFCODE('O','S','K'),"Osaki Electric, Japan")\ -X(OSV,MANFCODE('O','S','V'),"Onstar Venture, India")\ -X(OTM,MANFCODE('O','T','M'),"OTMetric, France")\ -X(OTS,MANFCODE('O','T','S'),"Shenzhen Oasis Technology Service, China")\ -X(OYK,MANFCODE('O','Y','K'),"GULLWING TIANJIN INDUSTRY DEVELOPMENGT, China")\ -X(OZK,MANFCODE('O','Z','K'),"Oz kar Enerji, Turkey")\ -X(PAD,MANFCODE('P','A','D'),"PadMess, Germany")\ -X(PAF,MANFCODE('P','A','F'),"FAP PAFAL, Poland")\ -X(PAI,MANFCODE('P','A','I'),"PAIBOONKIJTHANA, Thailand")\ -X(PAK,MANFCODE('P','A','K'),"Paktim Energy Sp zo o, Poland")\ -X(PAN,MANFCODE('P','A','N'),"Panasonic Corporation, Japan")\ -X(PBR,MANFCODE('P','B','R'),"PFIFFNER do Brasil, Brazil")\ -X(PCE,MANFCODE('P','C','E'),"Precise Electronics, Thailand")\ -X(PCI,MANFCODE('P','C','I'),"POLYCAB, India")\ -X(PCR,MANFCODE('P','C','R'),"Powercom, Israel")\ -X(PDE,MANFCODE('P','D','E'),"Pfiffner Deutschland, Germany")\ -X(PDX,MANFCODE('P','D','X'),"Paradox Engineering, Switzerland")\ -X(PEA,MANFCODE('P','E','A'),"PEA J V Company, Ecuador")\ -X(PEE,MANFCODE('P','E','E'),"Prantech Energy Solutions, India")\ -X(PEL,MANFCODE('P','E','L'),"Pak Elektron PEL, Pakistan")\ -X(PEM,MANFCODE('P','E','M'),"peerMetering, Germany")\ -X(PEP,MANFCODE('P','E','P'),"POWRTEC ENERGY, India")\ -X(PER,MANFCODE('P','E','R'),"Perry Electric, Italy")\ -X(PFI,MANFCODE('P','F','I'),"PFIFFNER Instrument Transformers, Switzerland")\ -X(PGE,MANFCODE('P','G','E'),"PayGo Energy")\ -X(PGP,MANFCODE('P','G','P'),"P G P Smart Sensing, Belgium")\ -X(PHL,MANFCODE('P','H','L'),"HangZhou PAX Electronic Technology, China")\ -X(PHX,MANFCODE('P','H','X'),"Phoenix Systems, Poland")\ -X(PII,MANFCODE('P','I','I'),"PiiGAB Processinformation i Goteborg, Sweden")\ -X(PIK,MANFCODE('P','I','K'),"pikkerton, Germany")\ -X(PIL,MANFCODE('P','I','L'),"Pilot Systems London, United Kingdom")\ -X(PIM,MANFCODE('P','I','M'),"Power Innovation, Germany")\ -X(PIN,MANFCODE('P','I','N'),"PFIFFNER Instr Transformers, India")\ -X(PIP,MANFCODE('P','I','P'),"Hermann Pipersberg jr, Germany")\ -X(PIV,MANFCODE('P','I','V'),"Pivot Access, Rwanda")\ -X(PLL,MANFCODE('P','L','L'),"POLL, Czech Republic")\ -X(PLN,MANFCODE('P','L','N'),"Prolan Process Control Company, Hungary")\ -X(PLO,MANFCODE('P','L','O'),"Weihai Ploumeter, China")\ -X(PLU,MANFCODE('P','L','U'),"Plutus Smart Energy Solutions, India")\ -X(PME,MANFCODE('P','M','E'),"Pal Mohan Electronics, India")\ -X(PMG,MANFCODE('P','M','G'),"Sensus Metering Systems, Germany")\ -X(PML,MANFCODE('P','M','L'),"PROGRESSIVE METERS, India")\ -X(PMP,MANFCODE('P','M','P'),"REDUR, Germany")\ -X(PMS,MANFCODE('P','M','S'),"PMS Elektronik, Germany")\ -X(PMX,MANFCODE('P','M','X'),"Dell Anna Ing Luca, Italy")\ -X(PNC,MANFCODE('P','N','C'),"PNC Technology, South Korea")\ -X(PNG,MANFCODE('P','N','G'),"PAKTIM METERING, Nigeria")\ -X(POD,MANFCODE('P','O','D'),"PowerOneData, India")\ -X(POL,MANFCODE('P','O','L'),"Pronal sp k, Poland")\ -X(POW,MANFCODE('P','O','W'),"PowerApp, Denmark")\ -X(POZ,MANFCODE('P','O','Z'),"ZEUP Pozyton, Poland")\ -X(PPC,MANFCODE('P','P','C'),"Power Plus Communications, Germany")\ -X(PPL,MANFCODE('P','P','L'),"Prompribor plant, Russian Federation")\ -X(PPS,MANFCODE('P','P','S'),"Palace Power Systems, South Africa")\ -X(PQP,MANFCODE('P','Q','P'),"PQ Plus, Germany")\ -X(PRA,MANFCODE('P','R','A'),"Paya Ravesh Aria")\ -X(PRE,MANFCODE('P','R','E'),"Predicate Software, South Africa")\ -X(PRG,MANFCODE('P','R','G'),"Paud Raad Industrial Group, Iran")\ -X(PRI,MANFCODE('P','R','I'),"Polymeters Response International")\ -X(PRM,MANFCODE('P','R','M'),"Promatic, Croatia")\ -X(PRO,MANFCODE('P','R','O'),"Proton Elektromed, Turkey")\ -X(PRY,MANFCODE('P','R','Y'),"ShanDong LiChuang Science and Technology, China")\ -X(PSC,MANFCODE('P','S','C'),"PS Tec, Korea, Democratic People's Republic of (North Korea)")\ -X(PSE,MANFCODE('P','S','E'),"pSenso, Poland")\ -X(PSN,MANFCODE('P','S','N'),"PySENSE, Poland")\ -X(PSS,MANFCODE('P','S','S'),"Part Sanat Sanjesh, Iran")\ -X(PST,MANFCODE('P','S','T'),"PS Tec, South Korea")\ -X(PTI,MANFCODE('P','T','I'),"Prolific Technology, Taiwan")\ -X(PTR,MANFCODE('P','T','R'),"Pfiffner Transformatoer, Turkey")\ -X(PUK,MANFCODE('P','U','K'),"Paktim Consulting UK, United Kingdom")\ -X(PUL,MANFCODE('P','U','L'),"NPP Teplovodokhran")\ -X(PVT,MANFCODE('P','V','T'),"Pavo Tasarim Uretim Elektronik Tic, Turkey")\ -X(PWB,MANFCODE('P','W','B'),"Paul Wegener, Germany")\ -X(PWR,MANFCODE('P','W','R'),"Powrtec, United States of America")\ -X(PXC,MANFCODE('P','X','C'),"Phoenix Contact, Germany")\ -X(PYU,MANFCODE('P','Y','U'),"PYUNGIL, South Korea")\ -X(PYX,MANFCODE('P','Y','X'),"Pyrexx, Germany")\ -X(QDR,MANFCODE('Q','D','R'),"Quadrar Tecnologia, Brazil")\ -X(QDS,MANFCODE('Q','D','S'),"Qundis, Germany")\ -X(QFP,MANFCODE('Q','F','P'),"QFP Engineering, Italy")\ -X(QLE,MANFCODE('Q','L','E'),"Quickline Energy, Switzerland")\ -X(QSD,MANFCODE('Q','S','D'),"Quicksand, Belgium")\ -X(QTS,MANFCODE('Q','T','S'),"QT systems, Sweden")\ -X(RAC,MANFCODE('R','A','C'),"Michael Rac, Germany")\ -X(RAD,MANFCODE('R','A','D'),"Radiocrafts, Norway")\ -X(RAM,MANFCODE('R','A','M'),"Rossweiner Armaturen und Messgeraete OHG, Germany")\ -X(RAS,MANFCODE('R','A','S'),"Rubikon Apskaitos Sistemos, Lithuania")\ -X(RAY,MANFCODE('R','A','Y'),"Rayleigh Instruments, United Kingdom")\ -X(RBM,MANFCODE('R','B','M'),"R B M, Italy")\ -X(RCE,MANFCODE('R','C','E'),"RC ENERGY METERING, India")\ -X(RDK,MANFCODE('R','D','K'),"Wuhan Radarking Electronics, China")\ -X(RDM,MANFCODE('R','D','M'),"RDMx Consulting, Brazil")\ -X(REC,MANFCODE('R','E','C'),"Zhejiang Reallin Electron, China")\ -X(REF,MANFCODE('R','E','F'),"REFERANS ELEKTRIK SAYACLARI, Turkey")\ -X(REI,MANFCODE('R','E','I'),"RENESAS ELECTRONICS, India")\ -X(REL,MANFCODE('R','E','L'),"Relay, Germany")\ -X(REM,MANFCODE('R','E','M'),"Remote Energy Monitoring, United Kingdom")\ -X(REP,MANFCODE('R','E','P'),"Radiant Energy Solutions, India")\ -X(RES,MANFCODE('R','E','S'),"Renesas Electronics, Singapore")\ -X(RHN,MANFCODE('R','H','N'),"Rhino, Poland")\ -X(RIC,MANFCODE('R','I','C'),"Richa Equipments, India")\ -X(RIL,MANFCODE('R','I','L'),"Rikken Instrumentation, India")\ -X(RIM,MANFCODE('R','I','M'),"CJSC Radio and Microelectronics, Russian Federation")\ -X(RIN,MANFCODE('R','I','N'),"Rayleigh Instruments, United Kingdom")\ -X(RIT,MANFCODE('R','I','T'),"Ritz Instrument Transformers, Germany")\ -X(RIX,MANFCODE('R','I','X'),"Raonix, South Korea")\ -X(RIZ,MANFCODE('R','I','Z'),"RIZ Transmitters, Croatia")\ -X(RKD,MANFCODE('R','K','D'),"Ramakrishna Electro Components, India")\ -X(RKE,MANFCODE('R','K','E'),"Viterra Energy Services formerly Raab Karcher ES")\ -X(RMA,MANFCODE('R','M','A'),"Mess und Regeltechnik, Germany")\ -X(RMG,MANFCODE('R','M','G'),"RMG Messtechnik, Germany")\ -X(RMI,MANFCODE('R','M','I'),"Racemosa Energy, India")\ -X(RML,MANFCODE('R','M','L'),"ROLEX METERS RPIVATE, India")\ -X(RMR,MANFCODE('R','M','R'),"Advanced Technology RAMAR, United Kingdom")\ -X(RMT,MANFCODE('R','M','T'),"Richter Messtechnik, Germany")\ -X(RNW,MANFCODE('R','N','W'),"Renergy Micro Technologies, China")\ -X(ROK,MANFCODE('R','O','K'),"ROKIP Liability Company, Russian Federation")\ -X(RSA,MANFCODE('R','S','A'),"Rahrovan Sepehr Andisheh")\ -X(RSM,MANFCODE('R','S','M'),"Zhejiang Risesun Science and Technology, China")\ -X(RSW,MANFCODE('R','S','W'),"RSW Technik, Germany")\ -X(SAA,MANFCODE('S','A','A'),"Sanjesh Afzar Asia, Iran")\ -X(SAC,MANFCODE('S','A','C'),"SACOFGAS, Italy")\ -X(SAE,MANFCODE('S','A','E'),"SAE IT systems, Germany")\ -X(SAG,MANFCODE('S','A','G'),"Sagemcom Energy Telecom, France")\ -X(SAL,MANFCODE('S','A','L'),"Salzer Electronics, India")\ -X(SAM,MANFCODE('S','A','M'),"Siemens OEsterreich AMIS Automated Metering and Information System, Austria")\ -X(SAN,MANFCODE('S','A','N'),"SANN Stockholm, Sweden")\ -X(SAP,MANFCODE('S','A','P'),"Diehl Metering, Germany")\ -X(SAT,MANFCODE('S','A','T'),"SATEC, Israel")\ -X(SBC,MANFCODE('S','B','C'),"Saia Burgess Controls, Switzerland")\ -X(SCA,MANFCODE('S','C','A'),"SENECA, Italy")\ -X(SCE,MANFCODE('S','C','E'),"Seo Chang Electric Communication, South Korea")\ -X(SCH,MANFCODE('S','C','H'),"Schinzel")\ -X(SCL,MANFCODE('S','C','L'),"Steama Company")\ -X(SCM,MANFCODE('S','C','M'),"CalinMeter, China")\ -X(SCR,MANFCODE('S','C','R'),"Sanchuan Wisdom Technology, China")\ -X(SCT,MANFCODE('S','C','T'),"S I C E Telecomunicazioni, Italy")\ -X(SCW,MANFCODE('S','C','W'),"ScatterWeb, Germany")\ -X(SDC,MANFCODE('S','D','C'),"SdC Sistemas de Contagem")\ -X(SDM,MANFCODE('S','D','M'),"Shandong Delu Measurement, China")\ -X(SDS,MANFCODE('S','D','S'),"Sitec Elettronica industriale, Italy")\ -X(SEC,MANFCODE('S','E','C'),"Schneider Electric, Canada")\ -X(SEE,MANFCODE('S','E','E'),"El Sewedy Electrometer, Egypt")\ -X(SEH,MANFCODE('S','E','H'),"SAMRAKSHANA ELECTRICALS, India")\ -X(SEI,MANFCODE('S','E','I'),"Sicom Electronics International, Chile")\ -X(SEK,MANFCODE('S','E','K'),"StroyEnergoKom, Russian Federation")\ -X(SEL,MANFCODE('S','E','L'),"Selec Controls, India")\ -X(SEM,MANFCODE('S','E','M'),"Smart Meter Company, Myanmar")\ -X(SEN,MANFCODE('S','E','N'),"Sensus Metering Systems, Germany")\ -X(SEO,MANFCODE('S','E','O'),"SENSOCO Greatech, Germany")\ -X(SET,MANFCODE('S','E','T'),"Sagemcom Energy Telecom, France")\ -X(SFE,MANFCODE('S','F','E'),"WUHAN SANFRAN ELECTRONICS, China")\ -X(SFI,MANFCODE('S','F','I'),"Siemens DI PA MI, Germany")\ -X(SFR,MANFCODE('S','F','R'),"WUHAN SANFRAN ELECTRONICS, China")\ -X(SFT,MANFCODE('S','F','T'),"Sotflink, Czech Republic")\ -X(SGA,MANFCODE('S','G','A'),"smartGAS Mikrosensorik, Germany")\ -X(SGI,MANFCODE('S','G','I'),"Smartgrid Innovations, India")\ -X(SGM,MANFCODE('S','G','M'),"Swiss Gas Metering, Switzerland")\ -X(SGN,MANFCODE('S','G','N'),"SYGNITY, Poland")\ -X(SGX,MANFCODE('S','G','X'),"Gao Xiang Water Meter, China")\ -X(SHD,MANFCODE('S','H','D'),"Beijing SanHuaDeBao Energy Technoligy, China")\ -X(SHE,MANFCODE('S','H','E'),"Shenzhen SingHang Elec tech, China")\ -X(SHM,MANFCODE('S','H','M'),"Shanghai Metering, China")\ -X(SHT,MANFCODE('S','H','T'),"Shitek Technology, Italy")\ -X(SIC,MANFCODE('S','I','C'),"SICK Engineering, Germany")\ -X(SIE,MANFCODE('S','I','E'),"Siemens")\ -X(SIG,MANFCODE('S','I','G'),"Sigrenea, France")\ -X(SIJ,MANFCODE('S','I','J'),"SUN STAR INDUSTRIES, India")\ -X(SIL,MANFCODE('S','I','L'),"Silicon Laboratories, United States of America")\ -X(SIM,MANFCODE('S','I','M'),"Sana Intelligent Meter, Iran")\ -X(SIN,MANFCODE('S','I','N'),"SINAPSI, Italy")\ -X(SIS,MANFCODE('S','I','S'),"Sevme Informatique Service, France")\ -X(SIT,MANFCODE('S','I','T'),"SITEL, Serbia")\ -X(SIV,MANFCODE('S','I','V'),"Sieverding Heizungs und Sanitaertechnik, Germany")\ -X(SIX,MANFCODE('S','I','X'),"Six Innovation, Sweden")\ -X(SJA,MANFCODE('S','J','A'),"Sheng Jay Automation Technologies, Taiwan")\ -X(SKI,MANFCODE('S','K','I'),"S K I, Germany")\ -X(SKK,MANFCODE('S','K','K'),"Shikoku Instrumentation, Japan")\ -X(SKN,MANFCODE('S','K','N'),"BENTEX CONTROL AND SWITCH GEAR, India")\ -X(SKQ,MANFCODE('S','K','Q'),"SKI sherkate kontorsazi, Iran")\ -X(SKT,MANFCODE('S','K','T'),"SkyToll, Slovakia")\ -X(SKY,MANFCODE('S','K','Y'),"SKYLARK SMARTMETERS, India")\ -X(SLB,MANFCODE('S','L','B'),"Schlumberger Industries, France")\ -X(SLG,MANFCODE('S','L','G'),"Silergy Technologies")\ -X(SLP,MANFCODE('S','L','P'),"Sylop spolka z ograniczona odpowiedzialnoscia sp k, Poland")\ -X(SLV,MANFCODE('S','L','V'),"Solvimus, Germany")\ -X(SLW,MANFCODE('S','L','W'),"SOLID WAY SAS, France")\ -X(SLX,MANFCODE('S','L','X'),"Ymatron, Switzerland")\ -X(SMA,MANFCODE('S','M','A'),"Smart me, Switzerland")\ -X(SMC,MANFCODE('S','M','C'),"Sierra Monitor Corporation, United States of America")\ -X(SME,MANFCODE('S','M','E'),"Siame, Tunisia")\ -X(SMF,MANFCODE('S','M','F'),"Star Measure, Brazil")\ -X(SMG,MANFCODE('S','M','G'),"Samgas, Italy")\ -X(SMI,MANFCODE('S','M','I'),"PT SMART METER, Indonesia")\ -X(SML,MANFCODE('S','M','L'),"Siemens Measurements")\ -X(SMM,MANFCODE('S','M','M'),"Smart Metering, Italy")\ -X(SMN,MANFCODE('S','M','N'),"Saiman Corporation, Kazakhstan")\ -X(SMP,MANFCODE('S','M','P'),"LLC FIRM SEMPAL, Ukraine")\ -X(SMQ,MANFCODE('S','M','Q'),"SMARTMETERQ, Italy")\ -X(SMR,MANFCODE('S','M','R'),"Saudi Meters Company, Saudi Arabia")\ -X(SMS,MANFCODE('S','M','S'),"Smart Metering Solutions Changsha, China")\ -X(SMT,MANFCODE('S','M','T'),"Smarteh, Slovenia")\ -X(SMV,MANFCODE('S','M','V'),"Societe Marocaine des Compteurs, Morocco")\ -X(SMX,MANFCODE('S','M','X'),"Smart Meters Technologies Sdn Bhd, Malaysia")\ -X(SNB,MANFCODE('S','N','B'),"Stromnetz Berlin, Germany")\ -X(SNM,MANFCODE('S','N','M'),"ShenZhen Northmeter, China")\ -X(SNR,MANFCODE('S','N','R'),"NTN SNR, France")\ -X(SNS,MANFCODE('S','N','S'),"Signals and Systems, India")\ -X(SNT,MANFCODE('S','N','T'),"SensorNet, Italy")\ -X(SOC,MANFCODE('S','O','C'),"SOCOMEC, France")\ -X(SOF,MANFCODE('S','O','F'),"Softflow de, Germany")\ -X(SOG,MANFCODE('S','O','G'),"Sogecam Industrial, Spain")\ -X(SOL,MANFCODE('S','O','L'),"Soledia, Italy")\ -X(SOM,MANFCODE('S','O','M'),"Somesca, France")\ -X(SON,MANFCODE('S','O','N'),"Sontex, Switzerland")\ -X(SOS,MANFCODE('S','O','S'),"SOFTSERVICE, Ukraine")\ -X(SOT,MANFCODE('S','O','T'),"Sotronix, Germany")\ -X(SPC,MANFCODE('S','P','C'),"JSC RPC Spektr, Russian Federation")\ -X(SPE,MANFCODE('S','P','E'),"SUKHILA POWER ELECTRONICS, India")\ -X(SPL,MANFCODE('S','P','L'),"Diehl Metering, Germany")\ -X(SPM,MANFCODE('S','P','M'),"Stelmec Powercom, India")\ -X(SPX,MANFCODE('S','P','X'),"Sensus Metering Systems, Germany")\ -X(SPZ,MANFCODE('S','P','Z'),"SPowerZ Solutions, India")\ -X(SRE,MANFCODE('S','R','E'),"Guangzhou Sunrise Electronics Development, China")\ -X(SRF,MANFCODE('S','R','F'),"Saraf Industries, India")\ -X(SRN,MANFCODE('S','R','N'),"Shandong SARON Intelligent Technology, China")\ -X(SRV,MANFCODE('S','R','V'),"Servic, Ukraine")\ -X(SSE,MANFCODE('S','S','E'),"S T Smart Energy, Austria")\ -X(SSG,MANFCODE('S','S','G'),"Super Star Electronics, Bangladesh")\ -X(SSI,MANFCODE('S','S','I'),"Security Solutions Institute, Bulgaria")\ -X(SSL,MANFCODE('S','S','L'),"Synapsys Solutions")\ -X(SSM,MANFCODE('S','S','M'),"SCC METERS, Bangladesh")\ -X(SSN,MANFCODE('S','S','N'),"Silver Spring Networks, United States of America")\ -X(SSS,MANFCODE('S','S','S'),"SUEZ SMART SOLUTIONS, France")\ -X(SST,MANFCODE('S','S','T'),"Qingdao Haina Electric Automation Systems, China")\ -X(STA,MANFCODE('S','T','A'),"Shenzhen Star Instrument, China")\ -X(STC,MANFCODE('S','T','C'),"Sunrise Technology, China")\ -X(STD,MANFCODE('S','T','D'),"Stedin, Netherlands")\ -X(STE,MANFCODE('S','T','E'),"Satera Elektrik Elektronik San ve Tic")\ -X(STF,MANFCODE('S','T','F'),"STACKFORCE, Germany")\ -X(STL,MANFCODE('S','T','L'),"Saudi Technical Group, Saudi Arabia")\ -X(STM,MANFCODE('S','T','M'),"STELMEC, India")\ -X(STN,MANFCODE('S','T','N'),"Hunan Stron Smart, China")\ -X(STO,MANFCODE('S','T','O'),"ABB STOTZ KONTAKT, Germany")\ -X(STP,MANFCODE('S','T','P'),"Spectrum Tool Engineers, India")\ -X(STR,MANFCODE('S','T','R'),"Strike Technologies, South Africa")\ -X(STS,MANFCODE('S','T','S'),"Swistec Systems, Switzerland")\ -X(STV,MANFCODE('S','T','V'),"STV Automation Branch of STV Electronic, Germany")\ -X(STW,MANFCODE('S','T','W'),"Siterwell Electronics, China")\ -X(STZ,MANFCODE('S','T','Z'),"Steinbeis Innovation Center Embedded Design and Networking, Germany")\ -X(SUL,MANFCODE('S','U','L'),"SINHAL UDYOG, India")\ -X(SUM,MANFCODE('S','U','M'),"Sumeru Verrdhe Technologies, India")\ -X(SUN,MANFCODE('S','U','N'),"TIAN JIN SUN ENERGY TECHNOLOGY, China")\ -X(SUP,MANFCODE('S','U','P'),"SUPERMAX COMPONENTS, India")\ -X(SUX,MANFCODE('S','U','X'),"Supermax Components, India")\ -X(SVM,MANFCODE('S','V','M'),"AB Svensk Vaermemaetning SVM")\ -X(SVT,MANFCODE('S','V','T'),"SPUTNIK, Russian Federation")\ -X(SWI,MANFCODE('S','W','I'),"Swistec, Germany")\ -X(SWM,MANFCODE('S','W','M'),"Smart Water Metering, Canada")\ -X(SWS,MANFCODE('S','W','S'),"Stadtwerke Senftenberg, Germany")\ -X(SWT,MANFCODE('S','W','T'),"Beijing Swirling Technology, China")\ -X(SYC,MANFCODE('S','Y','C'),"Sycous Leeds Innovation Centre, United Kingdom")\ -X(SYD,MANFCODE('S','Y','D'),"Sydler Electronics, India")\ -X(SYN,MANFCODE('S','Y','N'),"SMSISTEM, Turkey")\ -X(SYR,MANFCODE('S','Y','R'),"SAYRON GLOBAL ENERJAE A AA, Turkey")\ -X(SYS,MANFCODE('S','Y','S'),"Softwareentwicklung Systemdesign, Austria")\ -X(SYX,MANFCODE('S','Y','X'),"SyxthSense, United Kingdom")\ -X(SZS,MANFCODE('S','Z','S'),"Szerver Szektor Kft, Hungary")\ -X(TAG,MANFCODE('T','A','G'),"Telma, Switzerland")\ -X(TAL,MANFCODE('T','A','L'),"TALAMONA, Italy")\ -X(TAN,MANFCODE('T','A','N'),"Tianxin instrument group, China")\ -X(TAS,MANFCODE('T','A','S'),"Pouyeshgaran Tabriz Azma, Iran")\ -X(TAT,MANFCODE('T','A','T'),"Tatung, Taiwan")\ -X(TAU,MANFCODE('T','A','U'),"TARGUS TECHNOLOGIES, India")\ -X(TAY,MANFCODE('T','A','Y'),"Taytech Otomasyon ve Bilisim Teknolojileri Sti, Turkey")\ -X(TAZ,MANFCODE('T','A','Z'),"PoyeshGaran Sahandzaran Tabrizazma, Iran")\ -X(TBN,MANFCODE('T','B','N'),"TBEA Nanjing Intelligent Electric, China")\ -X(TBS,MANFCODE('T','B','S'),"TEBAS END ISI SIS INS TUR SAN VE TIC STI, Turkey")\ -X(TCE,MANFCODE('T','C','E'),"Qingdao iTechene Technologies, China")\ -X(TCH,MANFCODE('T','C','H'),"Techem")\ -X(TCM,MANFCODE('T','C','M'),"TCOM, Serbia")\ -X(TCO,MANFCODE('T','C','O'),"Teco, Czech Republic")\ -X(TCP,MANFCODE('T','C','P'),"Telecard Prilad, Ukraine")\ -X(TCT,MANFCODE('T','C','T'),"Tecnotel, Italy")\ -X(TCX,MANFCODE('T','C','X'),"Qingdao Topscomm Communication, China")\ -X(TCZ,MANFCODE('T','C','Z'),"Tianjin Chuangzhan Tongcheng Technology Development, China")\ -X(TDC,MANFCODE('T','D','C'),"Telecom Design, France")\ -X(TEA,MANFCODE('T','E','A'),"TEAM R")\ -X(TEC,MANFCODE('T','E','C'),"TECSON Digital, Germany")\ -X(TEH,MANFCODE('T','E','H'),"IOTech")\ -X(TEI,MANFCODE('T','E','I'),"Tohoku Electric Meter Industry, Japan")\ -X(TEK,MANFCODE('T','E','K'),"Tekmar Regelsysteme, Germany")\ -X(TEL,MANFCODE('T','E','L'),"TURBO ENERGY, Nigeria")\ -X(TEN,MANFCODE('T','E','N'),"TekhnoEnergo, Russian Federation")\ -X(TEO,MANFCODE('T','E','O'),"ubitricity Gesellschaft fur verteilte Energiesysteme mbH, Germany")\ -X(TEP,MANFCODE('T','E','P'),"TEPEECAL, France")\ -X(TER,MANFCODE('T','E','R'),"Tavrida Electric Russia, Russian Federation")\ -X(TEU,MANFCODE('T','E','U'),"TOSHELECTROAPPARAT FE, Uzbekistan")\ -X(TFC,MANFCODE('T','F','C'),"Toos Fuse, Iran")\ -X(TFG,MANFCODE('T','F','G'),"TERMICS Divisione FIMIGAS, Italy")\ -X(TGX,MANFCODE('T','G','X'),"Tiesse, Italy")\ -X(THE,MANFCODE('T','H','E'),"Theben, Germany")\ -X(THS,MANFCODE('T','H','S'),"JOZI 20, Bulgaria")\ -X(THW,MANFCODE('T','H','W'),"TW TEAMWARE, Italy")\ -X(TIC,MANFCODE('T','I','C'),"TOKYO KEISO, Japan")\ -X(TID,MANFCODE('T','I','D'),"TIDE, South Korea")\ -X(TIG,MANFCODE('T','I','G'),"ZHONGHUAN TIG, China")\ -X(TII,MANFCODE('T','I','I'),"TRIKALA INDUSTRIES, India")\ -X(TIL,MANFCODE('T','I','L'),"Thermal Integration, United Kingdom")\ -X(TIP,MANFCODE('T','I','P'),"TIP, Germany")\ -X(TIS,MANFCODE('T','I','S'),"Texas Instruments, Hong Kong")\ -X(TIX,MANFCODE('T','I','X'),"Tixi Com")\ -X(TKN,MANFCODE('T','K','N'),"TECHKNAVE, India")\ -X(TKS,MANFCODE('T','K','S'),"Teksan Teknolojik OElcum Sistemleri, Turkey")\ -X(TLC,MANFCODE('T','L','C'),"TELECON GALICIA, Spain")\ -X(TLM,MANFCODE('T','L','M'),"Theodor Lange Messgeraete, Germany")\ -X(TLR,MANFCODE('T','L','R'),"Telereading, Italy")\ -X(TLS,MANFCODE('T','L','S'),"TLS Energimaetning, Sweden")\ -X(TLT,MANFCODE('T','L','T'),"Telecommunication Technologies, Ukraine")\ -X(TLY,MANFCODE('T','L','Y'),"LLC SPE Telemetry, Ukraine")\ -X(TME,MANFCODE('T','M','E'),"TECHNO METERS ELECTRONICS, India")\ -X(TMK,MANFCODE('T','M','K'),"Timi Kosova Sh p k")\ -X(TMS,MANFCODE('T','M','S'),"TEMASS IMALAT, Turkey")\ -X(TMZ,MANFCODE('T','M','Z'),"TMZ Thuringer Mess und Zaehlerwesen Service, Germany")\ -X(TNT,MANFCODE('T','N','T'),"Deltamess DWWF, Germany")\ -X(TOE,MANFCODE('T','O','E'),"Top For Electrical Industries, Egypt")\ -X(TOP,MANFCODE('T','O','P'),"KMB systems, Czech Republic")\ -X(TPB,MANFCODE('T','P','B'),"2 Plus AD, Bulgaria")\ -X(TPC,MANFCODE('T','P','C'),"Taipit measuring equipment, Russian Federation")\ -X(TPG,MANFCODE('T','P','G'),"Texnopark, Uzbekistan")\ -X(TPI,MANFCODE('T','P','I'),"Transfopower Industries, Pakistan")\ -X(TPJ,MANFCODE('T','P','J'),"TAKAHATA PRECISION, Japan")\ -X(TPL,MANFCODE('T','P','L'),"Teplocom Holding, Russian Federation")\ -X(TPZ,MANFCODE('T','P','Z'),"PLC Technology")\ -X(TRC,MANFCODE('T','R','C'),"Traco Doo, Serbia")\ -X(TRG,MANFCODE('T','R','G'),"Trench, Germany")\ -X(TRI,MANFCODE('T','R','I'),"Tritech Technology, Sweden")\ -X(TRJ,MANFCODE('T','R','J'),"SHENZHEN TECHRISE ELECTRONICS, China")\ -X(TRL,MANFCODE('T','R','L'),"Trilliant, Canada")\ -X(TRN,MANFCODE('T','R','N'),"TERRANOVA, Italy")\ -X(TRO,MANFCODE('T','R','O'),"TROX, Germany")\ -X(TRU,MANFCODE('T','R','U'),"Truteq Wireless, South Africa")\ -X(TRV,MANFCODE('T','R','V'),"Transvaro Elektron Aletleri, Turkey")\ -X(TRW,MANFCODE('T','R','W'),"Triowatt")\ -X(TRX,MANFCODE('T','R','X'),"Beijing TianRuiXiang Equipment, China")\ -X(TRZ,MANFCODE('T','R','Z'),"TR Group, South Africa")\ -X(TSD,MANFCODE('T','S','D'),"Theobroma Systems Design und Consulting, Austria")\ -X(TSE,MANFCODE('T','S','E'),"Nichi Manufacturing, India")\ -X(TSG,MANFCODE('T','S','G'),"Testo Sensor, Germany")\ -X(TSW,MANFCODE('T','S','W'),"T7 WENMAX SDN BHD, Malaysia")\ -X(TTG,MANFCODE('T','T','G'),"Tatung, Taiwan")\ -X(TTM,MANFCODE('T','T','M'),"Toshiba Toko Meter Systems, Japan")\ -X(TTN,MANFCODE('T','T','N'),"Tatung, Taiwan")\ -X(TTR,MANFCODE('T','T','R'),"Tetraedre, Switzerland")\ -X(TTT,MANFCODE('T','T','T'),"Telephone and Telegraph Technique, Bulgaria")\ -X(TUR,MANFCODE('T','U','R'),"TURKSAY ELEKTRONIK ELEKTRIK ENDUSTRISI")\ -X(TWO,MANFCODE('T','W','O'),"2Flow, Sweden")\ -X(TXL,MANFCODE('T','X','L'),"CETC46 TianJin New Top Electronics Technology, China")\ -X(UAG,MANFCODE('U','A','G'),"Uher")\ -X(UBI,MANFCODE('U','B','I'),"Ubitronix system solutions, Austria")\ -X(UBY,MANFCODE('U','B','Y'),"ubitricity Gesellschaft fur verteilte Energiesysteme mbH, Germany")\ -X(UEI,MANFCODE('U','E','I'),"United Electrical Industries, India")\ -X(UEL,MANFCODE('U','E','L'),"united electrical industries, India")\ -X(UGI,MANFCODE('U','G','I'),"United Gas Industries")\ -X(UGT,MANFCODE('U','G','T'),"DP UKRGAZTECH Ukrainian gas technologies, Ukraine")\ -X(UHM,MANFCODE('U','H','M'),"Micronics, United Kingdom")\ -X(UNI,MANFCODE('U','N','I'),"Univations, Hong Kong")\ -X(UNM,MANFCODE('U','N','M'),"UniMetric, Croatia")\ -X(UPS,MANFCODE('U','P','S'),"UMA POLY SOLUTIONS, India")\ -X(URM,MANFCODE('U','R','M'),"Urmet Telecomunicazioni, Italy")\ -X(USC,MANFCODE('U','S','C'),"Usanca Soluciones, Spain")\ -X(UTF,MANFCODE('U','T','F'),"UtiliFlex, United States of America")\ -X(UTI,MANFCODE('U','T','I'),"Utilia, Italy")\ -X(UTL,MANFCODE('U','T','L'),"United Telecoms, India")\ -X(UTS,MANFCODE('U','T','S'),"UNIQUE TECHNOSOLUTIONS, India")\ -X(UTT,MANFCODE('U','T','T'),"UtiliTools, Germany")\ -X(VAI,MANFCODE('V','A','I'),"Shete Advanced Technologies, India")\ -X(VAL,MANFCODE('V','A','L'),"Valkoder, Bulgaria")\ -X(VAR,MANFCODE('V','A','R'),"VARAAHI ELECTROMECH, India")\ -X(VDP,MANFCODE('V','D','P'),"Plant Vodopribor management company, Russian Federation")\ -X(VEC,MANFCODE('V','E','C'),"Energo Control, Ukraine")\ -X(VEL,MANFCODE('V','E','L'),"VELANKANI ELECTRONICS, India")\ -X(VEN,MANFCODE('V','E','N'),"INVENDIS TECHNOLOGIES, India")\ -X(VER,MANFCODE('V','E','R'),"VERAUT, Austria")\ -X(VES,MANFCODE('V','E','S'),"Viterra Energy Services")\ -X(VEX,MANFCODE('V','E','X'),"Vertex Electronics, Pakistan")\ -X(VFS,MANFCODE('V','F','S'),"Semas, Serbia")\ -X(VGO,MANFCODE('V','G','O'),"Vango Technologies, Taiwan")\ -X(VIE,MANFCODE('V','I','E'),"Vientech, France")\ -X(VIK,MANFCODE('V','I','K'),"VI KO ELEKTRIK, Turkey")\ -X(VIM,MANFCODE('V','I','M'),"VIEN ENERJI SISTEMLERI SANAYI VE TICARET SIRKETI, Turkey")\ -X(VIN,MANFCODE('V','I','N'),"Vektor innovations, India")\ -X(VIP,MANFCODE('V','I','P'),"VIPA CZ, Czech Republic")\ -X(VIT,MANFCODE('V','I','T'),"Vitir, Norway")\ -X(VLT,MANFCODE('V','L','T'),"ABB EPMV Division, Czech Republic")\ -X(VMP,MANFCODE('V','M','P'),"VAMP, Finland")\ -X(VNE,MANFCODE('V','N','E'),"Vision Networks, India")\ -X(VPI,MANFCODE('V','P','I'),"Van Putten Instruments")\ -X(VPL,MANFCODE('V','P','L'),"Verticross, India")\ -X(VSE,MANFCODE('V','S','E'),"Valenciana Smart Energy of Mediterranean Sea, Spain")\ -X(VSN,MANFCODE('V','S','N'),"Viewshine, China")\ -X(VTC,MANFCODE('V','T','C'),"Vitelec, Netherlands")\ -X(VTG,MANFCODE('V','T','G'),"vemm tec Messtechnik")\ -X(VTK,MANFCODE('V','T','K'),"Linkwell Telesystems, India")\ -X(VTN,MANFCODE('V','T','N'),"VITEX NOVA, Ukraine")\ -X(VTZ,MANFCODE('V','T','Z'),"VitzroSys, South Korea")\ -X(VVV,MANFCODE('V','V','V'),"VIVAVIS, Germany")\ -X(WAD,MANFCODE('W','A','D'),"Waher Data, Sweden")\ -X(WAH,MANFCODE('W','A','H'),"WAHESOFT, Germany")\ -X(WAI,MANFCODE('W','A','I'),"Chongqing WECAN Precision Instruments, China")\ -X(WAL,MANFCODE('W','A','L'),"Wallaby Metering Systems, India")\ -X(WDN,MANFCODE('W','D','N'),"Webdyn, France")\ -X(WEB,MANFCODE('W','E','B'),"Webolution, Germany")\ -X(WEG,MANFCODE('W','E','G'),"WEG Equipamentos Eletricos Automacao, Brazil")\ -X(WEH,MANFCODE('W','E','H'),"E Wehrle, Germany")\ -X(WEL,MANFCODE('W','E','L'),"WELLTECH automation, China")\ -X(WEP,MANFCODE('W','E','P'),"Weptech elektronik, Germany")\ -X(WFT,MANFCODE('W','F','T'),"Waft Embedded Circuit Solutions, India")\ -X(WGN,MANFCODE('W','G','N'),"Wasser Geraete, Germany")\ -X(WGP,MANFCODE('W','G','P'),"GreenPoint, Poland")\ -X(WHU,MANFCODE('W','H','U'),"Wasion Kft, Hungary")\ -X(WIG,MANFCODE('W','I','G'),"Wigersma Sikkema, Netherlands")\ -X(WIK,MANFCODE('W','I','K'),"WIKON Kommunikationstechnik, Germany")\ -X(WIN,MANFCODE('W','I','N'),"Windmill Innovations, Netherlands")\ -X(WKL,MANFCODE('W','K','L'),"Shenyang jia DE lian yi energy technology, China")\ -X(WKX,MANFCODE('W','K','X'),"JINING WUKEXING METER, China")\ -X(WMO,MANFCODE('W','M','O'),"Westermo Teleindustri, Sweden")\ -X(WNC,MANFCODE('W','N','C'),"wireless netcontrol, Germany")\ -X(WNW,MANFCODE('W','N','W'),"Wistron NeWeb Corporation Marketing Center, Taiwan")\ -X(WOW,MANFCODE('W','O','W'),"Sciknow Techno Solutions, India")\ -X(WPZ,MANFCODE('W','P','Z'),"Weepiez SaS, France")\ -X(WSD,MANFCODE('W','S','D'),"Yantai Wisdom Electric, China")\ -X(WSE,MANFCODE('W','S','E'),"Wasion Group, China")\ -X(WSG,MANFCODE('W','S','G'),"ZHEJIANG WELLSUN INTELLIGENT TECHNOLOGY, China")\ -X(WSM,MANFCODE('W','S','M'),"winAMR Systems, India")\ -X(WSR,MANFCODE('W','S','R'),"WESER, Russian Federation")\ -X(WTI,MANFCODE('W','T','I'),"Weihai Sunts Electric Meter, China")\ -X(WTL,MANFCODE('W','T','L'),"Wipro Technologies, India")\ -X(WTM,MANFCODE('W','T','M'),"Watertech, Italy")\ -X(WTT,MANFCODE('W','T','T'),"Watertech, Italy")\ -X(WUR,MANFCODE('W','U','R'),"Wurm Elektronische Systeme, Germany")\ -X(WZG,MANFCODE('W','Z','G'),"Neumann Wasserzaehler Glaubitz, Germany")\ -X(WZT,MANFCODE('W','Z','T'),"Wizit, South Korea")\ -X(XAO,MANFCODE('X','A','O'),"Info Solution, Italy")\ -X(XEE,MANFCODE('X','E','E'),"XEE Technology, Germany")\ -X(XEM,MANFCODE('X','E','M'),"XEMTEC, Switzerland")\ -X(XIO,MANFCODE('X','I','O'),"XIOTA, India")\ -X(XJM,MANFCODE('X','J','M'),"XJ Metering, China")\ -X(XLG,MANFCODE('X','L','G'),"X LOGIC, Croatia")\ -X(XMA,MANFCODE('X','M','A'),"XMASTER, Poland")\ -X(XMX,MANFCODE('X','M','X'),"Xemex, Belgium")\ -X(XPP,MANFCODE('X','P','P'),"XYROSYS, India")\ -X(XTM,MANFCODE('X','T','M'),"Suntront Tech, China")\ -X(XTR,MANFCODE('X','T','R'),"HENAN SUNTRONT TECH, China")\ -X(XTY,MANFCODE('X','T','Y'),"LianYuanGang Tengyue Electronics Technology")\ -X(XYL,MANFCODE('X','Y','L'),"Sensus Ludwigshafen, Germany")\ -X(YDD,MANFCODE('Y','D','D'),"Jilin Yongda Group")\ -X(YDS,MANFCODE('Y','D','S'),"YIDU Smart Technology Beijing, China")\ -X(YEM,MANFCODE('Y','E','M'),"KYENM TECH, India")\ -X(YFC,MANFCODE('Y','F','C'),"Yufeng Changhui Automation Instrument, China")\ -X(YGM,MANFCODE('Y','G','M'),"YGM Shandong Yiguang heat metering technology, China")\ -X(YHE,MANFCODE('Y','H','E'),"Youho Electric, South Korea")\ -X(YHF,MANFCODE('Y','H','F'),"Hebei Haifeng Electronic Technology, China")\ -X(YJZ,MANFCODE('Y','J','Z'),"Zhuhai Yujian Science Technology, China")\ -X(YNE,MANFCODE('Y','N','E'),"YOONO ENERGY TECHNOLOGY JIANGSU, China")\ -X(YNP,MANFCODE('Y','N','P'),"Ningbo Yong Neng Power Technology, China")\ -X(YPP,MANFCODE('Y','P','P'),"YPP, South Korea")\ -X(YSS,MANFCODE('Y','S','S'),"Yellowstone Soft, Germany")\ -X(YTE,MANFCODE('Y','T','E'),"Yuksek Teknoloji, Turkey")\ -X(YTL,MANFCODE('Y','T','L'),"ZheJiang yongtailong electronic, China")\ -X(YYW,MANFCODE('Y','Y','W'),"Shandong youyuan instrument, China")\ -X(YZR,MANFCODE('Y','Z','R'),"Cangzhou City Hebei Electronic Technology, China")\ -X(ZAG,MANFCODE('Z','A','G'),"Zellweger Uster, Switzerland")\ -X(ZAP,MANFCODE('Z','A','P'),"Zaptronix")\ -X(ZAR,MANFCODE('Z','A','R'),"SNC ZARALAB, Bulgaria")\ -X(ZCH,MANFCODE('Z','C','H'),"Zhejiang Chaorong Electric Technology, China")\ -X(ZCU,MANFCODE('Z','C','U'),"Suzhou Dojustek Intelligent Technology, China")\ -X(ZEL,MANFCODE('Z','E','L'),"Dr techn Josef Zelisko, Austria")\ -X(ZEN,MANFCODE('Z','E','N'),"ZenMeter Solutions, India")\ -X(ZEX,MANFCODE('Z','E','X'),"ZEXON ENERGY, India")\ -X(ZFY,MANFCODE('Z','F','Y'),"Hang Zhou Fu Yang Instrument General Factory, China")\ -X(ZGX,MANFCODE('Z','G','X'),"ZIGNUX TECHNOLOGY, Spain")\ -X(ZIP,MANFCODE('Z','I','P'),"St Petersburg Meters Factory, Russian Federation")\ -X(ZIV,MANFCODE('Z','I','V'),"ZIV Aplicaciones y Tecnologia")\ -X(ZJY,MANFCODE('Z','J','Y'),"Zhejiang Jiayou Thermal Technology Equipment, China")\ -X(ZLW,MANFCODE('Z','L','W'),"Ningbo ZLINK Technology, China")\ -X(ZOT,MANFCODE('Z','O','T'),"Zhejiang Ocaenergy Technology")\ -X(ZPA,MANFCODE('Z','P','A'),"ZPA Smart Energy, Czech Republic")\ -X(ZQB,MANFCODE('Z','Q','B'),"Zhejiang Qibin Technology, China")\ -X(ZRI,MANFCODE('Z','R','I'),"ZENNER International, Germany")\ -X(ZRM,MANFCODE('Z','R','M'),"ZENNER International, Germany")\ -X(ZTE,MANFCODE('Z','T','E'),"ZTE Corporation ZTE Plaza, China")\ -X(ZTX,MANFCODE('Z','T','X'),"ZHEJIANG TANCY INSTRUMENT TECHNOLOGY eTD, China")\ -X(ZTY,MANFCODE('Z','T','Y'),"ZHEJIANG CHINT INSTRUMENT METER, China")\ -X(ZVC,MANFCODE('Z','V','C'),"SigmaHeat")\ -X(ZXT,MANFCODE('Z','X','T'),"Zhejiang Xinghao Technology")\ -X(ZXY,MANFCODE('Z','X','Y'),"Linyi City Xiaoyuan Water Meter, China")\ -X(ZYB,MANFCODE('Z','Y','B'),"WEIHAI ZHENYU INTELL TECH, China")\ -X(ZYS,MANFCODE('Z','Y','S'),"Wuxi Zhongyi Smart Technology, China")\ -X(ZZZ,MANFCODE('Z','Z','Z'),"Michael Rac, Germany")\ - -#define MANUFACTURER_AAA MANFCODE('A','A','A') -#define MANUFACTURER_AAB MANFCODE('A','A','B') -#define MANUFACTURER_ABB MANFCODE('A','B','B') -#define MANUFACTURER_ABC MANFCODE('A','B','C') -#define MANUFACTURER_ABN MANFCODE('A','B','N') -#define MANUFACTURER_ABR MANFCODE('A','B','R') -#define MANUFACTURER_ACA MANFCODE('A','C','A') -#define MANUFACTURER_ACB MANFCODE('A','C','B') -#define MANUFACTURER_ACC MANFCODE('A','C','C') -#define MANUFACTURER_ACE MANFCODE('A','C','E') -#define MANUFACTURER_ACG MANFCODE('A','C','G') -#define MANUFACTURER_ACH MANFCODE('A','C','H') -#define MANUFACTURER_ACL MANFCODE('A','C','L') -#define MANUFACTURER_ACN MANFCODE('A','C','N') -#define MANUFACTURER_ACS MANFCODE('A','C','S') -#define MANUFACTURER_ACT MANFCODE('A','C','T') -#define MANUFACTURER_ACW MANFCODE('A','C','W') -#define MANUFACTURER_ADA MANFCODE('A','D','A') -#define MANUFACTURER_ADD MANFCODE('A','D','D') -#define MANUFACTURER_ADE MANFCODE('A','D','E') -#define MANUFACTURER_ADF MANFCODE('A','D','F') -#define MANUFACTURER_ADN MANFCODE('A','D','N') -#define MANUFACTURER_ADR MANFCODE('A','D','R') -#define MANUFACTURER_ADU MANFCODE('A','D','U') -#define MANUFACTURER_ADV MANFCODE('A','D','V') -#define MANUFACTURER_ADX MANFCODE('A','D','X') -#define MANUFACTURER_AEA MANFCODE('A','E','A') -#define MANUFACTURER_AEC MANFCODE('A','E','C') -#define MANUFACTURER_AEE MANFCODE('A','E','E') -#define MANUFACTURER_AEG MANFCODE('A','E','G') -#define MANUFACTURER_AEI MANFCODE('A','E','I') -#define MANUFACTURER_AEL MANFCODE('A','E','L') -#define MANUFACTURER_AEM MANFCODE('A','E','M') -#define MANUFACTURER_AEN MANFCODE('A','E','N') -#define MANUFACTURER_AER MANFCODE('A','E','R') -#define MANUFACTURER_AES MANFCODE('A','E','S') -#define MANUFACTURER_AFS MANFCODE('A','F','S') -#define MANUFACTURER_AFX MANFCODE('A','F','X') -#define MANUFACTURER_AGE MANFCODE('A','G','E') -#define MANUFACTURER_AGI MANFCODE('A','G','I') -#define MANUFACTURER_AGT MANFCODE('A','G','T') -#define MANUFACTURER_AHV MANFCODE('A','H','V') -#define MANUFACTURER_AHX MANFCODE('A','H','X') -#define MANUFACTURER_AIC MANFCODE('A','I','C') -#define MANUFACTURER_AIM MANFCODE('A','I','M') -#define MANUFACTURER_AKK MANFCODE('A','K','K') -#define MANUFACTURER_AKU MANFCODE('A','K','U') -#define MANUFACTURER_ALC MANFCODE('A','L','C') -#define MANUFACTURER_ALE MANFCODE('A','L','E') -#define MANUFACTURER_ALF MANFCODE('A','L','F') -#define MANUFACTURER_ALG MANFCODE('A','L','G') -#define MANUFACTURER_ALH MANFCODE('A','L','H') -#define MANUFACTURER_ALK MANFCODE('A','L','K') -#define MANUFACTURER_ALL MANFCODE('A','L','L') -#define MANUFACTURER_ALP MANFCODE('A','L','P') -#define MANUFACTURER_ALR MANFCODE('A','L','R') -#define MANUFACTURER_ALT MANFCODE('A','L','T') -#define MANUFACTURER_ALV MANFCODE('A','L','V') -#define MANUFACTURER_AMB MANFCODE('A','M','B') -#define MANUFACTURER_AMC MANFCODE('A','M','C') -#define MANUFACTURER_AMD MANFCODE('A','M','D') -#define MANUFACTURER_AME MANFCODE('A','M','E') -#define MANUFACTURER_AMG MANFCODE('A','M','G') -#define MANUFACTURER_AMH MANFCODE('A','M','H') -#define MANUFACTURER_AMI MANFCODE('A','M','I') -#define MANUFACTURER_AML MANFCODE('A','M','L') -#define MANUFACTURER_AMP MANFCODE('A','M','P') -#define MANUFACTURER_AMR MANFCODE('A','M','R') -#define MANUFACTURER_AMS MANFCODE('A','M','S') -#define MANUFACTURER_AMT MANFCODE('A','M','T') -#define MANUFACTURER_AMX MANFCODE('A','M','X') -#define MANUFACTURER_ANA MANFCODE('A','N','A') -#define MANUFACTURER_AND MANFCODE('A','N','D') -#define MANUFACTURER_ANE MANFCODE('A','N','E') -#define MANUFACTURER_ANZ MANFCODE('A','N','Z') -#define MANUFACTURER_AOM MANFCODE('A','O','M') -#define MANUFACTURER_AON MANFCODE('A','O','N') -#define MANUFACTURER_APA MANFCODE('A','P','A') -#define MANUFACTURER_APL MANFCODE('A','P','L') -#define MANUFACTURER_APO MANFCODE('A','P','O') -#define MANUFACTURER_APR MANFCODE('A','P','R') -#define MANUFACTURER_APS MANFCODE('A','P','S') -#define MANUFACTURER_APT MANFCODE('A','P','T') -#define MANUFACTURER_APX MANFCODE('A','P','X') -#define MANUFACTURER_AQA MANFCODE('A','Q','A') -#define MANUFACTURER_AQL MANFCODE('A','Q','L') -#define MANUFACTURER_AQM MANFCODE('A','Q','M') -#define MANUFACTURER_AQN MANFCODE('A','Q','N') -#define MANUFACTURER_AQT MANFCODE('A','Q','T') -#define MANUFACTURER_AQU MANFCODE('A','Q','U') -#define MANUFACTURER_ARA MANFCODE('A','R','A') -#define MANUFACTURER_ARC MANFCODE('A','R','C') -#define MANUFACTURER_ARD MANFCODE('A','R','D') -#define MANUFACTURER_ARF MANFCODE('A','R','F') -#define MANUFACTURER_ARG MANFCODE('A','R','G') -#define MANUFACTURER_ARH MANFCODE('A','R','H') -#define MANUFACTURER_ARK MANFCODE('A','R','K') -#define MANUFACTURER_ARM MANFCODE('A','R','M') -#define MANUFACTURER_ARQ MANFCODE('A','R','Q') -#define MANUFACTURER_ARS MANFCODE('A','R','S') -#define MANUFACTURER_ART MANFCODE('A','R','T') -#define MANUFACTURER_ARW MANFCODE('A','R','W') -#define MANUFACTURER_ARX MANFCODE('A','R','X') -#define MANUFACTURER_ASA MANFCODE('A','S','A') -#define MANUFACTURER_ASC MANFCODE('A','S','C') -#define MANUFACTURER_ASD MANFCODE('A','S','D') -#define MANUFACTURER_ASG MANFCODE('A','S','G') -#define MANUFACTURER_ASH MANFCODE('A','S','H') -#define MANUFACTURER_ASJ MANFCODE('A','S','J') -#define MANUFACTURER_ASM MANFCODE('A','S','M') -#define MANUFACTURER_ASR MANFCODE('A','S','R') -#define MANUFACTURER_AST MANFCODE('A','S','T') -#define MANUFACTURER_ASY MANFCODE('A','S','Y') -#define MANUFACTURER_ATA MANFCODE('A','T','A') -#define MANUFACTURER_ATE MANFCODE('A','T','E') -#define MANUFACTURER_ATF MANFCODE('A','T','F') -#define MANUFACTURER_ATI MANFCODE('A','T','I') -#define MANUFACTURER_ATL MANFCODE('A','T','L') -#define MANUFACTURER_ATM MANFCODE('A','T','M') -#define MANUFACTURER_ATS MANFCODE('A','T','S') -#define MANUFACTURER_AUR MANFCODE('A','U','R') -#define MANUFACTURER_AUT MANFCODE('A','U','T') -#define MANUFACTURER_AUX MANFCODE('A','U','X') -#define MANUFACTURER_AVA MANFCODE('A','V','A') -#define MANUFACTURER_AVE MANFCODE('A','V','E') -#define MANUFACTURER_AXI MANFCODE('A','X','I') -#define MANUFACTURER_AXS MANFCODE('A','X','S') -#define MANUFACTURER_AYS MANFCODE('A','Y','S') -#define MANUFACTURER_AZE MANFCODE('A','Z','E') -#define MANUFACTURER_BAM MANFCODE('B','A','M') -#define MANUFACTURER_BAR MANFCODE('B','A','R') -#define MANUFACTURER_BAS MANFCODE('B','A','S') -#define MANUFACTURER_BBE MANFCODE('B','B','E') -#define MANUFACTURER_BBS MANFCODE('B','B','S') -#define MANUFACTURER_BCE MANFCODE('B','C','E') -#define MANUFACTURER_BCR MANFCODE('B','C','R') -#define MANUFACTURER_BEA MANFCODE('B','E','A') -#define MANUFACTURER_BEE MANFCODE('B','E','E') -#define MANUFACTURER_BEF MANFCODE('B','E','F') -#define MANUFACTURER_BEG MANFCODE('B','E','G') -#define MANUFACTURER_BEL MANFCODE('B','E','L') -#define MANUFACTURER_BEN MANFCODE('B','E','N') -#define MANUFACTURER_BER MANFCODE('B','E','R') -#define MANUFACTURER_BFW MANFCODE('B','F','W') -#define MANUFACTURER_BHG MANFCODE('B','H','G') -#define MANUFACTURER_BIM MANFCODE('B','I','M') -#define MANUFACTURER_BJY MANFCODE('B','J','Y') -#define MANUFACTURER_BKB MANFCODE('B','K','B') -#define MANUFACTURER_BKO MANFCODE('B','K','O') -#define MANUFACTURER_BKT MANFCODE('B','K','T') -#define MANUFACTURER_BLM MANFCODE('B','L','M') -#define MANUFACTURER_BLU MANFCODE('B','L','U') -#define MANUFACTURER_BME MANFCODE('B','M','E') -#define MANUFACTURER_BMI MANFCODE('B','M','I') -#define MANUFACTURER_BMP MANFCODE('B','M','P') -#define MANUFACTURER_BMT MANFCODE('B','M','T') -#define MANUFACTURER_BNR MANFCODE('B','N','R') -#define MANUFACTURER_BOV MANFCODE('B','O','V') -#define MANUFACTURER_BPM MANFCODE('B','P','M') -#define MANUFACTURER_BRA MANFCODE('B','R','A') -#define MANUFACTURER_BRC MANFCODE('B','R','C') -#define MANUFACTURER_BRN MANFCODE('B','R','N') -#define MANUFACTURER_BRT MANFCODE('B','R','T') -#define MANUFACTURER_BRU MANFCODE('B','R','U') -#define MANUFACTURER_BSC MANFCODE('B','S','C') -#define MANUFACTURER_BSD MANFCODE('B','S','D') -#define MANUFACTURER_BSE MANFCODE('B','S','E') -#define MANUFACTURER_BSI MANFCODE('B','S','I') -#define MANUFACTURER_BSM MANFCODE('B','S','M') -#define MANUFACTURER_BSP MANFCODE('B','S','P') -#define MANUFACTURER_BSS MANFCODE('B','S','S') -#define MANUFACTURER_BST MANFCODE('B','S','T') -#define MANUFACTURER_BSX MANFCODE('B','S','X') -#define MANUFACTURER_BSY MANFCODE('B','S','Y') -#define MANUFACTURER_BTA MANFCODE('B','T','A') -#define MANUFACTURER_BTL MANFCODE('B','T','L') -#define MANUFACTURER_BTM MANFCODE('B','T','M') -#define MANUFACTURER_BTR MANFCODE('B','T','R') -#define MANUFACTURER_BTS MANFCODE('B','T','S') -#define MANUFACTURER_BUR MANFCODE('B','U','R') -#define MANUFACTURER_BUY MANFCODE('B','U','Y') -#define MANUFACTURER_BXC MANFCODE('B','X','C') -#define MANUFACTURER_BYD MANFCODE('B','Y','D') -#define MANUFACTURER_BYL MANFCODE('B','Y','L') -#define MANUFACTURER_BYW MANFCODE('B','Y','W') -#define MANUFACTURER_BZR MANFCODE('B','Z','R') -#define MANUFACTURER_CAB MANFCODE('C','A','B') -#define MANUFACTURER_CAD MANFCODE('C','A','D') -#define MANUFACTURER_CAH MANFCODE('C','A','H') -#define MANUFACTURER_CAL MANFCODE('C','A','L') -#define MANUFACTURER_CAN MANFCODE('C','A','N') -#define MANUFACTURER_CAR MANFCODE('C','A','R') -#define MANUFACTURER_CAT MANFCODE('C','A','T') -#define MANUFACTURER_CAV MANFCODE('C','A','V') -#define MANUFACTURER_CBF MANFCODE('C','B','F') -#define MANUFACTURER_CBI MANFCODE('C','B','I') -#define MANUFACTURER_CBS MANFCODE('C','B','S') -#define MANUFACTURER_CCP MANFCODE('C','C','P') -#define MANUFACTURER_CCS MANFCODE('C','C','S') -#define MANUFACTURER_CDA MANFCODE('C','D','A') -#define MANUFACTURER_CDL MANFCODE('C','D','L') -#define MANUFACTURER_CDT MANFCODE('C','D','T') -#define MANUFACTURER_CEB MANFCODE('C','E','B') -#define MANUFACTURER_CED MANFCODE('C','E','D') -#define MANUFACTURER_CEG MANFCODE('C','E','G') -#define MANUFACTURER_CEH MANFCODE('C','E','H') -#define MANUFACTURER_CEL MANFCODE('C','E','L') -#define MANUFACTURER_CEM MANFCODE('C','E','M') -#define MANUFACTURER_CET MANFCODE('C','E','T') -#define MANUFACTURER_CGC MANFCODE('C','G','C') -#define MANUFACTURER_CGO MANFCODE('C','G','O') -#define MANUFACTURER_CHA MANFCODE('C','H','A') -#define MANUFACTURER_CHC MANFCODE('C','H','C') -#define MANUFACTURER_CHE MANFCODE('C','H','E') -#define MANUFACTURER_CHM MANFCODE('C','H','M') -#define MANUFACTURER_CHS MANFCODE('C','H','S') -#define MANUFACTURER_CIC MANFCODE('C','I','C') -#define MANUFACTURER_CIR MANFCODE('C','I','R') -#define MANUFACTURER_CJS MANFCODE('C','J','S') -#define MANUFACTURER_CKT MANFCODE('C','K','T') -#define MANUFACTURER_CLA MANFCODE('C','L','A') -#define MANUFACTURER_CLB MANFCODE('C','L','B') -#define MANUFACTURER_CLE MANFCODE('C','L','E') -#define MANUFACTURER_CLO MANFCODE('C','L','O') -#define MANUFACTURER_CLS MANFCODE('C','L','S') -#define MANUFACTURER_CLT MANFCODE('C','L','T') -#define MANUFACTURER_CLX MANFCODE('C','L','X') -#define MANUFACTURER_CLY MANFCODE('C','L','Y') -#define MANUFACTURER_CMC MANFCODE('C','M','C') -#define MANUFACTURER_CMP MANFCODE('C','M','P') -#define MANUFACTURER_CMS MANFCODE('C','M','S') -#define MANUFACTURER_CMT MANFCODE('C','M','T') -#define MANUFACTURER_CMV MANFCODE('C','M','V') -#define MANUFACTURER_CNM MANFCODE('C','N','M') -#define MANUFACTURER_CNU MANFCODE('C','N','U') -#define MANUFACTURER_COH MANFCODE('C','O','H') -#define MANUFACTURER_COM MANFCODE('C','O','M') -#define MANUFACTURER_CON MANFCODE('C','O','N') -#define MANUFACTURER_CPG MANFCODE('C','P','G') -#define MANUFACTURER_CPL MANFCODE('C','P','L') -#define MANUFACTURER_CPO MANFCODE('C','P','O') -#define MANUFACTURER_CPS MANFCODE('C','P','S') -#define MANUFACTURER_CQC MANFCODE('C','Q','C') -#define MANUFACTURER_CRD MANFCODE('C','R','D') -#define MANUFACTURER_CRT MANFCODE('C','R','T') -#define MANUFACTURER_CRW MANFCODE('C','R','W') -#define MANUFACTURER_CRX MANFCODE('C','R','X') -#define MANUFACTURER_CRY MANFCODE('C','R','Y') -#define MANUFACTURER_CSC MANFCODE('C','S','C') -#define MANUFACTURER_CSL MANFCODE('C','S','L') -#define MANUFACTURER_CSP MANFCODE('C','S','P') -#define MANUFACTURER_CSW MANFCODE('C','S','W') -#define MANUFACTURER_CTE MANFCODE('C','T','E') -#define MANUFACTURER_CTL MANFCODE('C','T','L') -#define MANUFACTURER_CTP MANFCODE('C','T','P') -#define MANUFACTURER_CTQ MANFCODE('C','T','Q') -#define MANUFACTURER_CTR MANFCODE('C','T','R') -#define MANUFACTURER_CTT MANFCODE('C','T','T') -#define MANUFACTURER_CTX MANFCODE('C','T','X') -#define MANUFACTURER_CUB MANFCODE('C','U','B') -#define MANUFACTURER_CUC MANFCODE('C','U','C') -#define MANUFACTURER_CUR MANFCODE('C','U','R') -#define MANUFACTURER_CWA MANFCODE('C','W','A') -#define MANUFACTURER_CWI MANFCODE('C','W','I') -#define MANUFACTURER_CWV MANFCODE('C','W','V') -#define MANUFACTURER_CYE MANFCODE('C','Y','E') -#define MANUFACTURER_CYI MANFCODE('C','Y','I') -#define MANUFACTURER_CYN MANFCODE('C','Y','N') -#define MANUFACTURER_CZA MANFCODE('C','Z','A') -#define MANUFACTURER_CZM MANFCODE('C','Z','M') -#define MANUFACTURER_DAE MANFCODE('D','A','E') -#define MANUFACTURER_DAF MANFCODE('D','A','F') -#define MANUFACTURER_DAN MANFCODE('D','A','N') -#define MANUFACTURER_DBD MANFCODE('D','B','D') -#define MANUFACTURER_DBE MANFCODE('D','B','E') -#define MANUFACTURER_DCD MANFCODE('D','C','D') -#define MANUFACTURER_DCO MANFCODE('D','C','O') -#define MANUFACTURER_DCZ MANFCODE('D','C','Z') -#define MANUFACTURER_DDE MANFCODE('D','D','E') -#define MANUFACTURER_DDI MANFCODE('D','D','I') -#define MANUFACTURER_DDL MANFCODE('D','D','L') -#define MANUFACTURER_DEA MANFCODE('D','E','A') -#define MANUFACTURER_DEC MANFCODE('D','E','C') -#define MANUFACTURER_DEI MANFCODE('D','E','I') -#define MANUFACTURER_DEL MANFCODE('D','E','L') -#define MANUFACTURER_DEN MANFCODE('D','E','N') -#define MANUFACTURER_DES MANFCODE('D','E','S') -#define MANUFACTURER_DEV MANFCODE('D','E','V') -#define MANUFACTURER_DFE MANFCODE('D','F','E') -#define MANUFACTURER_DFS MANFCODE('D','F','S') -#define MANUFACTURER_DGC MANFCODE('D','G','C') -#define MANUFACTURER_DGI MANFCODE('D','G','I') -#define MANUFACTURER_DGM MANFCODE('D','G','M') -#define MANUFACTURER_DGY MANFCODE('D','G','Y') -#define MANUFACTURER_DHA MANFCODE('D','H','A') -#define MANUFACTURER_DHQ MANFCODE('D','H','Q') -#define MANUFACTURER_DIE MANFCODE('D','I','E') -#define MANUFACTURER_DIG MANFCODE('D','I','G') -#define MANUFACTURER_DIL MANFCODE('D','I','L') -#define MANUFACTURER_DJA MANFCODE('D','J','A') -#define MANUFACTURER_DKY MANFCODE('D','K','Y') -#define MANUFACTURER_DLX MANFCODE('D','L','X') -#define MANUFACTURER_DMC MANFCODE('D','M','C') -#define MANUFACTURER_DME MANFCODE('D','M','E') -#define MANUFACTURER_DMG MANFCODE('D','M','G') -#define MANUFACTURER_DMI MANFCODE('D','M','I') -#define MANUFACTURER_DMO MANFCODE('D','M','O') -#define MANUFACTURER_DMP MANFCODE('D','M','P') -#define MANUFACTURER_DMS MANFCODE('D','M','S') -#define MANUFACTURER_DNB MANFCODE('D','N','B') -#define MANUFACTURER_DNC MANFCODE('D','N','C') -#define MANUFACTURER_DNO MANFCODE('D','N','O') -#define MANUFACTURER_DNT MANFCODE('D','N','T') -#define MANUFACTURER_DNV MANFCODE('D','N','V') -#define MANUFACTURER_DOJ MANFCODE('D','O','J') -#define MANUFACTURER_DOM MANFCODE('D','O','M') -#define MANUFACTURER_DOS MANFCODE('D','O','S') -#define MANUFACTURER_DPM MANFCODE('D','P','M') -#define MANUFACTURER_DPP MANFCODE('D','P','P') -#define MANUFACTURER_DRC MANFCODE('D','R','C') -#define MANUFACTURER_DRP MANFCODE('D','R','P') -#define MANUFACTURER_DRT MANFCODE('D','R','T') -#define MANUFACTURER_DSE MANFCODE('D','S','E') -#define MANUFACTURER_DSM MANFCODE('D','S','M') -#define MANUFACTURER_DST MANFCODE('D','S','T') -#define MANUFACTURER_DTE MANFCODE('D','T','E') -#define MANUFACTURER_DUA MANFCODE('D','U','A') -#define MANUFACTURER_DUS MANFCODE('D','U','S') -#define MANUFACTURER_DVE MANFCODE('D','V','E') -#define MANUFACTURER_DVG MANFCODE('D','V','G') -#define MANUFACTURER_DVL MANFCODE('D','V','L') -#define MANUFACTURER_DWY MANFCODE('D','W','Y') -#define MANUFACTURER_DWZ MANFCODE('D','W','Z') -#define MANUFACTURER_DZG MANFCODE('D','Z','G') -#define MANUFACTURER_EAA MANFCODE('E','A','A') -#define MANUFACTURER_EAD MANFCODE('E','A','D') -#define MANUFACTURER_EAH MANFCODE('E','A','H') -#define MANUFACTURER_EAS MANFCODE('E','A','S') -#define MANUFACTURER_EBE MANFCODE('E','B','E') -#define MANUFACTURER_EBK MANFCODE('E','B','K') -#define MANUFACTURER_EBV MANFCODE('E','B','V') -#define MANUFACTURER_EBZ MANFCODE('E','B','Z') -#define MANUFACTURER_ECA MANFCODE('E','C','A') -#define MANUFACTURER_ECB MANFCODE('E','C','B') -#define MANUFACTURER_ECC MANFCODE('E','C','C') -#define MANUFACTURER_ECH MANFCODE('E','C','H') -#define MANUFACTURER_ECI MANFCODE('E','C','I') -#define MANUFACTURER_ECL MANFCODE('E','C','L') -#define MANUFACTURER_ECM MANFCODE('E','C','M') -#define MANUFACTURER_ECO MANFCODE('E','C','O') -#define MANUFACTURER_ECS MANFCODE('E','C','S') -#define MANUFACTURER_EDI MANFCODE('E','D','I') -#define MANUFACTURER_EDM MANFCODE('E','D','M') -#define MANUFACTURER_EDP MANFCODE('E','D','P') -#define MANUFACTURER_EEC MANFCODE('E','E','C') -#define MANUFACTURER_EEE MANFCODE('E','E','E') -#define MANUFACTURER_EEI MANFCODE('E','E','I') -#define MANUFACTURER_EEM MANFCODE('E','E','M') -#define MANUFACTURER_EEO MANFCODE('E','E','O') -#define MANUFACTURER_EEP MANFCODE('E','E','P') -#define MANUFACTURER_EES MANFCODE('E','E','S') -#define MANUFACTURER_EFA MANFCODE('E','F','A') -#define MANUFACTURER_EFE MANFCODE('E','F','E') -#define MANUFACTURER_EFI MANFCODE('E','F','I') -#define MANUFACTURER_EFN MANFCODE('E','F','N') -#define MANUFACTURER_EFR MANFCODE('E','F','R') -#define MANUFACTURER_EFS MANFCODE('E','F','S') -#define MANUFACTURER_EGA MANFCODE('E','G','A') -#define MANUFACTURER_EGC MANFCODE('E','G','C') -#define MANUFACTURER_EGD MANFCODE('E','G','D') -#define MANUFACTURER_EGM MANFCODE('E','G','M') -#define MANUFACTURER_EGT MANFCODE('E','G','T') -#define MANUFACTURER_EGW MANFCODE('E','G','W') -#define MANUFACTURER_EGY MANFCODE('E','G','Y') -#define MANUFACTURER_EHL MANFCODE('E','H','L') -#define MANUFACTURER_EHS MANFCODE('E','H','S') -#define MANUFACTURER_EIE MANFCODE('E','I','E') -#define MANUFACTURER_EIG MANFCODE('E','I','G') -#define MANUFACTURER_EIP MANFCODE('E','I','P') -#define MANUFACTURER_EIT MANFCODE('E','I','T') -#define MANUFACTURER_EKA MANFCODE('E','K','A') -#define MANUFACTURER_EKF MANFCODE('E','K','F') -#define MANUFACTURER_EKO MANFCODE('E','K','O') -#define MANUFACTURER_EKS MANFCODE('E','K','S') -#define MANUFACTURER_EKT MANFCODE('E','K','T') -#define MANUFACTURER_ELC MANFCODE('E','L','C') -#define MANUFACTURER_ELD MANFCODE('E','L','D') -#define MANUFACTURER_ELE MANFCODE('E','L','E') -#define MANUFACTURER_ELG MANFCODE('E','L','G') -#define MANUFACTURER_ELL MANFCODE('E','L','L') -#define MANUFACTURER_ELM MANFCODE('E','L','M') -#define MANUFACTURER_ELO MANFCODE('E','L','O') -#define MANUFACTURER_ELP MANFCODE('E','L','P') -#define MANUFACTURER_ELQ MANFCODE('E','L','Q') -#define MANUFACTURER_ELR MANFCODE('E','L','R') -#define MANUFACTURER_ELS MANFCODE('E','L','S') -#define MANUFACTURER_ELT MANFCODE('E','L','T') -#define MANUFACTURER_ELV MANFCODE('E','L','V') -#define MANUFACTURER_ELY MANFCODE('E','L','Y') -#define MANUFACTURER_EMB MANFCODE('E','M','B') -#define MANUFACTURER_EMC MANFCODE('E','M','C') -#define MANUFACTURER_EME MANFCODE('E','M','E') -#define MANUFACTURER_EMF MANFCODE('E','M','F') -#define MANUFACTURER_EMH MANFCODE('E','M','H') -#define MANUFACTURER_EMI MANFCODE('E','M','I') -#define MANUFACTURER_EML MANFCODE('E','M','L') -#define MANUFACTURER_EMM MANFCODE('E','M','M') -#define MANUFACTURER_EMO MANFCODE('E','M','O') -#define MANUFACTURER_EMR MANFCODE('E','M','R') -#define MANUFACTURER_EMS MANFCODE('E','M','S') -#define MANUFACTURER_EMT MANFCODE('E','M','T') -#define MANUFACTURER_EMU MANFCODE('E','M','U') -#define MANUFACTURER_ENC MANFCODE('E','N','C') -#define MANUFACTURER_END MANFCODE('E','N','D') -#define MANUFACTURER_ENE MANFCODE('E','N','E') -#define MANUFACTURER_ENG MANFCODE('E','N','G') -#define MANUFACTURER_ENH MANFCODE('E','N','H') -#define MANUFACTURER_ENI MANFCODE('E','N','I') -#define MANUFACTURER_ENL MANFCODE('E','N','L') -#define MANUFACTURER_ENN MANFCODE('E','N','N') -#define MANUFACTURER_ENO MANFCODE('E','N','O') -#define MANUFACTURER_ENP MANFCODE('E','N','P') -#define MANUFACTURER_ENR MANFCODE('E','N','R') -#define MANUFACTURER_ENS MANFCODE('E','N','S') -#define MANUFACTURER_ENT MANFCODE('E','N','T') -#define MANUFACTURER_ENX MANFCODE('E','N','X') -#define MANUFACTURER_EPI MANFCODE('E','P','I') -#define MANUFACTURER_EPL MANFCODE('E','P','L') -#define MANUFACTURER_EPT MANFCODE('E','P','T') -#define MANUFACTURER_ERE MANFCODE('E','R','E') -#define MANUFACTURER_ERI MANFCODE('E','R','I') -#define MANUFACTURER_ERL MANFCODE('E','R','L') -#define MANUFACTURER_ERN MANFCODE('E','R','N') -#define MANUFACTURER_ERS MANFCODE('E','R','S') -#define MANUFACTURER_ERW MANFCODE('E','R','W') -#define MANUFACTURER_ESA MANFCODE('E','S','A') -#define MANUFACTURER_ESC MANFCODE('E','S','C') -#define MANUFACTURER_ESD MANFCODE('E','S','D') -#define MANUFACTURER_ESE MANFCODE('E','S','E') -#define MANUFACTURER_ESG MANFCODE('E','S','G') -#define MANUFACTURER_ESH MANFCODE('E','S','H') -#define MANUFACTURER_ESI MANFCODE('E','S','I') -#define MANUFACTURER_ESL MANFCODE('E','S','L') -#define MANUFACTURER_ESM MANFCODE('E','S','M') -#define MANUFACTURER_ESN MANFCODE('E','S','N') -#define MANUFACTURER_ESO MANFCODE('E','S','O') -#define MANUFACTURER_ESP MANFCODE('E','S','P') -#define MANUFACTURER_ESR MANFCODE('E','S','R') -#define MANUFACTURER_ESS MANFCODE('E','S','S') -#define MANUFACTURER_ESV MANFCODE('E','S','V') -#define MANUFACTURER_ESY MANFCODE('E','S','Y') -#define MANUFACTURER_ETD MANFCODE('E','T','D') -#define MANUFACTURER_ETK MANFCODE('E','T','K') -#define MANUFACTURER_ETO MANFCODE('E','T','O') -#define MANUFACTURER_ETP MANFCODE('E','T','P') -#define MANUFACTURER_EUE MANFCODE('E','U','E') -#define MANUFACTURER_EUR MANFCODE('E','U','R') -#define MANUFACTURER_EUS MANFCODE('E','U','S') -#define MANUFACTURER_EVD MANFCODE('E','V','D') -#define MANUFACTURER_EVK MANFCODE('E','V','K') -#define MANUFACTURER_EVL MANFCODE('E','V','L') -#define MANUFACTURER_EVT MANFCODE('E','V','T') -#define MANUFACTURER_EWA MANFCODE('E','W','A') -#define MANUFACTURER_EWE MANFCODE('E','W','E') -#define MANUFACTURER_EWG MANFCODE('E','W','G') -#define MANUFACTURER_EWS MANFCODE('E','W','S') -#define MANUFACTURER_EWT MANFCODE('E','W','T') -#define MANUFACTURER_EXS MANFCODE('E','X','S') -#define MANUFACTURER_EXT MANFCODE('E','X','T') -#define MANUFACTURER_EYE MANFCODE('E','Y','E') -#define MANUFACTURER_EYT MANFCODE('E','Y','T') -#define MANUFACTURER_FAC MANFCODE('F','A','C') -#define MANUFACTURER_FAE MANFCODE('F','A','E') -#define MANUFACTURER_FAN MANFCODE('F','A','N') -#define MANUFACTURER_FAR MANFCODE('F','A','R') -#define MANUFACTURER_FAS MANFCODE('F','A','S') -#define MANUFACTURER_FED MANFCODE('F','E','D') -#define MANUFACTURER_FEI MANFCODE('F','E','I') -#define MANUFACTURER_FEM MANFCODE('F','E','M') -#define MANUFACTURER_FFD MANFCODE('F','F','D') -#define MANUFACTURER_FID MANFCODE('F','I','D') -#define MANUFACTURER_FIM MANFCODE('F','I','M') -#define MANUFACTURER_FIN MANFCODE('F','I','N') -#define MANUFACTURER_FIO MANFCODE('F','I','O') -#define MANUFACTURER_FLA MANFCODE('F','L','A') -#define MANUFACTURER_FLD MANFCODE('F','L','D') -#define MANUFACTURER_FLE MANFCODE('F','L','E') -#define MANUFACTURER_FLG MANFCODE('F','L','G') -#define MANUFACTURER_FLO MANFCODE('F','L','O') -#define MANUFACTURER_FLS MANFCODE('F','L','S') -#define MANUFACTURER_FLU MANFCODE('F','L','U') -#define MANUFACTURER_FLW MANFCODE('F','L','W') -#define MANUFACTURER_FLX MANFCODE('F','L','X') -#define MANUFACTURER_FMG MANFCODE('F','M','G') -#define MANUFACTURER_FML MANFCODE('F','M','L') -#define MANUFACTURER_FMM MANFCODE('F','M','M') -#define MANUFACTURER_FND MANFCODE('F','N','D') -#define MANUFACTURER_FNF MANFCODE('F','N','F') -#define MANUFACTURER_FNX MANFCODE('F','N','X') -#define MANUFACTURER_FOX MANFCODE('F','O','X') -#define MANUFACTURER_FPL MANFCODE('F','P','L') -#define MANUFACTURER_FPR MANFCODE('F','P','R') -#define MANUFACTURER_FRC MANFCODE('F','R','C') -#define MANUFACTURER_FRE MANFCODE('F','R','E') -#define MANUFACTURER_FRU MANFCODE('F','R','U') -#define MANUFACTURER_FSE MANFCODE('F','S','E') -#define MANUFACTURER_FSP MANFCODE('F','S','P') -#define MANUFACTURER_FST MANFCODE('F','S','T') -#define MANUFACTURER_FSY MANFCODE('F','S','Y') -#define MANUFACTURER_FTL MANFCODE('F','T','L') -#define MANUFACTURER_FUN MANFCODE('F','U','N') -#define MANUFACTURER_FUS MANFCODE('F','U','S') -#define MANUFACTURER_FUT MANFCODE('F','U','T') -#define MANUFACTURER_FWS MANFCODE('F','W','S') -#define MANUFACTURER_FZK MANFCODE('F','Z','K') -#define MANUFACTURER_GAT MANFCODE('G','A','T') -#define MANUFACTURER_GAV MANFCODE('G','A','V') -#define MANUFACTURER_GBJ MANFCODE('G','B','J') -#define MANUFACTURER_GBX MANFCODE('G','B','X') -#define MANUFACTURER_GCE MANFCODE('G','C','E') -#define MANUFACTURER_GCG MANFCODE('G','C','G') -#define MANUFACTURER_GCX MANFCODE('G','C','X') -#define MANUFACTURER_GDC MANFCODE('G','D','C') -#define MANUFACTURER_GDS MANFCODE('G','D','S') -#define MANUFACTURER_GEC MANFCODE('G','E','C') -#define MANUFACTURER_GEE MANFCODE('G','E','E') -#define MANUFACTURER_GEL MANFCODE('G','E','L') -#define MANUFACTURER_GEN MANFCODE('G','E','N') -#define MANUFACTURER_GEO MANFCODE('G','E','O') -#define MANUFACTURER_GEP MANFCODE('G','E','P') -#define MANUFACTURER_GET MANFCODE('G','E','T') -#define MANUFACTURER_GEX MANFCODE('G','E','X') -#define MANUFACTURER_GFM MANFCODE('G','F','M') -#define MANUFACTURER_GIL MANFCODE('G','I','L') -#define MANUFACTURER_GIN MANFCODE('G','I','N') -#define MANUFACTURER_GIO MANFCODE('G','I','O') -#define MANUFACTURER_GKA MANFCODE('G','K','A') -#define MANUFACTURER_GKM MANFCODE('G','K','M') -#define MANUFACTURER_GLM MANFCODE('G','L','M') -#define MANUFACTURER_GLO MANFCODE('G','L','O') -#define MANUFACTURER_GLX MANFCODE('G','L','X') -#define MANUFACTURER_GMC MANFCODE('G','M','C') -#define MANUFACTURER_GME MANFCODE('G','M','E') -#define MANUFACTURER_GMM MANFCODE('G','M','M') -#define MANUFACTURER_GMS MANFCODE('G','M','S') -#define MANUFACTURER_GMT MANFCODE('G','M','T') -#define MANUFACTURER_GNP MANFCODE('G','N','P') -#define MANUFACTURER_GNY MANFCODE('G','N','Y') -#define MANUFACTURER_GOE MANFCODE('G','O','E') -#define MANUFACTURER_GOM MANFCODE('G','O','M') -#define MANUFACTURER_GPM MANFCODE('G','P','M') -#define MANUFACTURER_GPT MANFCODE('G','P','T') -#define MANUFACTURER_GRA MANFCODE('G','R','A') -#define MANUFACTURER_GRD MANFCODE('G','R','D') -#define MANUFACTURER_GRE MANFCODE('G','R','E') -#define MANUFACTURER_GRI MANFCODE('G','R','I') -#define MANUFACTURER_GRN MANFCODE('G','R','N') -#define MANUFACTURER_GRP MANFCODE('G','R','P') -#define MANUFACTURER_GRS MANFCODE('G','R','S') -#define MANUFACTURER_GRU MANFCODE('G','R','U') -#define MANUFACTURER_GRV MANFCODE('G','R','V') -#define MANUFACTURER_GRX MANFCODE('G','R','X') -#define MANUFACTURER_GSD MANFCODE('G','S','D') -#define MANUFACTURER_GSE MANFCODE('G','S','E') -#define MANUFACTURER_GSP MANFCODE('G','S','P') -#define MANUFACTURER_GSS MANFCODE('G','S','S') -#define MANUFACTURER_GST MANFCODE('G','S','T') -#define MANUFACTURER_GTE MANFCODE('G','T','E') -#define MANUFACTURER_GTM MANFCODE('G','T','M') -#define MANUFACTURER_GTR MANFCODE('G','T','R') -#define MANUFACTURER_GTS MANFCODE('G','T','S') -#define MANUFACTURER_GUH MANFCODE('G','U','H') -#define MANUFACTURER_GVL MANFCODE('G','V','L') -#define MANUFACTURER_GWF MANFCODE('G','W','F') -#define MANUFACTURER_GWI MANFCODE('G','W','I') -#define MANUFACTURER_HAG MANFCODE('H','A','G') -#define MANUFACTURER_HAK MANFCODE('H','A','K') -#define MANUFACTURER_HBY MANFCODE('H','B','Y') -#define MANUFACTURER_HCE MANFCODE('H','C','E') -#define MANUFACTURER_HCM MANFCODE('H','C','M') -#define MANUFACTURER_HDA MANFCODE('H','D','A') -#define MANUFACTURER_HDK MANFCODE('H','D','K') -#define MANUFACTURER_HDX MANFCODE('H','D','X') -#define MANUFACTURER_HDY MANFCODE('H','D','Y') -#define MANUFACTURER_HEG MANFCODE('H','E','G') -#define MANUFACTURER_HEI MANFCODE('H','E','I') -#define MANUFACTURER_HEK MANFCODE('H','E','K') -#define MANUFACTURER_HEL MANFCODE('H','E','L') -#define MANUFACTURER_HEM MANFCODE('H','E','M') -#define MANUFACTURER_HER MANFCODE('H','E','R') -#define MANUFACTURER_HEW MANFCODE('H','E','W') -#define MANUFACTURER_HEX MANFCODE('H','E','X') -#define MANUFACTURER_HFI MANFCODE('H','F','I') -#define MANUFACTURER_HFL MANFCODE('H','F','L') -#define MANUFACTURER_HFR MANFCODE('H','F','R') -#define MANUFACTURER_HGM MANFCODE('H','G','M') -#define MANUFACTURER_HGP MANFCODE('H','G','P') -#define MANUFACTURER_HHE MANFCODE('H','H','E') -#define MANUFACTURER_HHM MANFCODE('H','H','M') -#define MANUFACTURER_HIE MANFCODE('H','I','E') -#define MANUFACTURER_HIN MANFCODE('H','I','N') -#define MANUFACTURER_HIT MANFCODE('H','I','T') -#define MANUFACTURER_HJM MANFCODE('H','J','M') -#define MANUFACTURER_HKK MANFCODE('H','K','K') -#define MANUFACTURER_HKT MANFCODE('H','K','T') -#define MANUFACTURER_HLD MANFCODE('H','L','D') -#define MANUFACTURER_HLT MANFCODE('H','L','T') -#define MANUFACTURER_HLY MANFCODE('H','L','Y') -#define MANUFACTURER_HMI MANFCODE('H','M','I') -#define MANUFACTURER_HML MANFCODE('H','M','L') -#define MANUFACTURER_HMS MANFCODE('H','M','S') -#define MANUFACTURER_HMT MANFCODE('H','M','T') -#define MANUFACTURER_HMU MANFCODE('H','M','U') -#define MANUFACTURER_HNA MANFCODE('H','N','A') -#define MANUFACTURER_HND MANFCODE('H','N','D') -#define MANUFACTURER_HNN MANFCODE('H','N','N') -#define MANUFACTURER_HOE MANFCODE('H','O','E') -#define MANUFACTURER_HOL MANFCODE('H','O','L') -#define MANUFACTURER_HON MANFCODE('H','O','N') -#define MANUFACTURER_HOY MANFCODE('H','O','Y') -#define MANUFACTURER_HPL MANFCODE('H','P','L') -#define MANUFACTURER_HPM MANFCODE('H','P','M') -#define MANUFACTURER_HQL MANFCODE('H','Q','L') -#define MANUFACTURER_HRM MANFCODE('H','R','M') -#define MANUFACTURER_HRS MANFCODE('H','R','S') -#define MANUFACTURER_HRZ MANFCODE('H','R','Z') -#define MANUFACTURER_HSD MANFCODE('H','S','D') -#define MANUFACTURER_HST MANFCODE('H','S','T') -#define MANUFACTURER_HSX MANFCODE('H','S','X') -#define MANUFACTURER_HTC MANFCODE('H','T','C') -#define MANUFACTURER_HTE MANFCODE('H','T','E') -#define MANUFACTURER_HTI MANFCODE('H','T','I') -#define MANUFACTURER_HTL MANFCODE('H','T','L') -#define MANUFACTURER_HTS MANFCODE('H','T','S') -#define MANUFACTURER_HUK MANFCODE('H','U','K') -#define MANUFACTURER_HUM MANFCODE('H','U','M') -#define MANUFACTURER_HVG MANFCODE('H','V','G') -#define MANUFACTURER_HVT MANFCODE('H','V','T') -#define MANUFACTURER_HWC MANFCODE('H','W','C') -#define MANUFACTURER_HWM MANFCODE('H','W','M') -#define MANUFACTURER_HWT MANFCODE('H','W','T') -#define MANUFACTURER_HXD MANFCODE('H','X','D') -#define MANUFACTURER_HXE MANFCODE('H','X','E') -#define MANUFACTURER_HXT MANFCODE('H','X','T') -#define MANUFACTURER_HXW MANFCODE('H','X','W') -#define MANUFACTURER_HYC MANFCODE('H','Y','C') -#define MANUFACTURER_HYD MANFCODE('H','Y','D') -#define MANUFACTURER_HYE MANFCODE('H','Y','E') -#define MANUFACTURER_HYG MANFCODE('H','Y','G') -#define MANUFACTURER_HYM MANFCODE('H','Y','M') -#define MANUFACTURER_HYS MANFCODE('H','Y','S') -#define MANUFACTURER_HYT MANFCODE('H','Y','T') -#define MANUFACTURER_HZC MANFCODE('H','Z','C') -#define MANUFACTURER_HZI MANFCODE('H','Z','I') -#define MANUFACTURER_HZZ MANFCODE('H','Z','Z') -#define MANUFACTURER_IAC MANFCODE('I','A','C') -#define MANUFACTURER_IBE MANFCODE('I','B','E') -#define MANUFACTURER_IBG MANFCODE('I','B','G') -#define MANUFACTURER_IBT MANFCODE('I','B','T') -#define MANUFACTURER_ICB MANFCODE('I','C','B') -#define MANUFACTURER_ICE MANFCODE('I','C','E') -#define MANUFACTURER_ICK MANFCODE('I','C','K') -#define MANUFACTURER_ICM MANFCODE('I','C','M') -#define MANUFACTURER_ICP MANFCODE('I','C','P') -#define MANUFACTURER_ICS MANFCODE('I','C','S') -#define MANUFACTURER_ICT MANFCODE('I','C','T') -#define MANUFACTURER_ICU MANFCODE('I','C','U') -#define MANUFACTURER_IDE MANFCODE('I','D','E') -#define MANUFACTURER_IDS MANFCODE('I','D','S') -#define MANUFACTURER_IEC MANFCODE('I','E','C') -#define MANUFACTURER_IEE MANFCODE('I','E','E') -#define MANUFACTURER_IEI MANFCODE('I','E','I') -#define MANUFACTURER_IEN MANFCODE('I','E','N') -#define MANUFACTURER_IES MANFCODE('I','E','S') -#define MANUFACTURER_IFC MANFCODE('I','F','C') -#define MANUFACTURER_IFX MANFCODE('I','F','X') -#define MANUFACTURER_IGE MANFCODE('I','G','E') -#define MANUFACTURER_IGM MANFCODE('I','G','M') -#define MANUFACTURER_IGR MANFCODE('I','G','R') -#define MANUFACTURER_IGS MANFCODE('I','G','S') -#define MANUFACTURER_IHM MANFCODE('I','H','M') -#define MANUFACTURER_IID MANFCODE('I','I','D') -#define MANUFACTURER_IIF MANFCODE('I','I','F') -#define MANUFACTURER_IJE MANFCODE('I','J','E') -#define MANUFACTURER_IJK MANFCODE('I','J','K') -#define MANUFACTURER_IKE MANFCODE('I','K','E') -#define MANUFACTURER_IKM MANFCODE('I','K','M') -#define MANUFACTURER_IKO MANFCODE('I','K','O') -#define MANUFACTURER_IKS MANFCODE('I','K','S') -#define MANUFACTURER_IMC MANFCODE('I','M','C') -#define MANUFACTURER_IME MANFCODE('I','M','E') -#define MANUFACTURER_IMR MANFCODE('I','M','R') -#define MANUFACTURER_IMS MANFCODE('I','M','S') -#define MANUFACTURER_IMT MANFCODE('I','M','T') -#define MANUFACTURER_IMZ MANFCODE('I','M','Z') -#define MANUFACTURER_INC MANFCODE('I','N','C') -#define MANUFACTURER_IND MANFCODE('I','N','D') -#define MANUFACTURER_INE MANFCODE('I','N','E') -#define MANUFACTURER_INF MANFCODE('I','N','F') -#define MANUFACTURER_INI MANFCODE('I','N','I') -#define MANUFACTURER_INM MANFCODE('I','N','M') -#define MANUFACTURER_INN MANFCODE('I','N','N') -#define MANUFACTURER_INO MANFCODE('I','N','O') -#define MANUFACTURER_INP MANFCODE('I','N','P') -#define MANUFACTURER_INS MANFCODE('I','N','S') -#define MANUFACTURER_INT MANFCODE('I','N','T') -#define MANUFACTURER_INV MANFCODE('I','N','V') -#define MANUFACTURER_INX MANFCODE('I','N','X') -#define MANUFACTURER_IOM MANFCODE('I','O','M') -#define MANUFACTURER_ION MANFCODE('I','O','N') -#define MANUFACTURER_IOT MANFCODE('I','O','T') -#define MANUFACTURER_IPD MANFCODE('I','P','D') -#define MANUFACTURER_IQO MANFCODE('I','Q','O') -#define MANUFACTURER_IQS MANFCODE('I','Q','S') -#define MANUFACTURER_IRE MANFCODE('I','R','E') -#define MANUFACTURER_ISA MANFCODE('I','S','A') -#define MANUFACTURER_ISE MANFCODE('I','S','E') -#define MANUFACTURER_ISF MANFCODE('I','S','F') -#define MANUFACTURER_ISI MANFCODE('I','S','I') -#define MANUFACTURER_ISK MANFCODE('I','S','K') -#define MANUFACTURER_ISO MANFCODE('I','S','O') -#define MANUFACTURER_ISS MANFCODE('I','S','S') -#define MANUFACTURER_IST MANFCODE('I','S','T') -#define MANUFACTURER_ISW MANFCODE('I','S','W') -#define MANUFACTURER_ITA MANFCODE('I','T','A') -#define MANUFACTURER_ITB MANFCODE('I','T','B') -#define MANUFACTURER_ITC MANFCODE('I','T','C') -#define MANUFACTURER_ITE MANFCODE('I','T','E') -#define MANUFACTURER_ITF MANFCODE('I','T','F') -#define MANUFACTURER_ITG MANFCODE('I','T','G') -#define MANUFACTURER_ITH MANFCODE('I','T','H') -#define MANUFACTURER_ITI MANFCODE('I','T','I') -#define MANUFACTURER_ITK MANFCODE('I','T','K') -#define MANUFACTURER_ITL MANFCODE('I','T','L') -#define MANUFACTURER_ITM MANFCODE('I','T','M') -#define MANUFACTURER_ITP MANFCODE('I','T','P') -#define MANUFACTURER_ITR MANFCODE('I','T','R') -#define MANUFACTURER_ITS MANFCODE('I','T','S') -#define MANUFACTURER_ITU MANFCODE('I','T','U') -#define MANUFACTURER_ITW MANFCODE('I','T','W') -#define MANUFACTURER_ITX MANFCODE('I','T','X') -#define MANUFACTURER_ITZ MANFCODE('I','T','Z') -#define MANUFACTURER_IUS MANFCODE('I','U','S') -#define MANUFACTURER_IVY MANFCODE('I','V','Y') -#define MANUFACTURER_IWD MANFCODE('I','W','D') -#define MANUFACTURER_IWK MANFCODE('I','W','K') -#define MANUFACTURER_IYI MANFCODE('I','Y','I') -#define MANUFACTURER_IZE MANFCODE('I','Z','E') -#define MANUFACTURER_JAC MANFCODE('J','A','C') -#define MANUFACTURER_JAN MANFCODE('J','A','N') -#define MANUFACTURER_JBF MANFCODE('J','B','F') -#define MANUFACTURER_JCE MANFCODE('J','C','E') -#define MANUFACTURER_JED MANFCODE('J','E','D') -#define MANUFACTURER_JGD MANFCODE('J','G','D') -#define MANUFACTURER_JGF MANFCODE('J','G','F') -#define MANUFACTURER_JGH MANFCODE('J','G','H') -#define MANUFACTURER_JHA MANFCODE('J','H','A') -#define MANUFACTURER_JHM MANFCODE('J','H','M') -#define MANUFACTURER_JJN MANFCODE('J','J','N') -#define MANUFACTURER_JKW MANFCODE('J','K','W') -#define MANUFACTURER_JMA MANFCODE('J','M','A') -#define MANUFACTURER_JMT MANFCODE('J','M','T') -#define MANUFACTURER_JNC MANFCODE('J','N','C') -#define MANUFACTURER_JND MANFCODE('J','N','D') -#define MANUFACTURER_JNJ MANFCODE('J','N','J') -#define MANUFACTURER_JOY MANFCODE('J','O','Y') -#define MANUFACTURER_JPM MANFCODE('J','P','M') -#define MANUFACTURER_JSM MANFCODE('J','S','M') -#define MANUFACTURER_JSO MANFCODE('J','S','O') -#define MANUFACTURER_JST MANFCODE('J','S','T') -#define MANUFACTURER_JUM MANFCODE('J','U','M') -#define MANUFACTURER_JWH MANFCODE('J','W','H') -#define MANUFACTURER_JWR MANFCODE('J','W','R') -#define MANUFACTURER_JWS MANFCODE('J','W','S') -#define MANUFACTURER_JYS MANFCODE('J','Y','S') -#define MANUFACTURER_KAA MANFCODE('K','A','A') -#define MANUFACTURER_KAC MANFCODE('K','A','C') -#define MANUFACTURER_KAL MANFCODE('K','A','L') -#define MANUFACTURER_KAM MANFCODE('K','A','M') -#define MANUFACTURER_KAR MANFCODE('K','A','R') -#define MANUFACTURER_KAS MANFCODE('K','A','S') -#define MANUFACTURER_KAT MANFCODE('K','A','T') -#define MANUFACTURER_KAW MANFCODE('K','A','W') -#define MANUFACTURER_KBH MANFCODE('K','B','H') -#define MANUFACTURER_KBK MANFCODE('K','B','K') -#define MANUFACTURER_KBL MANFCODE('K','B','L') -#define MANUFACTURER_KBN MANFCODE('K','B','N') -#define MANUFACTURER_KDN MANFCODE('K','D','N') -#define MANUFACTURER_KDS MANFCODE('K','D','S') -#define MANUFACTURER_KEE MANFCODE('K','E','E') -#define MANUFACTURER_KEL MANFCODE('K','E','L') -#define MANUFACTURER_KER MANFCODE('K','E','R') -#define MANUFACTURER_KES MANFCODE('K','E','S') -#define MANUFACTURER_KEY MANFCODE('K','E','Y') -#define MANUFACTURER_KFM MANFCODE('K','F','M') -#define MANUFACTURER_KFM MANFCODE('K','F','M') -#define MANUFACTURER_KGE MANFCODE('K','G','E') -#define MANUFACTURER_KHL MANFCODE('K','H','L') -#define MANUFACTURER_KIG MANFCODE('K','I','G') -#define MANUFACTURER_KIT MANFCODE('K','I','T') -#define MANUFACTURER_KJN MANFCODE('K','J','N') -#define MANUFACTURER_KKE MANFCODE('K','K','E') -#define MANUFACTURER_KKG MANFCODE('K','K','G') -#define MANUFACTURER_KLE MANFCODE('K','L','E') -#define MANUFACTURER_KLK MANFCODE('K','L','K') -#define MANUFACTURER_KMB MANFCODE('K','M','B') -#define MANUFACTURER_KMT MANFCODE('K','M','T') -#define MANUFACTURER_KNI MANFCODE('K','N','I') -#define MANUFACTURER_KNR MANFCODE('K','N','R') -#define MANUFACTURER_KNX MANFCODE('K','N','X') -#define MANUFACTURER_KPD MANFCODE('K','P','D') -#define MANUFACTURER_KPL MANFCODE('K','P','L') -#define MANUFACTURER_KRO MANFCODE('K','R','O') -#define MANUFACTURER_KRT MANFCODE('K','R','T') -#define MANUFACTURER_KSM MANFCODE('K','S','M') -#define MANUFACTURER_KST MANFCODE('K','S','T') -#define MANUFACTURER_KSY MANFCODE('K','S','Y') -#define MANUFACTURER_KTC MANFCODE('K','T','C') -#define MANUFACTURER_KTL MANFCODE('K','T','L') -#define MANUFACTURER_KUK MANFCODE('K','U','K') -#define MANUFACTURER_KVS MANFCODE('K','V','S') -#define MANUFACTURER_KWH MANFCODE('K','W','H') -#define MANUFACTURER_KZK MANFCODE('K','Z','K') -#define MANUFACTURER_LAC MANFCODE('L','A','C') -#define MANUFACTURER_LAI MANFCODE('L','A','I') -#define MANUFACTURER_LAN MANFCODE('L','A','N') -#define MANUFACTURER_LAS MANFCODE('L','A','S') -#define MANUFACTURER_LCG MANFCODE('L','C','G') -#define MANUFACTURER_LCR MANFCODE('L','C','R') -#define MANUFACTURER_LDE MANFCODE('L','D','E') -#define MANUFACTURER_LEC MANFCODE('L','E','C') -#define MANUFACTURER_LEI MANFCODE('L','E','I') -#define MANUFACTURER_LEM MANFCODE('L','E','M') -#define MANUFACTURER_LES MANFCODE('L','E','S') -#define MANUFACTURER_LET MANFCODE('L','E','T') -#define MANUFACTURER_LFS MANFCODE('L','F','S') -#define MANUFACTURER_LFX MANFCODE('L','F','X') -#define MANUFACTURER_LGA MANFCODE('L','G','A') -#define MANUFACTURER_LGB MANFCODE('L','G','B') -#define MANUFACTURER_LGC MANFCODE('L','G','C') -#define MANUFACTURER_LGD MANFCODE('L','G','D') -#define MANUFACTURER_LGE MANFCODE('L','G','E') -#define MANUFACTURER_LGF MANFCODE('L','G','F') -#define MANUFACTURER_LGG MANFCODE('L','G','G') -#define MANUFACTURER_LGI MANFCODE('L','G','I') -#define MANUFACTURER_LGS MANFCODE('L','G','S') -#define MANUFACTURER_LGU MANFCODE('L','G','U') -#define MANUFACTURER_LGZ MANFCODE('L','G','Z') -#define MANUFACTURER_LHA MANFCODE('L','H','A') -#define MANUFACTURER_LIN MANFCODE('L','I','N') -#define MANUFACTURER_LIT MANFCODE('L','I','T') -#define MANUFACTURER_LJP MANFCODE('L','J','P') -#define MANUFACTURER_LLM MANFCODE('L','L','M') -#define MANUFACTURER_LMC MANFCODE('L','M','C') -#define MANUFACTURER_LMG MANFCODE('L','M','G') -#define MANUFACTURER_LML MANFCODE('L','M','L') -#define MANUFACTURER_LMP MANFCODE('L','M','P') -#define MANUFACTURER_LMS MANFCODE('L','M','S') -#define MANUFACTURER_LNC MANFCODE('L','N','C') -#define MANUFACTURER_LNK MANFCODE('L','N','K') -#define MANUFACTURER_LNT MANFCODE('L','N','T') -#define MANUFACTURER_LOB MANFCODE('L','O','B') -#define MANUFACTURER_LOG MANFCODE('L','O','G') -#define MANUFACTURER_LOV MANFCODE('L','O','V') -#define MANUFACTURER_LSC MANFCODE('L','S','C') -#define MANUFACTURER_LSE MANFCODE('L','S','E') -#define MANUFACTURER_LSK MANFCODE('L','S','K') -#define MANUFACTURER_LSP MANFCODE('L','S','P') -#define MANUFACTURER_LSZ MANFCODE('L','S','Z') -#define MANUFACTURER_LTO MANFCODE('L','T','O') -#define MANUFACTURER_LUG MANFCODE('L','U','G') -#define MANUFACTURER_LUN MANFCODE('L','U','N') -#define MANUFACTURER_LUP MANFCODE('L','U','P') -#define MANUFACTURER_LWT MANFCODE('L','W','T') -#define MANUFACTURER_LYE MANFCODE('L','Y','E') -#define MANUFACTURER_LZG MANFCODE('L','Z','G') -#define MANUFACTURER_MAC MANFCODE('M','A','C') -#define MANUFACTURER_MAD MANFCODE('M','A','D') -#define MANUFACTURER_MAE MANFCODE('M','A','E') -#define MANUFACTURER_MAG MANFCODE('M','A','G') -#define MANUFACTURER_MAH MANFCODE('M','A','H') -#define MANUFACTURER_MAN MANFCODE('M','A','N') -#define MANUFACTURER_MAS MANFCODE('M','A','S') -#define MANUFACTURER_MAT MANFCODE('M','A','T') -#define MANUFACTURER_MAX MANFCODE('M','A','X') -#define MANUFACTURER_MBH MANFCODE('M','B','H') -#define MANUFACTURER_MBS MANFCODE('M','B','S') -#define MANUFACTURER_MCI MANFCODE('M','C','I') -#define MANUFACTURER_MCP MANFCODE('M','C','P') -#define MANUFACTURER_MCR MANFCODE('M','C','R') -#define MANUFACTURER_MCS MANFCODE('M','C','S') -#define MANUFACTURER_MCV MANFCODE('M','C','V') -#define MANUFACTURER_MDA MANFCODE('M','D','A') -#define MANUFACTURER_MDE MANFCODE('M','D','E') -#define MANUFACTURER_MDL MANFCODE('M','D','L') -#define MANUFACTURER_MDT MANFCODE('M','D','T') -#define MANUFACTURER_MDX MANFCODE('M','D','X') -#define MANUFACTURER_MEC MANFCODE('M','E','C') -#define MANUFACTURER_MED MANFCODE('M','E','D') -#define MANUFACTURER_MEE MANFCODE('M','E','E') -#define MANUFACTURER_MEH MANFCODE('M','E','H') -#define MANUFACTURER_MEI MANFCODE('M','E','I') -#define MANUFACTURER_MEL MANFCODE('M','E','L') -#define MANUFACTURER_MEM MANFCODE('M','E','M') -#define MANUFACTURER_MES MANFCODE('M','E','S') -#define MANUFACTURER_MET MANFCODE('M','E','T') -#define MANUFACTURER_MEX MANFCODE('M','E','X') -#define MANUFACTURER_MHT MANFCODE('M','H','T') -#define MANUFACTURER_MIC MANFCODE('M','I','C') -#define MANUFACTURER_MID MANFCODE('M','I','D') -#define MANUFACTURER_MII MANFCODE('M','I','I') -#define MANUFACTURER_MIJ MANFCODE('M','I','J') -#define MANUFACTURER_MIK MANFCODE('M','I','K') -#define MANUFACTURER_MIL MANFCODE('M','I','L') -#define MANUFACTURER_MIM MANFCODE('M','I','M') -#define MANUFACTURER_MIO MANFCODE('M','I','O') -#define MANUFACTURER_MIR MANFCODE('M','I','R') -#define MANUFACTURER_MIS MANFCODE('M','I','S') -#define MANUFACTURER_MIT MANFCODE('M','I','T') -#define MANUFACTURER_MIV MANFCODE('M','I','V') -#define MANUFACTURER_MKA MANFCODE('M','K','A') -#define MANUFACTURER_MKE MANFCODE('M','K','E') -#define MANUFACTURER_MKL MANFCODE('M','K','L') -#define MANUFACTURER_MKS MANFCODE('M','K','S') -#define MANUFACTURER_MLQ MANFCODE('M','L','Q') -#define MANUFACTURER_MLR MANFCODE('M','L','R') -#define MANUFACTURER_MMC MANFCODE('M','M','C') -#define MANUFACTURER_MMF MANFCODE('M','M','F') -#define MANUFACTURER_MMI MANFCODE('M','M','I') -#define MANUFACTURER_MMM MANFCODE('M','M','M') -#define MANUFACTURER_MMP MANFCODE('M','M','P') -#define MANUFACTURER_MMS MANFCODE('M','M','S') -#define MANUFACTURER_MMT MANFCODE('M','M','T') -#define MANUFACTURER_MNS MANFCODE('M','N','S') -#define MANUFACTURER_MNW MANFCODE('M','N','W') -#define MANUFACTURER_MOJ MANFCODE('M','O','J') -#define MANUFACTURER_MOS MANFCODE('M','O','S') -#define MANUFACTURER_MOT MANFCODE('M','O','T') -#define MANUFACTURER_MPA MANFCODE('M','P','A') -#define MANUFACTURER_MPL MANFCODE('M','P','L') -#define MANUFACTURER_MPR MANFCODE('M','P','R') -#define MANUFACTURER_MPS MANFCODE('M','P','S') -#define MANUFACTURER_MRN MANFCODE('M','R','N') -#define MANUFACTURER_MRT MANFCODE('M','R','T') -#define MANUFACTURER_MSB MANFCODE('M','S','B') -#define MANUFACTURER_MSE MANFCODE('M','S','E') -#define MANUFACTURER_MSI MANFCODE('M','S','I') -#define MANUFACTURER_MSM MANFCODE('M','S','M') -#define MANUFACTURER_MSO MANFCODE('M','S','O') -#define MANUFACTURER_MST MANFCODE('M','S','T') -#define MANUFACTURER_MSY MANFCODE('M','S','Y') -#define MANUFACTURER_MTC MANFCODE('M','T','C') -#define MANUFACTURER_MTD MANFCODE('M','T','D') -#define MANUFACTURER_MTE MANFCODE('M','T','E') -#define MANUFACTURER_MTH MANFCODE('M','T','H') -#define MANUFACTURER_MTI MANFCODE('M','T','I') -#define MANUFACTURER_MTL MANFCODE('M','T','L') -#define MANUFACTURER_MTM MANFCODE('M','T','M') -#define MANUFACTURER_MTN MANFCODE('M','T','N') -#define MANUFACTURER_MTP MANFCODE('M','T','P') -#define MANUFACTURER_MTR MANFCODE('M','T','R') -#define MANUFACTURER_MTS MANFCODE('M','T','S') -#define MANUFACTURER_MTT MANFCODE('M','T','T') -#define MANUFACTURER_MTX MANFCODE('M','T','X') -#define MANUFACTURER_MUK MANFCODE('M','U','K') -#define MANUFACTURER_MWI MANFCODE('M','W','I') -#define MANUFACTURER_MWU MANFCODE('M','W','U') -#define MANUFACTURER_MXE MANFCODE('M','X','E') -#define MANUFACTURER_MXM MANFCODE('M','X','M') -#define MANUFACTURER_MYS MANFCODE('M','Y','S') -#define MANUFACTURER_NAE MANFCODE('N','A','E') -#define MANUFACTURER_NAG MANFCODE('N','A','G') -#define MANUFACTURER_NAR MANFCODE('N','A','R') -#define MANUFACTURER_NAS MANFCODE('N','A','S') -#define MANUFACTURER_NAS MANFCODE('N','A','S') -#define MANUFACTURER_NAT MANFCODE('N','A','T') -#define MANUFACTURER_NCK MANFCODE('N','C','K') -#define MANUFACTURER_NDF MANFCODE('N','D','F') -#define MANUFACTURER_NDM MANFCODE('N','D','M') -#define MANUFACTURER_NEE MANFCODE('N','E','E') -#define MANUFACTURER_NEI MANFCODE('N','E','I') -#define MANUFACTURER_NEO MANFCODE('N','E','O') -#define MANUFACTURER_NER MANFCODE('N','E','R') -#define MANUFACTURER_NES MANFCODE('N','E','S') -#define MANUFACTURER_NET MANFCODE('N','E','T') -#define MANUFACTURER_NEX MANFCODE('N','E','X') -#define MANUFACTURER_NGP MANFCODE('N','G','P') -#define MANUFACTURER_NIK MANFCODE('N','I','K') -#define MANUFACTURER_NIS MANFCODE('N','I','S') -#define MANUFACTURER_NJC MANFCODE('N','J','C') -#define MANUFACTURER_NKS MANFCODE('N','K','S') -#define MANUFACTURER_NLI MANFCODE('N','L','I') -#define MANUFACTURER_NME MANFCODE('N','M','E') -#define MANUFACTURER_NMG MANFCODE('N','M','G') -#define MANUFACTURER_NMS MANFCODE('N','M','S') -#define MANUFACTURER_NMT MANFCODE('N','M','T') -#define MANUFACTURER_NNT MANFCODE('N','N','T') -#define MANUFACTURER_NOI MANFCODE('N','O','I') -#define MANUFACTURER_NOQ MANFCODE('N','O','Q') -#define MANUFACTURER_NOV MANFCODE('N','O','V') -#define MANUFACTURER_NPL MANFCODE('N','P','L') -#define MANUFACTURER_NPS MANFCODE('N','P','S') -#define MANUFACTURER_NPT MANFCODE('N','P','T') -#define MANUFACTURER_NRM MANFCODE('N','R','M') -#define MANUFACTURER_NRN MANFCODE('N','R','N') -#define MANUFACTURER_NRT MANFCODE('N','R','T') -#define MANUFACTURER_NSE MANFCODE('N','S','E') -#define MANUFACTURER_NSP MANFCODE('N','S','P') -#define MANUFACTURER_NTC MANFCODE('N','T','C') -#define MANUFACTURER_NTM MANFCODE('N','T','M') -#define MANUFACTURER_NVD MANFCODE('N','V','D') -#define MANUFACTURER_NVN MANFCODE('N','V','N') -#define MANUFACTURER_NWG MANFCODE('N','W','G') -#define MANUFACTURER_NWM MANFCODE('N','W','M') -#define MANUFACTURER_NXG MANFCODE('N','X','G') -#define MANUFACTURER_NXP MANFCODE('N','X','P') -#define MANUFACTURER_NYG MANFCODE('N','Y','G') -#define MANUFACTURER_NYN MANFCODE('N','Y','N') -#define MANUFACTURER_NZR MANFCODE('N','Z','R') -#define MANUFACTURER_OAS MANFCODE('O','A','S') -#define MANUFACTURER_OBC MANFCODE('O','B','C') -#define MANUFACTURER_OBE MANFCODE('O','B','E') -#define MANUFACTURER_OBR MANFCODE('O','B','R') -#define MANUFACTURER_ODI MANFCODE('O','D','I') -#define MANUFACTURER_ODK MANFCODE('O','D','K') -#define MANUFACTURER_OEE MANFCODE('O','E','E') -#define MANUFACTURER_OJA MANFCODE('O','J','A') -#define MANUFACTURER_OKN MANFCODE('O','K','N') -#define MANUFACTURER_OLI MANFCODE('O','L','I') -#define MANUFACTURER_OMG MANFCODE('O','M','G') -#define MANUFACTURER_OMS MANFCODE('O','M','S') -#define MANUFACTURER_ONE MANFCODE('O','N','E') -#define MANUFACTURER_ONR MANFCODE('O','N','R') -#define MANUFACTURER_ONS MANFCODE('O','N','S') -#define MANUFACTURER_OPT MANFCODE('O','P','T') -#define MANUFACTURER_ORB MANFCODE('O','R','B') -#define MANUFACTURER_ORM MANFCODE('O','R','M') -#define MANUFACTURER_OSA MANFCODE('O','S','A') -#define MANUFACTURER_OSB MANFCODE('O','S','B') -#define MANUFACTURER_OSC MANFCODE('O','S','C') -#define MANUFACTURER_OSK MANFCODE('O','S','K') -#define MANUFACTURER_OSV MANFCODE('O','S','V') -#define MANUFACTURER_OTM MANFCODE('O','T','M') -#define MANUFACTURER_OTS MANFCODE('O','T','S') -#define MANUFACTURER_OYK MANFCODE('O','Y','K') -#define MANUFACTURER_OZK MANFCODE('O','Z','K') -#define MANUFACTURER_PAD MANFCODE('P','A','D') -#define MANUFACTURER_PAF MANFCODE('P','A','F') -#define MANUFACTURER_PAI MANFCODE('P','A','I') -#define MANUFACTURER_PAK MANFCODE('P','A','K') -#define MANUFACTURER_PAN MANFCODE('P','A','N') -#define MANUFACTURER_PBR MANFCODE('P','B','R') -#define MANUFACTURER_PCE MANFCODE('P','C','E') -#define MANUFACTURER_PCI MANFCODE('P','C','I') -#define MANUFACTURER_PCR MANFCODE('P','C','R') -#define MANUFACTURER_PDE MANFCODE('P','D','E') -#define MANUFACTURER_PDX MANFCODE('P','D','X') -#define MANUFACTURER_PEA MANFCODE('P','E','A') -#define MANUFACTURER_PEE MANFCODE('P','E','E') -#define MANUFACTURER_PEL MANFCODE('P','E','L') -#define MANUFACTURER_PEM MANFCODE('P','E','M') -#define MANUFACTURER_PEP MANFCODE('P','E','P') -#define MANUFACTURER_PER MANFCODE('P','E','R') -#define MANUFACTURER_PFI MANFCODE('P','F','I') -#define MANUFACTURER_PGE MANFCODE('P','G','E') -#define MANUFACTURER_PGP MANFCODE('P','G','P') -#define MANUFACTURER_PHL MANFCODE('P','H','L') -#define MANUFACTURER_PHX MANFCODE('P','H','X') -#define MANUFACTURER_PII MANFCODE('P','I','I') -#define MANUFACTURER_PIK MANFCODE('P','I','K') -#define MANUFACTURER_PIL MANFCODE('P','I','L') -#define MANUFACTURER_PIM MANFCODE('P','I','M') -#define MANUFACTURER_PIN MANFCODE('P','I','N') -#define MANUFACTURER_PIP MANFCODE('P','I','P') -#define MANUFACTURER_PIV MANFCODE('P','I','V') -#define MANUFACTURER_PLL MANFCODE('P','L','L') -#define MANUFACTURER_PLN MANFCODE('P','L','N') -#define MANUFACTURER_PLO MANFCODE('P','L','O') -#define MANUFACTURER_PLU MANFCODE('P','L','U') -#define MANUFACTURER_PME MANFCODE('P','M','E') -#define MANUFACTURER_PMG MANFCODE('P','M','G') -#define MANUFACTURER_PML MANFCODE('P','M','L') -#define MANUFACTURER_PMP MANFCODE('P','M','P') -#define MANUFACTURER_PMS MANFCODE('P','M','S') -#define MANUFACTURER_PMX MANFCODE('P','M','X') -#define MANUFACTURER_PNC MANFCODE('P','N','C') -#define MANUFACTURER_PNG MANFCODE('P','N','G') -#define MANUFACTURER_POD MANFCODE('P','O','D') -#define MANUFACTURER_POL MANFCODE('P','O','L') -#define MANUFACTURER_POW MANFCODE('P','O','W') -#define MANUFACTURER_POZ MANFCODE('P','O','Z') -#define MANUFACTURER_PPC MANFCODE('P','P','C') -#define MANUFACTURER_PPL MANFCODE('P','P','L') -#define MANUFACTURER_PPS MANFCODE('P','P','S') -#define MANUFACTURER_PQP MANFCODE('P','Q','P') -#define MANUFACTURER_PRA MANFCODE('P','R','A') -#define MANUFACTURER_PRE MANFCODE('P','R','E') -#define MANUFACTURER_PRG MANFCODE('P','R','G') -#define MANUFACTURER_PRI MANFCODE('P','R','I') -#define MANUFACTURER_PRM MANFCODE('P','R','M') -#define MANUFACTURER_PRO MANFCODE('P','R','O') -#define MANUFACTURER_PRY MANFCODE('P','R','Y') -#define MANUFACTURER_PSC MANFCODE('P','S','C') -#define MANUFACTURER_PSE MANFCODE('P','S','E') -#define MANUFACTURER_PSN MANFCODE('P','S','N') -#define MANUFACTURER_PSS MANFCODE('P','S','S') -#define MANUFACTURER_PST MANFCODE('P','S','T') -#define MANUFACTURER_PTI MANFCODE('P','T','I') -#define MANUFACTURER_PTR MANFCODE('P','T','R') -#define MANUFACTURER_PUK MANFCODE('P','U','K') -#define MANUFACTURER_PUL MANFCODE('P','U','L') -#define MANUFACTURER_PVT MANFCODE('P','V','T') -#define MANUFACTURER_PWB MANFCODE('P','W','B') -#define MANUFACTURER_PWR MANFCODE('P','W','R') -#define MANUFACTURER_PXC MANFCODE('P','X','C') -#define MANUFACTURER_PYU MANFCODE('P','Y','U') -#define MANUFACTURER_PYX MANFCODE('P','Y','X') -#define MANUFACTURER_QDR MANFCODE('Q','D','R') -#define MANUFACTURER_QDS MANFCODE('Q','D','S') -#define MANUFACTURER_QFP MANFCODE('Q','F','P') -#define MANUFACTURER_QLE MANFCODE('Q','L','E') -#define MANUFACTURER_QSD MANFCODE('Q','S','D') -#define MANUFACTURER_QTS MANFCODE('Q','T','S') -#define MANUFACTURER_RAC MANFCODE('R','A','C') -#define MANUFACTURER_RAD MANFCODE('R','A','D') -#define MANUFACTURER_RAM MANFCODE('R','A','M') -#define MANUFACTURER_RAS MANFCODE('R','A','S') -#define MANUFACTURER_RAY MANFCODE('R','A','Y') -#define MANUFACTURER_RBM MANFCODE('R','B','M') -#define MANUFACTURER_RCE MANFCODE('R','C','E') -#define MANUFACTURER_RDK MANFCODE('R','D','K') -#define MANUFACTURER_RDM MANFCODE('R','D','M') -#define MANUFACTURER_REC MANFCODE('R','E','C') -#define MANUFACTURER_REF MANFCODE('R','E','F') -#define MANUFACTURER_REI MANFCODE('R','E','I') -#define MANUFACTURER_REL MANFCODE('R','E','L') -#define MANUFACTURER_REM MANFCODE('R','E','M') -#define MANUFACTURER_REP MANFCODE('R','E','P') -#define MANUFACTURER_RES MANFCODE('R','E','S') -#define MANUFACTURER_RHN MANFCODE('R','H','N') -#define MANUFACTURER_RIC MANFCODE('R','I','C') -#define MANUFACTURER_RIL MANFCODE('R','I','L') -#define MANUFACTURER_RIM MANFCODE('R','I','M') -#define MANUFACTURER_RIN MANFCODE('R','I','N') -#define MANUFACTURER_RIT MANFCODE('R','I','T') -#define MANUFACTURER_RIX MANFCODE('R','I','X') -#define MANUFACTURER_RIZ MANFCODE('R','I','Z') -#define MANUFACTURER_RKD MANFCODE('R','K','D') -#define MANUFACTURER_RKE MANFCODE('R','K','E') -#define MANUFACTURER_RMA MANFCODE('R','M','A') -#define MANUFACTURER_RMG MANFCODE('R','M','G') -#define MANUFACTURER_RMI MANFCODE('R','M','I') -#define MANUFACTURER_RML MANFCODE('R','M','L') -#define MANUFACTURER_RMR MANFCODE('R','M','R') -#define MANUFACTURER_RMT MANFCODE('R','M','T') -#define MANUFACTURER_RNW MANFCODE('R','N','W') -#define MANUFACTURER_ROK MANFCODE('R','O','K') -#define MANUFACTURER_RSA MANFCODE('R','S','A') -#define MANUFACTURER_RSM MANFCODE('R','S','M') -#define MANUFACTURER_RSW MANFCODE('R','S','W') -#define MANUFACTURER_SAA MANFCODE('S','A','A') -#define MANUFACTURER_SAC MANFCODE('S','A','C') -#define MANUFACTURER_SAE MANFCODE('S','A','E') -#define MANUFACTURER_SAG MANFCODE('S','A','G') -#define MANUFACTURER_SAL MANFCODE('S','A','L') -#define MANUFACTURER_SAM MANFCODE('S','A','M') -#define MANUFACTURER_SAN MANFCODE('S','A','N') -#define MANUFACTURER_SAP MANFCODE('S','A','P') -#define MANUFACTURER_SAT MANFCODE('S','A','T') -#define MANUFACTURER_SBC MANFCODE('S','B','C') -#define MANUFACTURER_SCA MANFCODE('S','C','A') -#define MANUFACTURER_SCE MANFCODE('S','C','E') -#define MANUFACTURER_SCH MANFCODE('S','C','H') -#define MANUFACTURER_SCL MANFCODE('S','C','L') -#define MANUFACTURER_SCM MANFCODE('S','C','M') -#define MANUFACTURER_SCR MANFCODE('S','C','R') -#define MANUFACTURER_SCT MANFCODE('S','C','T') -#define MANUFACTURER_SCW MANFCODE('S','C','W') -#define MANUFACTURER_SDC MANFCODE('S','D','C') -#define MANUFACTURER_SDM MANFCODE('S','D','M') -#define MANUFACTURER_SDS MANFCODE('S','D','S') -#define MANUFACTURER_SEC MANFCODE('S','E','C') -#define MANUFACTURER_SEE MANFCODE('S','E','E') -#define MANUFACTURER_SEH MANFCODE('S','E','H') -#define MANUFACTURER_SEI MANFCODE('S','E','I') -#define MANUFACTURER_SEK MANFCODE('S','E','K') -#define MANUFACTURER_SEL MANFCODE('S','E','L') -#define MANUFACTURER_SEM MANFCODE('S','E','M') -#define MANUFACTURER_SEN MANFCODE('S','E','N') -#define MANUFACTURER_SEO MANFCODE('S','E','O') -#define MANUFACTURER_SET MANFCODE('S','E','T') -#define MANUFACTURER_SFE MANFCODE('S','F','E') -#define MANUFACTURER_SFI MANFCODE('S','F','I') -#define MANUFACTURER_SFR MANFCODE('S','F','R') -#define MANUFACTURER_SFT MANFCODE('S','F','T') -#define MANUFACTURER_SGA MANFCODE('S','G','A') -#define MANUFACTURER_SGI MANFCODE('S','G','I') -#define MANUFACTURER_SGM MANFCODE('S','G','M') -#define MANUFACTURER_SGN MANFCODE('S','G','N') -#define MANUFACTURER_SGX MANFCODE('S','G','X') -#define MANUFACTURER_SHD MANFCODE('S','H','D') -#define MANUFACTURER_SHE MANFCODE('S','H','E') -#define MANUFACTURER_SHM MANFCODE('S','H','M') -#define MANUFACTURER_SHT MANFCODE('S','H','T') -#define MANUFACTURER_SIC MANFCODE('S','I','C') -#define MANUFACTURER_SIE MANFCODE('S','I','E') -#define MANUFACTURER_SIG MANFCODE('S','I','G') -#define MANUFACTURER_SIJ MANFCODE('S','I','J') -#define MANUFACTURER_SIL MANFCODE('S','I','L') -#define MANUFACTURER_SIM MANFCODE('S','I','M') -#define MANUFACTURER_SIN MANFCODE('S','I','N') -#define MANUFACTURER_SIS MANFCODE('S','I','S') -#define MANUFACTURER_SIT MANFCODE('S','I','T') -#define MANUFACTURER_SIV MANFCODE('S','I','V') -#define MANUFACTURER_SIX MANFCODE('S','I','X') -#define MANUFACTURER_SJA MANFCODE('S','J','A') -#define MANUFACTURER_SKI MANFCODE('S','K','I') -#define MANUFACTURER_SKK MANFCODE('S','K','K') -#define MANUFACTURER_SKN MANFCODE('S','K','N') -#define MANUFACTURER_SKQ MANFCODE('S','K','Q') -#define MANUFACTURER_SKT MANFCODE('S','K','T') -#define MANUFACTURER_SKY MANFCODE('S','K','Y') -#define MANUFACTURER_SLB MANFCODE('S','L','B') -#define MANUFACTURER_SLG MANFCODE('S','L','G') -#define MANUFACTURER_SLP MANFCODE('S','L','P') -#define MANUFACTURER_SLV MANFCODE('S','L','V') -#define MANUFACTURER_SLW MANFCODE('S','L','W') -#define MANUFACTURER_SLX MANFCODE('S','L','X') -#define MANUFACTURER_SMA MANFCODE('S','M','A') -#define MANUFACTURER_SMC MANFCODE('S','M','C') -#define MANUFACTURER_SME MANFCODE('S','M','E') -#define MANUFACTURER_SMF MANFCODE('S','M','F') -#define MANUFACTURER_SMG MANFCODE('S','M','G') -#define MANUFACTURER_SMI MANFCODE('S','M','I') -#define MANUFACTURER_SML MANFCODE('S','M','L') -#define MANUFACTURER_SMM MANFCODE('S','M','M') -#define MANUFACTURER_SMN MANFCODE('S','M','N') -#define MANUFACTURER_SMP MANFCODE('S','M','P') -#define MANUFACTURER_SMQ MANFCODE('S','M','Q') -#define MANUFACTURER_SMR MANFCODE('S','M','R') -#define MANUFACTURER_SMS MANFCODE('S','M','S') -#define MANUFACTURER_SMT MANFCODE('S','M','T') -#define MANUFACTURER_SMV MANFCODE('S','M','V') -#define MANUFACTURER_SMX MANFCODE('S','M','X') -#define MANUFACTURER_SNB MANFCODE('S','N','B') -#define MANUFACTURER_SNM MANFCODE('S','N','M') -#define MANUFACTURER_SNR MANFCODE('S','N','R') -#define MANUFACTURER_SNS MANFCODE('S','N','S') -#define MANUFACTURER_SNT MANFCODE('S','N','T') -#define MANUFACTURER_SOC MANFCODE('S','O','C') -#define MANUFACTURER_SOF MANFCODE('S','O','F') -#define MANUFACTURER_SOG MANFCODE('S','O','G') -#define MANUFACTURER_SOL MANFCODE('S','O','L') -#define MANUFACTURER_SOM MANFCODE('S','O','M') -#define MANUFACTURER_SON MANFCODE('S','O','N') -#define MANUFACTURER_SOS MANFCODE('S','O','S') -#define MANUFACTURER_SOT MANFCODE('S','O','T') -#define MANUFACTURER_SPC MANFCODE('S','P','C') -#define MANUFACTURER_SPE MANFCODE('S','P','E') -#define MANUFACTURER_SPL MANFCODE('S','P','L') -#define MANUFACTURER_SPM MANFCODE('S','P','M') -#define MANUFACTURER_SPX MANFCODE('S','P','X') -#define MANUFACTURER_SPZ MANFCODE('S','P','Z') -#define MANUFACTURER_SRE MANFCODE('S','R','E') -#define MANUFACTURER_SRF MANFCODE('S','R','F') -#define MANUFACTURER_SRN MANFCODE('S','R','N') -#define MANUFACTURER_SRV MANFCODE('S','R','V') -#define MANUFACTURER_SSE MANFCODE('S','S','E') -#define MANUFACTURER_SSG MANFCODE('S','S','G') -#define MANUFACTURER_SSI MANFCODE('S','S','I') -#define MANUFACTURER_SSL MANFCODE('S','S','L') -#define MANUFACTURER_SSM MANFCODE('S','S','M') -#define MANUFACTURER_SSN MANFCODE('S','S','N') -#define MANUFACTURER_SSS MANFCODE('S','S','S') -#define MANUFACTURER_SST MANFCODE('S','S','T') -#define MANUFACTURER_STA MANFCODE('S','T','A') -#define MANUFACTURER_STC MANFCODE('S','T','C') -#define MANUFACTURER_STD MANFCODE('S','T','D') -#define MANUFACTURER_STE MANFCODE('S','T','E') -#define MANUFACTURER_STF MANFCODE('S','T','F') -#define MANUFACTURER_STL MANFCODE('S','T','L') -#define MANUFACTURER_STM MANFCODE('S','T','M') -#define MANUFACTURER_STN MANFCODE('S','T','N') -#define MANUFACTURER_STO MANFCODE('S','T','O') -#define MANUFACTURER_STP MANFCODE('S','T','P') -#define MANUFACTURER_STR MANFCODE('S','T','R') -#define MANUFACTURER_STS MANFCODE('S','T','S') -#define MANUFACTURER_STV MANFCODE('S','T','V') -#define MANUFACTURER_STW MANFCODE('S','T','W') -#define MANUFACTURER_STZ MANFCODE('S','T','Z') -#define MANUFACTURER_SUL MANFCODE('S','U','L') -#define MANUFACTURER_SUM MANFCODE('S','U','M') -#define MANUFACTURER_SUN MANFCODE('S','U','N') -#define MANUFACTURER_SUP MANFCODE('S','U','P') -#define MANUFACTURER_SUX MANFCODE('S','U','X') -#define MANUFACTURER_SVM MANFCODE('S','V','M') -#define MANUFACTURER_SVT MANFCODE('S','V','T') -#define MANUFACTURER_SWI MANFCODE('S','W','I') -#define MANUFACTURER_SWM MANFCODE('S','W','M') -#define MANUFACTURER_SWS MANFCODE('S','W','S') -#define MANUFACTURER_SWT MANFCODE('S','W','T') -#define MANUFACTURER_SYC MANFCODE('S','Y','C') -#define MANUFACTURER_SYD MANFCODE('S','Y','D') -#define MANUFACTURER_SYN MANFCODE('S','Y','N') -#define MANUFACTURER_SYR MANFCODE('S','Y','R') -#define MANUFACTURER_SYS MANFCODE('S','Y','S') -#define MANUFACTURER_SYX MANFCODE('S','Y','X') -#define MANUFACTURER_SZS MANFCODE('S','Z','S') -#define MANUFACTURER_TAG MANFCODE('T','A','G') -#define MANUFACTURER_TAL MANFCODE('T','A','L') -#define MANUFACTURER_TAN MANFCODE('T','A','N') -#define MANUFACTURER_TAS MANFCODE('T','A','S') -#define MANUFACTURER_TAT MANFCODE('T','A','T') -#define MANUFACTURER_TAU MANFCODE('T','A','U') -#define MANUFACTURER_TAY MANFCODE('T','A','Y') -#define MANUFACTURER_TAZ MANFCODE('T','A','Z') -#define MANUFACTURER_TBN MANFCODE('T','B','N') -#define MANUFACTURER_TBS MANFCODE('T','B','S') -#define MANUFACTURER_TCE MANFCODE('T','C','E') -#define MANUFACTURER_TCH MANFCODE('T','C','H') -#define MANUFACTURER_TCM MANFCODE('T','C','M') -#define MANUFACTURER_TCO MANFCODE('T','C','O') -#define MANUFACTURER_TCP MANFCODE('T','C','P') -#define MANUFACTURER_TCT MANFCODE('T','C','T') -#define MANUFACTURER_TCX MANFCODE('T','C','X') -#define MANUFACTURER_TCZ MANFCODE('T','C','Z') -#define MANUFACTURER_TDC MANFCODE('T','D','C') -#define MANUFACTURER_TEA MANFCODE('T','E','A') -#define MANUFACTURER_TEC MANFCODE('T','E','C') -#define MANUFACTURER_TEH MANFCODE('T','E','H') -#define MANUFACTURER_TEI MANFCODE('T','E','I') -#define MANUFACTURER_TEK MANFCODE('T','E','K') -#define MANUFACTURER_TEL MANFCODE('T','E','L') -#define MANUFACTURER_TEN MANFCODE('T','E','N') -#define MANUFACTURER_TEO MANFCODE('T','E','O') -#define MANUFACTURER_TEP MANFCODE('T','E','P') -#define MANUFACTURER_TER MANFCODE('T','E','R') -#define MANUFACTURER_TEU MANFCODE('T','E','U') -#define MANUFACTURER_TFC MANFCODE('T','F','C') -#define MANUFACTURER_TFG MANFCODE('T','F','G') -#define MANUFACTURER_TGX MANFCODE('T','G','X') -#define MANUFACTURER_THE MANFCODE('T','H','E') -#define MANUFACTURER_THS MANFCODE('T','H','S') -#define MANUFACTURER_THW MANFCODE('T','H','W') -#define MANUFACTURER_TIC MANFCODE('T','I','C') -#define MANUFACTURER_TID MANFCODE('T','I','D') -#define MANUFACTURER_TIG MANFCODE('T','I','G') -#define MANUFACTURER_TII MANFCODE('T','I','I') -#define MANUFACTURER_TIL MANFCODE('T','I','L') -#define MANUFACTURER_TIP MANFCODE('T','I','P') -#define MANUFACTURER_TIS MANFCODE('T','I','S') -#define MANUFACTURER_TIX MANFCODE('T','I','X') -#define MANUFACTURER_TKN MANFCODE('T','K','N') -#define MANUFACTURER_TKS MANFCODE('T','K','S') -#define MANUFACTURER_TLC MANFCODE('T','L','C') -#define MANUFACTURER_TLM MANFCODE('T','L','M') -#define MANUFACTURER_TLR MANFCODE('T','L','R') -#define MANUFACTURER_TLS MANFCODE('T','L','S') -#define MANUFACTURER_TLT MANFCODE('T','L','T') -#define MANUFACTURER_TLY MANFCODE('T','L','Y') -#define MANUFACTURER_TME MANFCODE('T','M','E') -#define MANUFACTURER_TMK MANFCODE('T','M','K') -#define MANUFACTURER_TMS MANFCODE('T','M','S') -#define MANUFACTURER_TMZ MANFCODE('T','M','Z') -#define MANUFACTURER_TNT MANFCODE('T','N','T') -#define MANUFACTURER_TOE MANFCODE('T','O','E') -#define MANUFACTURER_TOP MANFCODE('T','O','P') -#define MANUFACTURER_TPB MANFCODE('T','P','B') -#define MANUFACTURER_TPC MANFCODE('T','P','C') -#define MANUFACTURER_TPG MANFCODE('T','P','G') -#define MANUFACTURER_TPI MANFCODE('T','P','I') -#define MANUFACTURER_TPJ MANFCODE('T','P','J') -#define MANUFACTURER_TPL MANFCODE('T','P','L') -#define MANUFACTURER_TPZ MANFCODE('T','P','Z') -#define MANUFACTURER_TRC MANFCODE('T','R','C') -#define MANUFACTURER_TRG MANFCODE('T','R','G') -#define MANUFACTURER_TRI MANFCODE('T','R','I') -#define MANUFACTURER_TRJ MANFCODE('T','R','J') -#define MANUFACTURER_TRL MANFCODE('T','R','L') -#define MANUFACTURER_TRN MANFCODE('T','R','N') -#define MANUFACTURER_TRO MANFCODE('T','R','O') -#define MANUFACTURER_TRU MANFCODE('T','R','U') -#define MANUFACTURER_TRV MANFCODE('T','R','V') -#define MANUFACTURER_TRW MANFCODE('T','R','W') -#define MANUFACTURER_TRX MANFCODE('T','R','X') -#define MANUFACTURER_TRZ MANFCODE('T','R','Z') -#define MANUFACTURER_TSD MANFCODE('T','S','D') -#define MANUFACTURER_TSE MANFCODE('T','S','E') -#define MANUFACTURER_TSG MANFCODE('T','S','G') -#define MANUFACTURER_TSW MANFCODE('T','S','W') -#define MANUFACTURER_TTG MANFCODE('T','T','G') -#define MANUFACTURER_TTM MANFCODE('T','T','M') -#define MANUFACTURER_TTN MANFCODE('T','T','N') -#define MANUFACTURER_TTR MANFCODE('T','T','R') -#define MANUFACTURER_TTT MANFCODE('T','T','T') -#define MANUFACTURER_TUR MANFCODE('T','U','R') -#define MANUFACTURER_TWO MANFCODE('T','W','O') -#define MANUFACTURER_TXL MANFCODE('T','X','L') -#define MANUFACTURER_UAG MANFCODE('U','A','G') -#define MANUFACTURER_UBI MANFCODE('U','B','I') -#define MANUFACTURER_UBY MANFCODE('U','B','Y') -#define MANUFACTURER_UEI MANFCODE('U','E','I') -#define MANUFACTURER_UEL MANFCODE('U','E','L') -#define MANUFACTURER_UGI MANFCODE('U','G','I') -#define MANUFACTURER_UGT MANFCODE('U','G','T') -#define MANUFACTURER_UHM MANFCODE('U','H','M') -#define MANUFACTURER_UNI MANFCODE('U','N','I') -#define MANUFACTURER_UNM MANFCODE('U','N','M') -#define MANUFACTURER_UPS MANFCODE('U','P','S') -#define MANUFACTURER_URM MANFCODE('U','R','M') -#define MANUFACTURER_USC MANFCODE('U','S','C') -#define MANUFACTURER_UTF MANFCODE('U','T','F') -#define MANUFACTURER_UTI MANFCODE('U','T','I') -#define MANUFACTURER_UTL MANFCODE('U','T','L') -#define MANUFACTURER_UTS MANFCODE('U','T','S') -#define MANUFACTURER_UTT MANFCODE('U','T','T') -#define MANUFACTURER_VAI MANFCODE('V','A','I') -#define MANUFACTURER_VAL MANFCODE('V','A','L') -#define MANUFACTURER_VAR MANFCODE('V','A','R') -#define MANUFACTURER_VDP MANFCODE('V','D','P') -#define MANUFACTURER_VEC MANFCODE('V','E','C') -#define MANUFACTURER_VEL MANFCODE('V','E','L') -#define MANUFACTURER_VEN MANFCODE('V','E','N') -#define MANUFACTURER_VER MANFCODE('V','E','R') -#define MANUFACTURER_VES MANFCODE('V','E','S') -#define MANUFACTURER_VEX MANFCODE('V','E','X') -#define MANUFACTURER_VFS MANFCODE('V','F','S') -#define MANUFACTURER_VGO MANFCODE('V','G','O') -#define MANUFACTURER_VIE MANFCODE('V','I','E') -#define MANUFACTURER_VIK MANFCODE('V','I','K') -#define MANUFACTURER_VIM MANFCODE('V','I','M') -#define MANUFACTURER_VIN MANFCODE('V','I','N') -#define MANUFACTURER_VIP MANFCODE('V','I','P') -#define MANUFACTURER_VIT MANFCODE('V','I','T') -#define MANUFACTURER_VLT MANFCODE('V','L','T') -#define MANUFACTURER_VMP MANFCODE('V','M','P') -#define MANUFACTURER_VNE MANFCODE('V','N','E') -#define MANUFACTURER_VPI MANFCODE('V','P','I') -#define MANUFACTURER_VPL MANFCODE('V','P','L') -#define MANUFACTURER_VSE MANFCODE('V','S','E') -#define MANUFACTURER_VSN MANFCODE('V','S','N') -#define MANUFACTURER_VTC MANFCODE('V','T','C') -#define MANUFACTURER_VTG MANFCODE('V','T','G') -#define MANUFACTURER_VTK MANFCODE('V','T','K') -#define MANUFACTURER_VTN MANFCODE('V','T','N') -#define MANUFACTURER_VTZ MANFCODE('V','T','Z') -#define MANUFACTURER_VVV MANFCODE('V','V','V') -#define MANUFACTURER_WAD MANFCODE('W','A','D') -#define MANUFACTURER_WAH MANFCODE('W','A','H') -#define MANUFACTURER_WAI MANFCODE('W','A','I') -#define MANUFACTURER_WAL MANFCODE('W','A','L') -#define MANUFACTURER_WDN MANFCODE('W','D','N') -#define MANUFACTURER_WEB MANFCODE('W','E','B') -#define MANUFACTURER_WEG MANFCODE('W','E','G') -#define MANUFACTURER_WEH MANFCODE('W','E','H') -#define MANUFACTURER_WEL MANFCODE('W','E','L') -#define MANUFACTURER_WEP MANFCODE('W','E','P') -#define MANUFACTURER_WFT MANFCODE('W','F','T') -#define MANUFACTURER_WGN MANFCODE('W','G','N') -#define MANUFACTURER_WGP MANFCODE('W','G','P') -#define MANUFACTURER_WHU MANFCODE('W','H','U') -#define MANUFACTURER_WIG MANFCODE('W','I','G') -#define MANUFACTURER_WIK MANFCODE('W','I','K') -#define MANUFACTURER_WIN MANFCODE('W','I','N') -#define MANUFACTURER_WKL MANFCODE('W','K','L') -#define MANUFACTURER_WKX MANFCODE('W','K','X') -#define MANUFACTURER_WMO MANFCODE('W','M','O') -#define MANUFACTURER_WNC MANFCODE('W','N','C') -#define MANUFACTURER_WNW MANFCODE('W','N','W') -#define MANUFACTURER_WOW MANFCODE('W','O','W') -#define MANUFACTURER_WPZ MANFCODE('W','P','Z') -#define MANUFACTURER_WSD MANFCODE('W','S','D') -#define MANUFACTURER_WSE MANFCODE('W','S','E') -#define MANUFACTURER_WSG MANFCODE('W','S','G') -#define MANUFACTURER_WSM MANFCODE('W','S','M') -#define MANUFACTURER_WSR MANFCODE('W','S','R') -#define MANUFACTURER_WTI MANFCODE('W','T','I') -#define MANUFACTURER_WTL MANFCODE('W','T','L') -#define MANUFACTURER_WTM MANFCODE('W','T','M') -#define MANUFACTURER_WTT MANFCODE('W','T','T') -#define MANUFACTURER_WUR MANFCODE('W','U','R') -#define MANUFACTURER_WZG MANFCODE('W','Z','G') -#define MANUFACTURER_WZT MANFCODE('W','Z','T') -#define MANUFACTURER_XAO MANFCODE('X','A','O') -#define MANUFACTURER_XEE MANFCODE('X','E','E') -#define MANUFACTURER_XEM MANFCODE('X','E','M') -#define MANUFACTURER_XIO MANFCODE('X','I','O') -#define MANUFACTURER_XJM MANFCODE('X','J','M') -#define MANUFACTURER_XLG MANFCODE('X','L','G') -#define MANUFACTURER_XMA MANFCODE('X','M','A') -#define MANUFACTURER_XMX MANFCODE('X','M','X') -#define MANUFACTURER_XPP MANFCODE('X','P','P') -#define MANUFACTURER_XTM MANFCODE('X','T','M') -#define MANUFACTURER_XTR MANFCODE('X','T','R') -#define MANUFACTURER_XTY MANFCODE('X','T','Y') -#define MANUFACTURER_XYL MANFCODE('X','Y','L') -#define MANUFACTURER_YDD MANFCODE('Y','D','D') -#define MANUFACTURER_YDS MANFCODE('Y','D','S') -#define MANUFACTURER_YEM MANFCODE('Y','E','M') -#define MANUFACTURER_YFC MANFCODE('Y','F','C') -#define MANUFACTURER_YGM MANFCODE('Y','G','M') -#define MANUFACTURER_YHE MANFCODE('Y','H','E') -#define MANUFACTURER_YHF MANFCODE('Y','H','F') -#define MANUFACTURER_YJZ MANFCODE('Y','J','Z') -#define MANUFACTURER_YNE MANFCODE('Y','N','E') -#define MANUFACTURER_YNP MANFCODE('Y','N','P') -#define MANUFACTURER_YPP MANFCODE('Y','P','P') -#define MANUFACTURER_YSS MANFCODE('Y','S','S') -#define MANUFACTURER_YTE MANFCODE('Y','T','E') -#define MANUFACTURER_YTL MANFCODE('Y','T','L') -#define MANUFACTURER_YYW MANFCODE('Y','Y','W') -#define MANUFACTURER_YZR MANFCODE('Y','Z','R') -#define MANUFACTURER_ZAG MANFCODE('Z','A','G') -#define MANUFACTURER_ZAP MANFCODE('Z','A','P') -#define MANUFACTURER_ZAR MANFCODE('Z','A','R') -#define MANUFACTURER_ZCH MANFCODE('Z','C','H') -#define MANUFACTURER_ZCU MANFCODE('Z','C','U') -#define MANUFACTURER_ZEL MANFCODE('Z','E','L') -#define MANUFACTURER_ZEN MANFCODE('Z','E','N') -#define MANUFACTURER_ZEX MANFCODE('Z','E','X') -#define MANUFACTURER_ZFY MANFCODE('Z','F','Y') -#define MANUFACTURER_ZGX MANFCODE('Z','G','X') -#define MANUFACTURER_ZIP MANFCODE('Z','I','P') -#define MANUFACTURER_ZIV MANFCODE('Z','I','V') -#define MANUFACTURER_ZJY MANFCODE('Z','J','Y') -#define MANUFACTURER_ZLW MANFCODE('Z','L','W') -#define MANUFACTURER_ZOT MANFCODE('Z','O','T') -#define MANUFACTURER_ZPA MANFCODE('Z','P','A') -#define MANUFACTURER_ZQB MANFCODE('Z','Q','B') -#define MANUFACTURER_ZRI MANFCODE('Z','R','I') -#define MANUFACTURER_ZRM MANFCODE('Z','R','M') -#define MANUFACTURER_ZTE MANFCODE('Z','T','E') -#define MANUFACTURER_ZTX MANFCODE('Z','T','X') -#define MANUFACTURER_ZTY MANFCODE('Z','T','Y') -#define MANUFACTURER_ZVC MANFCODE('Z','V','C') -#define MANUFACTURER_ZXT MANFCODE('Z','X','T') -#define MANUFACTURER_ZXY MANFCODE('Z','X','Y') -#define MANUFACTURER_ZYB MANFCODE('Z','Y','B') -#define MANUFACTURER_ZYS MANFCODE('Z','Y','S') -#define MANUFACTURER_ZZZ MANFCODE('Z','Z','Z') - -#endif diff --git a/components/wmbus_common/meter.cpp b/components/wmbus_common/meter.cpp new file mode 100644 index 00000000..05a8a4cf --- /dev/null +++ b/components/wmbus_common/meter.cpp @@ -0,0 +1,606 @@ +#include "meter.h" + +#include +#include +#include +#include + +#include "esphome/components/json/json_util.h" +#include "esphome/core/alloc_helpers.h" + +#include "quirks.h" +#include "walk.h" + +namespace wmbus { + +using namespace std; + +bool MatcherSpec::vif_listed(uint16_t vif) const { + return std::ranges::any_of(vif_ranges, [vif](VifSpan span) { return span.contains(vif); }); +} + +bool MatcherSpec::combinable_listed(uint16_t raw) const { + return std::ranges::any_of(combinables, [raw](VifSpan span) { return span.contains(raw); }); +} + +bool MatcherSpec::matches(const Record &e) const { + if (!active) + return false; + if (dif_vif_key[0] != 0) + return e.key_equals(dif_vif_key); + + const bool range = vif_ranges.empty() || vif_listed(e.vif); + const bool type = measurement_type == MeasurementType::Any || e.measurement_type == measurement_type; + const bool storage_ok = !storage || storage->contains(e.storage_nr); + const bool tariff_ok = !tariff || tariff->contains(e.tariff_nr); + const bool subunit_ok = !subunit || subunit->contains(e.subunit_nr); + if (!(range && type && storage_ok && tariff_ok && subunit_ok)) + return false; + + if (combinables.empty() && !any_combinable) + return e.combinables.empty(); + if (!std::ranges::all_of(combinables, [&e](VifSpan span) { return e.has_combinable_in(span); })) + return false; + return any_combinable || std::ranges::all_of(e.combinables, [this](uint16_t raw) { return combinable_listed(raw); }); +} + +bool MatcherSpec::matches_loosely(const Record &e) const { + if (dif_vif_key[0] != 0) + return e.key_equals(dif_vif_key); + // Any and Instantaneous both mean "unconstrained" here. + return vif_listed(e.vif) && + (measurement_type == MeasurementType::Any || measurement_type == MeasurementType::Instantaneous || + measurement_type == e.measurement_type) && + (!storage || storage->from == e.storage_nr) && (!tariff || tariff->from == e.tariff_nr); +} + +static double any_factor(std::span factors, uint16_t vif) { + const auto f = std::ranges::find_if(factors, [vif](const RangeFactor &f) { return f.range.contains(vif); }); + return f == factors.end() ? NOT_A_NUMBER : f->factor; +} + +void MeterState::extract_numeric(const FieldSpec &f, const Record &dve) { + const std::optional extracted = + dve.extract_double(f.vif_scaling == VifScaling::Auto, f.dif_signedness == DifSignedness::Unsigned); + if (!extracted) + return; + + double value = *extracted; + if (f.date_record) { + DecodedDate date = dve.extract_date(); + // An unset date (0xffff and its like) is null, not a year 2127. + value = date.valid ? (double) mktime(&date.value) : NOT_A_NUMBER; + } + value *= f.scale; + if (!f.any_factors.empty()) + value *= any_factor(f.any_factors, dve.vif); + if (f.null_value && value == *f.null_value) + value = NOT_A_NUMBER; + values[f.slot] = value; +} + +void MeterState::extract_tpl_status(const FieldSpec &f, const Telegram &t) { + Status status; + status.add("OK"); + status.add(spec->tpl_status->translate(t.tpl->sts)); + values[f.slot] = status.str(); +} + +void MeterState::extract_string(const FieldSpec &f, const Telegram &t, const Record &dve) { + if (f.lookup == nullptr && f.include_tpl_status) { + extract_tpl_status(f, t); + return; + } + if (f.lookup != nullptr) { + const std::optional bits = dve.extract_long(); + if (!bits) + return; + Status status = f.lookup->translate(*bits); + if (f.include_tpl_status) + status.add(spec->tpl_status->translate(t.tpl->sts)); + values[f.slot] = status.str(); + return; + } + + switch (f.text_form) { + case TextForm::DateTime: { + const DecodedDate date = dve.extract_date(); + // Six bytes is a date with seconds and a zone. + const TimeFormat fmt = dve.bytes().size() == 6 ? TimeFormat::DateTimeSec : TimeFormat::DateTime; + values[f.slot] = date.valid ? format_time(date.value, fmt) : ""; + break; + } + case TextForm::Date: { + const DecodedDate date = dve.extract_date(); + values[f.slot] = date.valid ? format_time(date.value, TimeFormat::Date) : ""; + break; + } + case TextForm::Readable: + values[f.slot] = dve.readable_string(false); + break; + case TextForm::Reversed: + values[f.slot] = dve.readable_string(true); + break; + case TextForm::Hex: + values[f.slot] = dve.hex_string(); + break; + } +} + +void MeterState::extract(const FieldSpec &f, const Telegram &t, const Record &dve) { + if (f.kind == FieldKind::Text) + extract_string(f, t, dve); + else if (f.calculate != nullptr) + values[f.slot] = f.calculate(*this, &dve); + else + extract_numeric(f, dve); +} + +std::optional DecoderSpec::transform_payload(Key &key, Telegram &t, std::span content) const { + const PayloadTransform &pt = *transform; + if (pt.tpl_acc_offset >= content.size() || pt.offset >= content.size()) + return {}; + + size_t payload_end = content.size(); + if (pt.length > 0) { + payload_end = pt.offset + pt.length; + if (payload_end > content.size()) + return {}; + } + if (!key) + return {}; + + t.tpl->acc = content[pt.tpl_acc_offset]; + + const std::span encrypted = content.subspan(pt.offset, payload_end - pt.offset); + if (!t.aes_cbc_iv_decrypt(encrypted, key)) + return {}; + + memmove(content.data(), encrypted.data(), encrypted.size()); + return encrypted.size(); +} + +void DecoderSpec::note_decoding_error(Telegram &t) const { + if (required) + t.decoding_errors.add(string("DECODING_ERROR_") + name); +} + +void MeterState::run_decoders(Telegram &t) { + for (const DecoderSpec &d : spec->decoders) { + if (d.scope == MatchScope::Frame) { + if (!t.add_ixml_records(0, t.frame, *d.grammar)) + d.note_decoding_error(t); + continue; + } + + if (d.scope == MatchScope::Payload) { + // Records point into these bytes, so a rewritten payload must live in `derived`. + std::span bytes = t.payload(); + if (spec->payload_quirk == PayloadQuirk::DiehlPrios) { + bytes = t.derived.front(); + } else if (d.transform) { + std::vector rewritten(bytes.begin(), bytes.end()); + const std::optional transformed = d.transform_payload(key, t, rewritten); + if (!transformed) + continue; + rewritten.resize(*transformed); + bytes = t.derived.emplace_back(std::move(rewritten)); + } + if (!t.add_ixml_records(t.header_size, bytes, *d.grammar)) + d.note_decoding_error(t); + continue; + } + + // Records this grammar adds are visited too, as upstream re-reads them. + for_each_record(t, [&](const Record &dve) { + if (!d.matcher.matches(dve)) + return; + + std::span bytes = dve.bytes(); + // A profile's point dies with the visit; the records must outlive it. + if (!dve.computed.empty()) + bytes = t.derived.emplace_back(bytes.begin(), bytes.end()); + bool decrypted = false; + if (spec->payload_quirk == PayloadQuirk::TryQundisDecode) { + if (std::optional> plain = quirks::qundis_walk_by_decode(t, key, bytes)) { + bytes = t.derived.emplace_back(std::move(*plain)); + decrypted = true; + } + } + + if (!t.add_ixml_records(dve.offset, bytes, *d.grammar)) { + d.note_decoding_error(t); + // Mode 5 has no integrity check: a wrong key decrypts into bytes the grammar rejects. + if (decrypted) + t.decoding_errors.add("FAILED_DECODE"); + } + }); + } +} + +static std::optional find_loosely(const MatcherSpec &m, const Telegram &t) { + if (!m.active) + return {}; + int nr = m.index_nr; + std::optional found; + for_each_record(t, [&](const Record &r) { + if (!found && m.matches_loosely(r) && --nr <= 0) + found = r; + }); + return found; +} + +namespace { + +struct ExtractionPass { + static constexpr uint16_t NO_OWNER = 0xffff; + + std::vector match_nr; + std::vector extracted; + std::vector owner; // by slot: the field that last wrote it + + ExtractionPass(size_t fields, size_t slots) : match_nr(fields, 0), extracted(fields, false), owner(slots, NO_OWNER) {} + + // Upstream is field-major, so of two fields sharing a slot the later one wins. + bool claim(uint16_t slot, size_t field_index) { + if (owner[slot] != NO_OWNER && owner[slot] > field_index) + return false; + owner[slot] = (uint16_t) field_index; + return true; + } +}; + +} // namespace + +// Record-major where upstream is field-major; claim() keeps the same write winning. +void MeterState::process_field_extractors(Telegram &t) { + const DriverSpec &driver = *spec; + const size_t field_count = driver.fields.size(); + + ExtractionPass pass(field_count, driver.value_slots); + + for_each_record(t, [&](const Record &dve) { + for (size_t i = 0; i < field_count; ++i) { + const FieldSpec &f = driver.fields[i]; + if (!f.matcher.active) + continue; + if (!f.matcher.matches(dve)) + continue; + pass.match_nr[i]++; + if (f.matcher.index_nr != pass.match_nr[i] && !f.extract_all_matches) + continue; + if (!pass.claim(f.slot, i)) + continue; + extract(f, t, dve); + pass.extracted[i] = true; + } + }); + + // Last, so a formula may read the values stored above. + for (size_t i = 0; i < field_count; ++i) { + const FieldSpec &f = driver.fields[i]; + if (f.matcher.active && pass.extracted[i]) + continue; + if (f.kind == FieldKind::Text && f.include_tpl_status) { + if (const std::optional loose = find_loosely(f.matcher, t)) + extract(f, t, *loose); + else + extract_tpl_status(f, t); + } else if (!f.matcher.active && f.calculate != nullptr) { + values[f.slot] = f.calculate(*this, nullptr); + } + } +} + +string LookupRule::to_string(uint64_t bits) const { + if (this->pre_shift_right) + bits >>= this->pre_shift_right; + switch (type) { + case MapType::BitToString: + return bit_to_string(bits); + case MapType::IndexToString: + return index_to_string(bits); + case MapType::DecimalsToString: + return decimals_to_string(bits); + } + return ""; +} + +string LookupRule::bit_to_string(uint64_t bits) const { + string s; + bits = bits & mask; + for (const LookupMap &m : map) { + const uint64_t from = m.from & mask; + if (m.test == TestBit::Set) { + if ((bits & from) != 0) { + s += m.to; + s += " "; + bits = bits & ~m.from; + } + } + if (m.test == TestBit::NotSet) { + if ((bits & from) == 0) { + s += m.to; + s += " "; + } else { + bits = bits & ~m.from; + } + } + } + if (bits != 0) + s += esphome::str_sprintf("%s_%X ", name, (unsigned) bits); + if (s.empty()) + s = default_message; + return s; +} + +string LookupRule::index_to_string(uint64_t bits) const { + string s; + bits = bits & mask; + bool found = false; + for (const LookupMap &m : map) { + if (bits == (m.from & mask)) { + s += m.to; + s += " "; + found = true; + } + } + if (!found) + s += esphome::str_sprintf("%s_%X ", name, (unsigned) bits); + return s; +} + +string LookupRule::decimals_to_string(uint64_t bits) const { + string s; + int number = bits % mask; + if (number == 0) + s = default_message; + for (const LookupMap &m : map) { + const int num = m.from % mask; + if ((number - num) >= 0) { + s += m.to; + s += " "; + number -= num; + } + } + if (number > 0) + s += esphome::str_sprintf("%s_%d ", name, number); + return s; +} + +Status LookupSpec::translate(uint64_t bits) const { + Status status; + for (const LookupRule &r : rules) + status.add(r.to_string(bits)); + return status; +} + +const FieldSpec *MeterState::find_field(const string &name) const { + if (spec == nullptr) + return nullptr; + const auto f = std::ranges::find(spec->fields, name, &FieldSpec::name); + return f == spec->fields.end() ? nullptr : &*f; +} + +// A filled slot always holds its field's kind, fixed at codegen. +template static std::optional value_of(const Value &slot) { + if (holds_alternative(slot)) + return {}; + return get(slot); +} + +string MeterState::status_field(const string &stored) const { + Status status; + status.add("OK"); + status.add(stored); + for (const FieldSpec &g : spec->fields) { + if (g.status_role != StatusRole::Injects) + continue; + if (const std::optional inject = value_of(values[g.slot])) + status.add(*inject); + } + // Decoding errors follow the flags unsorted, the way upstream joins them. + const string flags = status.str(); + if (decoding_errors.empty()) + return flags; + return flags == "OK" ? decoding_errors.str() : flags + " " + decoding_errors.str(); +} + +void MeterState::set_text_by_name(const char *field_name, const string &value) { + const FieldSpec *f = find_field(field_name); + if (f != nullptr) + values[f->slot] = value; +} + +bool MeterState::decode_diehl_prios(Telegram &t) { + if (!quirks::diehl_prios_decode(t, key)) + return false; + if (const std::optional who = quirks::sap_prios_identity(t)) { + set_text_by_name("prefix", who->prefix); + set_text_by_name("serial_number", who->serial_number); + set_text_by_name("manufacture_y", who->manufacture_y); + } + return true; +} + +// Not in the constructor: an auto meter learns its driver from its first telegram. +void MeterState::bind_driver(const DriverSpec &driver) { + spec = &driver; + values.assign(driver.value_slots, Value{}); + formats.registered = driver.compact_formats; +} + +MeterState::MeterState(const DriverSpec *driver, uint32_t meter_id, std::optional> key) + : id(meter_id), key(std::move(key)) { + if (driver != nullptr) + bind_driver(*driver); +} + +// Deepest layer first: behind a radio converter the meter is in the TPL header, the +// converter in the DLL, and some drivers' mvt lines match only one of the two. +static std::vector identities_of(const Telegram &t) { + // Bit 15 of M is reserved, but apator08 sets it (0x8614 for APT). + constexpr uint16_t MFCT_MASK = 0x7fff; + + const auto identity = [](const Address &a) { return DetectSpec{(uint16_t) (a.mfct & MFCT_MASK), a.version, a.type}; }; + std::vector who; + if (t.tpl && t.tpl->address) + who.push_back(identity(*t.tpl->address)); + who.push_back(identity(t.dll.address)); + return who; +} + +string DetectSpec::as_mvt() const { + char buf[16]; + snprintf(buf, sizeof(buf), "%c%c%c,%02x,%02x", (char) (((mfct >> 10) & 0x1f) + 64), + (char) (((mfct >> 5) & 0x1f) + 64), (char) ((mfct & 0x1f) + 64), version, type); + return buf; +} + +Result MeterState::handle_telegram(std::span frame, int rssi, time_t at) { + Telegram t; + const auto names_us = [this](const auto &layer) { return layer && layer->address && layer->address->id == id; }; + if (!t.parse_envelope(frame) || !(t.dll.address.id == id || names_us(t.ell) || names_us(t.tpl))) + return Result::NotForThisMeter; + + if (spec == nullptr) { + const DriverSpec *detected = nullptr; + string tried; + for (const DetectSpec &who : identities_of(t)) { + detected = detect_driver(who.mfct, who.version, who.type); + if (detected != nullptr) + break; + tried += (tried.empty() ? "" : " nor ") + who.as_mvt(); + } + + if (detected == nullptr) { + last_error = "no compiled driver detects " + tried; + return Result::NotDecoded; + } + bind_driver(*detected); + } + + if (!t.decode(key, formats, spec->payload_quirk) || + (spec->payload_quirk == PayloadQuirk::DiehlPrios && !decode_diehl_prios(t))) { + last_error = t.decryption_failed ? "decryption failed" + : key ? "payload not understood" + : "encrypted, no key configured"; + if (!t.decoding_errors.empty()) + last_error += " (" + t.decoding_errors.str() + ")"; + return Result::NotDecoded; + } + + rssi_dbm = rssi; + media_type = spec->force_media_type != nullptr ? spec->force_media_type : t.media_type(); + run_decoders(t); + process_field_extractors(t); + + decoding_errors = t.decoding_errors; + last_error.clear(); + timestamp = at ? at : time(nullptr); + return Result::Ok; +} + +Value MeterState::value(const FieldSpec &f) const { + const Value &stored = values[f.slot]; + if (f.status_role == StatusRole::Holds) { + if (const std::optional text = value_of(stored)) + return status_field(*text); + } + return stored; +} + +Value MeterState::value(const string &name) const { + const FieldSpec *f = find_field(name); + return f == nullptr ? Value{} : value(*f); +} + +string MeterState::to_json() const { + JsonDocument doc; + JsonObject root = doc.to(); + root["_"] = "telegram"; + root["media"] = media_type; + if (spec != nullptr) + root["driver"] = spec->name; + char id_buf[9]; + snprintf(id_buf, sizeof(id_buf), "%08x", (unsigned) id); + root["id"] = id_buf; + + if (spec != nullptr) { + for (const FieldSpec &f : spec->fields) { + const Value v = value(f); + if (holds_alternative(v)) + continue; + if (const double *number = get_if(&v); number && f.json_date && !std::isnan(*number)) + root[f.name] = format_time(*number, *f.json_date); + else if (number) + root[f.name] = *number; + else + root[f.name] = get(v); + } + } + + if (timestamp.has_value()) + root["timestamp"] = format_time((double) *timestamp, TimeFormat::TimestampUTC); + root["rssi_dbm"] = rssi_dbm; + + string out; + serializeJson(doc, out); + return out; +} + +const DriverSpec *detect_driver(uint16_t mfct, uint8_t version, uint8_t type) { + const auto detects = [&](const DetectSpec &d) { + return d.mfct == mfct && (d.version == DETECT_ANY || d.version == version) && + (d.type == DETECT_ANY || d.type == type); + }; + const auto spec = std::ranges::find_if(registered_drivers, + [&](const DriverSpec *s) { return std::ranges::any_of(s->detect, detects); }); + return spec == registered_drivers.end() ? nullptr : *spec; +} + +double MeterState::field_value(uint16_t slot) const { return value_of(values[slot]).value_or(NOT_A_NUMBER); } + +double counter_value(const Record *dve, DVEntryCounterType counter) { + if (dve == nullptr) + return NOT_A_NUMBER; + switch (counter) { + case DVEntryCounterType::STORAGE_COUNTER: + return dve->storage_nr; + case DVEntryCounterType::TARIFF_COUNTER: + return dve->tariff_nr; + case DVEntryCounterType::SUBUNIT_COUNTER: + return dve->subunit_nr; + } + return NOT_A_NUMBER; +} + +// A date literal converts as UTC calendar time. +double datetime_literal(int year, int month, int day, int hour, int minute, int second) { + struct tm t{}; + t.tm_year = year - 1900; + t.tm_mon = month - 1; + t.tm_mday = day; + t.tm_hour = hour; + t.tm_min = minute; + t.tm_sec = second; + t.tm_isdst = 0; + return (double) mktime(&t); +} + +double make_date(double year, double month, double day) { + int y = (int) llround(year); + int m = (int) llround(month); + int d = (int) llround(day); + if (m <= 0 || d <= 0) + return NOT_A_NUMBER; + + struct tm t{}; + t.tm_year = y - 1900; + t.tm_mon = m - 1; + t.tm_mday = d; + t.tm_isdst = 0; + return (double) mktime(&t); +} + +} // namespace wmbus diff --git a/components/wmbus_common/meter.h b/components/wmbus_common/meter.h new file mode 100644 index 00000000..1aac5c3b --- /dev/null +++ b/components/wmbus_common/meter.h @@ -0,0 +1,265 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "records.h" +#include "telegram.h" +#include "wmbusmeters_generated/registry.h" + +namespace wmbus { + +enum class TestBit : uint8_t { Set, NotSet }; + +enum class MapType : uint8_t { BitToString, IndexToString, DecimalsToString }; + +enum class FieldKind : uint8_t { Numeric, Text }; + +// Factor from one unit of an Any* range to the field's unit. +struct RangeFactor { + VifSpan range; + double factor; +}; + +enum class TextForm : uint8_t { Hex, Readable, Reversed, Date, DateTime }; + +// JSON prints it as null. +inline constexpr double NOT_A_NUMBER = std::numeric_limits::quiet_NaN(); + +// monostate: not filled yet. +using Value = std::variant; +enum class VifScaling : uint8_t { Auto, None }; +enum class DifSignedness : uint8_t { Signed, Unsigned }; + +// What an ixml grammar is matched against. +enum class MatchScope : uint8_t { Entry, Payload, Frame }; + +enum class DVEntryCounterType : uint8_t { STORAGE_COUNTER, TARIFF_COUNTER, SUBUNIT_COUNTER }; + +enum class StatusRole : uint8_t { + None, + Holds, // assembled with "OK", the injected texts and the decoding errors + Injects, // folded into the Holds field +}; + +struct CounterRange { + int16_t from; + int16_t to; + + constexpr bool contains(int32_t nr) const { return nr >= from && nr <= to; } +}; + +using MaybeRange = std::optional; + +// "Unconstrained" lives in the value: Any, an empty list, an empty range. +struct MatcherSpec { + const char *dif_vif_key; // "" when not matching on a key + // Required; without any_combinable also the only ones allowed. + std::span combinables; + bool any_combinable; + MeasurementType measurement_type; + std::span vif_ranges; + MaybeRange storage; + MaybeRange tariff; + MaybeRange subunit; // always set: upstream has no AnySubunitNr + uint8_t index_nr; + bool active; + + bool matches(const Record &e) const; + // A status field's fallback: ignores combinables and subunit, as upstream does. + bool matches_loosely(const Record &e) const; + + private: + bool vif_listed(uint16_t vif) const; + bool combinable_listed(uint16_t raw) const; +}; + +struct LookupMap { + uint64_t from; + const char *to; + TestBit test; +}; + +struct LookupRule { + const char *name; + MapType type; + uint64_t mask; // never 0: codegen fills in the bits of the map + const char *default_message; + std::span map; + uint8_t pre_shift_right{0}; + + std::string to_string(uint64_t bits) const; + + private: + std::string bit_to_string(uint64_t bits) const; + std::string index_to_string(uint64_t bits) const; + std::string decimals_to_string(uint64_t bits) const; +}; + +struct LookupSpec { + std::span rules; + + Status translate(uint64_t bits) const; +}; + +struct MeterState; + +enum class Result : uint8_t { + NotForThisMeter, + NotDecoded, // see last_error + Ok, +}; + +// A `calculate` formula compiled to C++; `dve` is null for a field without a matcher. +typedef double (*CalculateFn)(const MeterState &state, const Record *dve); + +// transform_payload = tpl_aes_cbc_iv,offset,length,tpl_acc_offset +struct PayloadTransform { + uint16_t offset; + uint16_t length; + uint16_t tpl_acc_offset; +}; + +struct FieldSpec { + const char *name; // unit suffix included: "total_m3" + FieldKind kind; + VifScaling vif_scaling; + DifSignedness dif_signedness; + StatusRole status_role; + bool include_tpl_status; + double scale; // force_scale times the matcher-unit-to-field-unit factor + std::span any_factors; + std::optional null_value; + MatcherSpec matcher; + bool extract_all_matches; // index_nr does not apply + const LookupSpec *lookup; + bool date_record; // numeric, in seconds since the epoch + std::optional json_date; + TextForm text_form; + CalculateFn calculate; + uint16_t slot; // index into MeterState::values +}; + +// An XMQ field whose ixml grammar derives records for the fields to match. +struct DecoderSpec { + const char *name; + const IxmlGrammar *grammar; + MatchScope scope; + MatcherSpec matcher; // Entry: the records the grammar runs over + std::optional transform; // Payload: a slice decrypted first + bool required; // a payload the grammar turns down is a decoding error + + // Decrypts a slice of the payload in place; returns the new content length. + std::optional transform_payload(Key &key, Telegram &t, std::span content) const; + void note_decoding_error(Telegram &t) const; +}; + +// XMQ's `*` in an mvt's version or type. +constexpr uint8_t DETECT_ANY = 0xff; + +// One `detect { mvt = APA,05,06 }` line; also the identity read out of a frame. +struct DetectSpec { + uint16_t mfct; + uint8_t version; + uint8_t type; + + std::string as_mvt() const; +}; + +struct DriverSpec { + const char *name; + std::span detect; + std::span fields; + std::span decoders; + uint16_t value_slots; + std::span> compact_formats; + const LookupSpec *tpl_status; // null when no field includes it + std::optional payload_quirk; + const char *force_media_type; // null: the telegram names the media +}; + +struct MeterState { + const DriverSpec *spec = nullptr; + uint32_t id = 0; + Key key; + + // By slot; JSON leaves monostate out and prints NaN as null. + std::vector values; + + int rssi_dbm = 0; + // Of the last decoded telegram; empty until one is. + std::optional timestamp; + const char *media_type = nullptr; + Status decoding_errors; + std::string last_error; + + // Outlives the telegram: a compact frame refers to a layout learned earlier. + CompactFormats formats; + + // A null driver is `type: auto`: detected from the first telegram for this id. + MeterState() = default; + MeterState(const DriverSpec *driver, uint32_t meter_id, std::optional> key); + + // The frame comes in without its link-layer CRCs. + Result handle_telegram(std::span frame, int rssi = 0, time_t at = 0); + + // {"media": "water", "total_m3": 12.5, "status": "OK"}; dates as upstream writes them. + std::string to_json() const; + + // Monostate when absent or unfilled; a STATUS field comes out assembled from the others. + Value value(const std::string &name) const; + + // NaN when the slot has none. + double field_value(uint16_t slot) const; + + const FieldSpec *find_field(const std::string &name) const; + + private: + void bind_driver(const DriverSpec &driver); + + Value value(const FieldSpec &f) const; + std::string status_field(const std::string &stored) const; + void set_text_by_name(const char *field_name, const std::string &value); + + void extract(const FieldSpec &f, const Telegram &t, const Record &dve); + void extract_numeric(const FieldSpec &f, const Record &dve); + void extract_string(const FieldSpec &f, const Telegram &t, const Record &dve); + void extract_tpl_status(const FieldSpec &f, const Telegram &t); + + void run_decoders(Telegram &t); + void process_field_extractors(Telegram &t); + + bool decode_diehl_prios(Telegram &t); +}; + +// An ambiguous mvt goes to the first match. +const DriverSpec *detect_driver(uint16_t mfct, uint8_t version, uint8_t type); + +// Formula values are all doubles: an integer operator rounds, applies itself and converts back. +double counter_value(const Record *dve, DVEntryCounterType counter); +double datetime_literal(int year, int month, int day, int hour, int minute, int second); +double make_date(double year, double month, double day); + +inline uint64_t as_integer(double v) { return (uint64_t) __builtin_llround(v); } + +inline double modulo(double l, double r) { return r == 0.0 ? __builtin_nan("") : __builtin_fmod(l, r); } + +inline double shift_left(double l, double r) { + return (r < 0.0 || r > 63.0) ? __builtin_nan("") : (double) (as_integer(l) << as_integer(r)); +} + +inline double shift_right(double l, double r) { + return (r < 0.0 || r > 63.0) ? __builtin_nan("") : (double) (as_integer(l) >> as_integer(r)); +} + +inline double bit_and(double l, double r) { return (double) (as_integer(l) & as_integer(r)); } +inline double bit_or(double l, double r) { return (double) (as_integer(l) | as_integer(r)); } +inline double bit_xor(double l, double r) { return (double) (as_integer(l) ^ as_integer(r)); } + +} // namespace wmbus diff --git a/components/wmbus_common/meters.cpp b/components/wmbus_common/meters.cpp deleted file mode 100644 index 0b1e2d95..00000000 --- a/components/wmbus_common/meters.cpp +++ /dev/null @@ -1,2904 +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.h" -#include"meters_common_implementation.h" -#include"units.h" -#include"wmbus.h" -#include"wmbus_utils.h" - -#include -#include -#include -#include -#include -#include -#include - -std::map *registered_drivers_ = NULL; -std::vector *registered_drivers_list_ = NULL; -std::map removed_driver_explanation_; - -void verifyDriverLookupCreated() -{ - if (registered_drivers_ == NULL) - { - registered_drivers_ = new std::map; - } - if (registered_drivers_list_ == NULL) - { - registered_drivers_list_ = new std::vector; - } -} - -DriverInfo *lookupDriver(std::string name) -{ - verifyDriverLookupCreated(); - - // Check if we have a compiled/loaded driver available. - if (registered_drivers_->count(name) == 1) - { - return &(*registered_drivers_)[name]; - } - - // No, ok lets look for driver aliases. - for (DriverInfo *di : *registered_drivers_list_) - { - for (DriverName &dn : di->nameAliases()) - { - if (dn.str() == name) - { - return di; - } - } - } - - return NULL; -} - -std::vector &allDrivers() -{ - return *registered_drivers_list_; -} - -void removeDriver(const std::string &name, std::string explanation) -{ - for (auto i = registered_drivers_list_->begin(); i != registered_drivers_list_->end(); i++) - { - if ((*i)->name().str() == name) - { - registered_drivers_list_->erase(i); - break; - } - } - - registered_drivers_->erase(name); - removed_driver_explanation_[name] = explanation; - assert(registered_drivers_->count(name) == 0); -} - -std::string removedDriverExplanation(const std::string& name) -{ - if (removed_driver_explanation_.count(name) > 0) - { - return removed_driver_explanation_[name]; - } - return ""; -} - -void addRegisteredDriver(DriverInfo di) -{ - verifyDriverLookupCreated(); - if (registered_drivers_->count(di.name().str()) != 0) - { - error("Two drivers trying to register the name \"%s\"\n", di.name().str().c_str()); - exit(1); - } - - (*registered_drivers_)[di.name().str()] = di; - // The list elements points into the map. - (*registered_drivers_list_).push_back(lookupDriver(di.name().str())); -} - -bool DriverInfo::detect(uint16_t mfct, uchar version, uchar type) -{ - for (auto &dd : mvts_) - { - if (dd.mfct == 0 && dd.type == 0 && dd.version == 0) continue; // Ignore drivers with no detection. - // Some weird meters (aptor08 and itronheat) send a mfct where the first character is lower case, - // which results in mfct which are bigger than 32767, therefore restrict mfct to correct range - // and the normal check will work. - if ((dd.mfct & 0x7fff) == (mfct & 0x7fff) && dd.version == version && dd.type == type ) return true; - } - return false; -} - -void DriverInfo::setAliases(string a) -{ - if (a == "") return; - - auto as = splitString(a, ','); - for (string& s : as) - { - addNameAlias(s); - } -} - -bool DriverInfo::isValidMedia(uchar type) -{ - for (auto &dd : mvts_) - { - if (dd.type == type) return true; - } - return false; -} - -DriverInfo::~DriverInfo() -{ -} - -bool DriverInfo::isCloseEnoughMedia(uchar type) -{ - for (auto &dd : mvts_) - { - if (isCloseEnough(dd.type, type)) return true; - } - return false; -} - -bool staticRegisterDriver(function setup) -{ - DriverInfo di; - setup(di); - - // Check that the driver name has not been registered before! - assert(lookupDriver(di.name().str()) == NULL); - - // Check that no other driver also triggers on the same detection values. - for (auto &d : di.mvts()) - { - for (DriverInfo *p : allDrivers()) - { - bool foo = p->detect(d.mfct, d.version, d.type); - if (foo) - { - error("Internal error: driver %s tried to register the same auto detect combo as driver %s alread has taken!\n", - di.name().str().c_str(), p->name().str().c_str()); - } - } - } - - // Everything looks, good install this driver. - addRegisteredDriver(di); - - // This code is invoked from the static initializers of DriverInfos when starting - // wmbusmeters. Thus we do not yet know if the user has supplied --debug or similar setting. - // To debug this you have to uncomment the printf below. - // fprintf(stderr, "(STATIC) added driver: %s\n", di.name().str().c_str()); - return true; -} - -bool lookupDriverInfo(const std::string& driver_name, DriverInfo *out_di) -{ - // Lookup an already loaded driver, it might be compiled in as well. - DriverInfo *di = lookupDriver(driver_name); - if (di) - { - if (out_di) *out_di = *di; - return true; - } - - return false; -} - -MeterCommonImplementation::MeterCommonImplementation(MeterInfo &mi, - DriverInfo &di) : - type_(di.type()), - driver_name_(di.name()), - driver_info_(&di), - bus_(mi.bus), - name_(mi.name), - mfct_tpl_status_bits_(di.mfctTPLStatusBits()), - has_process_content_(di.hasProcessContent()), - more_records_follow_(false) -{ - address_expressions_ = mi.address_expressions; - identity_mode_ = mi.identity_mode; - link_modes_ = mi.link_modes; - poll_interval_= mi.poll_interval; - - if (mi.key.length() > 0) - { - hex2bin(mi.key, &meter_keys_.confidentiality_key); - } - for (auto s : mi.shells) - { - addShellMeterUpdated(s); - } - for (auto s : mi.new_meter_shells) - { - addShellMeterAdded(s); - } - for (auto j : mi.extra_constant_fields) - { - addExtraConstantField(j); - } - - link_modes_.unionLinkModeSet(di.linkModes()); - force_mfct_index_ = di.forceMfctIndex(); -} - -void MeterCommonImplementation::addShellMeterAdded(std::string cmdline) -{ - shell_cmdlines_added_.push_back(cmdline); -} - -void MeterCommonImplementation::addShellMeterUpdated(std::string cmdline) -{ - shell_cmdlines_updated_.push_back(cmdline); -} - -void MeterCommonImplementation::addExtraConstantField(std::string ecf) -{ - extra_constant_fields_.push_back(ecf); -} - -void MeterCommonImplementation::addExtraCalculatedField(std::string ecf) -{ - verbose("(meter) Adding calculated field: %s\n", ecf.c_str()); - - std::vector parts = splitString(ecf, '='); - - if (parts.size() != 2) - { - warning("Invalid formula for calculated field. %s\n", ecf.c_str()); - return; - } - - std::string vname; - Unit unit; - - bool ok = extractUnit(parts[0], &vname, &unit); - if (!ok) - { - warning("Could not extract a valid unit from calculated field name %s\n", parts[0].c_str()); - return; - } - - Quantity quantity = toQuantity(unit); - - FieldInfo *existing = findFieldInfo(vname, quantity); - if (existing != NULL) - { - if (!canConvert(unit, existing->displayUnit())) - { - warning("Warning! Cannot add the calculated field: %s since it would conflict with the already declared field %s for quantity %s.\n", - parts[0].c_str(), vname.c_str(), toString(quantity)); - return; - } - } - - addNumericFieldWithCalculator( - vname, - "Calculated: "+ecf, - DEFAULT_PRINT_PROPERTIES, - quantity, - parts[1], - unit - ); -} - -std::vector &MeterCommonImplementation::shellCmdlinesMeterAdded() -{ - return shell_cmdlines_added_; -} - -std::vector &MeterCommonImplementation::shellCmdlinesMeterUpdated() -{ - return shell_cmdlines_updated_; -} - -std::vector &MeterCommonImplementation::meterExtraConstantFields() -{ - return extra_constant_fields_; -} - -DriverName MeterCommonImplementation::driverName() -{ - return driver_name_; -} - -DriverInfo *MeterCommonImplementation::driverInfo() -{ - return driver_info_; -} - -void MeterCommonImplementation::setMeterType(MeterType mt) -{ - type_ = mt; -} - -void MeterCommonImplementation::addLinkMode(LinkMode lm) -{ - link_modes_.addLinkMode(lm); -} - -void MeterCommonImplementation::setMfctTPLStatusBits(Translate::Lookup &lookup) -{ - mfct_tpl_status_bits_ = lookup; -} - -void MeterCommonImplementation::markLastFieldAsLibrary() -{ - field_infos_.back().markAsLibrary(); - num_driver_fields_--; -} - -void MeterCommonImplementation::addNumericFieldWithExtractor(std::string vname, - std::string help, - PrintProperties print_properties, - Quantity vquantity, - VifScaling vif_scaling, - DifSignedness dif_signedness, - FieldMatcher matcher, - Unit display_unit, - double scale) -{ - size_t index = num_driver_fields_++; - field_infos_.emplace_back( - FieldInfo(index, - vname, - vquantity, - display_unit == Unit::Unknown ? defaultUnitForQuantity(vquantity) : display_unit, - vif_scaling, - dif_signedness, - scale, - matcher, - help, - print_properties, - NULL, - NULL, - NULL, - NULL, - NoLookup, /* Lookup table */ - NULL, /* Formula */ - this /* Meter */ - )); -} - -void MeterCommonImplementation::addNumericFieldWithCalculator(std::string vname, - std::string help, - PrintProperties print_properties, - Quantity vquantity, - std::string formula, - Unit display_unit) -{ - Formula *f = newFormula(); - bool ok = f->parse(this, formula); - if (!ok) - { - std::string err = f->errors(); - warning("Warning! Ignoring calculated field %s because parse failed:\n%s", - vname.c_str(), - err.c_str()); - delete f; - return; - } - assert(ok); - - size_t index = num_driver_fields_++; - field_infos_.push_back( - FieldInfo(index, - vname, - vquantity, - display_unit == Unit::Unknown ? defaultUnitForQuantity(vquantity) : display_unit, - VifScaling::Auto, - DifSignedness::Signed, - 1.0, - FieldMatcher::noMatcher(), - help, - print_properties, - NULL, - NULL, - NULL, - NULL, - NoLookup, /* Lookup table */ - f, /* Formula */ - this /* Meter */ - )); -} - -void MeterCommonImplementation::addNumericFieldWithCalculatorAndMatcher(std::string vname, - std::string help, - PrintProperties print_properties, - Quantity vquantity, - std::string formula, - FieldMatcher matcher, - Unit display_unit) -{ - Formula *f = newFormula(); - bool ok = f->parse(this, formula); - if (!ok) - { - std::string err = f->errors(); - warning("Warning! Ignoring calculated field %s because parse failed:\n%s", - vname.c_str(), - err.c_str()); - delete f; - return; - } - assert(ok); - - size_t index = num_driver_fields_++; - field_infos_.push_back( - FieldInfo(index, - vname, - vquantity, - display_unit == Unit::Unknown ? defaultUnitForQuantity(vquantity) : display_unit, - VifScaling::Auto, - DifSignedness::Signed, - 1.0, - matcher, - help, - print_properties, - NULL, - NULL, - NULL, - NULL, - NoLookup, /* Lookup table */ - f, /* Formula */ - this /* Meter */ - )); -} - - -void MeterCommonImplementation::addNumericField( - std::string vname, - Quantity vquantity, - PrintProperties print_properties, - std::string help, - Unit display_unit) -{ - size_t index = num_driver_fields_++; - field_infos_.emplace_back( - FieldInfo(index, - vname, - vquantity, - display_unit == Unit::Unknown ? defaultUnitForQuantity(vquantity) : display_unit, - VifScaling::None, - DifSignedness::Signed, - 1.0, - FieldMatcher::noMatcher(), - help, - print_properties, - NULL, // getValueFunc, - NULL, - NULL, // setValueFunc - NULL, - NoLookup, /* Lookup table */ - NULL, /* Formula */ - this /* Meter */ - )); -} - -void MeterCommonImplementation::addStringFieldWithExtractor(std::string vname, - std::string help, - PrintProperties print_properties, - FieldMatcher matcher) -{ - size_t index = num_driver_fields_++; - field_infos_.emplace_back( - FieldInfo(index, - vname, - Quantity::Text, - defaultUnitForQuantity(Quantity::Text), - VifScaling::None, - DifSignedness::Signed, - 1.0, - matcher, - help, - print_properties, - NULL, - NULL, - NULL, - NULL, - NoLookup, /* Lookup table */ - NULL, /* Formula */ - this /* Meter */ - )); -} - -void MeterCommonImplementation::addStringFieldWithExtractorAndLookup(std::string vname, - std::string help, - PrintProperties print_properties, - FieldMatcher matcher, - Translate::Lookup lookup) -{ - size_t index = num_driver_fields_++; - field_infos_.emplace_back( - FieldInfo(index, - vname, - Quantity::Text, - defaultUnitForQuantity(Quantity::Text), - VifScaling::None, - DifSignedness::Signed, - 1.0, - matcher, - help, - print_properties, - NULL, - NULL, - NULL, - NULL, - lookup, - NULL, /* Formula */ - this /* Meter */ - )); -} - -void MeterCommonImplementation::addStringField(std::string vname, - std::string help, - PrintProperties print_properties) -{ - size_t index = num_driver_fields_++; - field_infos_.emplace_back( - FieldInfo(index, - vname, - Quantity::Text, - defaultUnitForQuantity(Quantity::Text), - VifScaling::None, - DifSignedness::Signed, - 1.0, - FieldMatcher(), - help, - print_properties, - NULL, - NULL, - NULL, - NULL, - NoLookup, /* Lookup table */ - NULL, /* Formula */ - this /* Meter */ - )); -} - -std::vector& MeterCommonImplementation::addressExpressions() -{ - return address_expressions_; -} - -IdentityMode MeterCommonImplementation::identityMode() -{ - return identity_mode_; -} - -std::vector &MeterCommonImplementation::fieldInfos() -{ - return field_infos_; -} - -std::vector &MeterCommonImplementation::extraConstantFields() -{ - return extra_constant_fields_; -} - -std::string MeterCommonImplementation::name() -{ - return name_; -} - -int MeterCommonImplementation::numUpdates() -{ - return num_updates_; -} - -std::string MeterCommonImplementation::datetimeOfUpdateHumanReadable() -{ - char datetime[40]; - memset(datetime, 0, sizeof(datetime)); - strftime(datetime, 20, "%Y-%m-%d %H:%M:%S", localtime(&datetime_of_update_)); - return std::string(datetime); -} - -std::string MeterCommonImplementation::datetimeOfUpdateRobot() -{ - char datetime[40]; - memset(datetime, 0, sizeof(datetime)); - // This is the date time in the Greenwich timezone (Zulu time), dont get surprised! - time_t d = datetime_of_update_; - struct tm ts; - gmtime_r(&d, &ts); - strftime(datetime, sizeof(datetime), "%FT%TZ", &ts); - return std::string(datetime); -} - -std::string MeterCommonImplementation::unixTimestampOfUpdate() -{ - char ut[40]; - memset(ut, 0, sizeof(ut)); - snprintf(ut, sizeof(ut)-1, "%lu", datetime_of_update_); - return std::string(ut); -} - -time_t MeterCommonImplementation::timestampLastUpdate() -{ - return datetime_of_update_; -} - -void MeterCommonImplementation::setPollInterval(time_t interval) -{ - poll_interval_ = interval; - if (usesPolling() && poll_interval_ == 0) - { - std::string aesc = AddressExpression::concat(addressExpressions()); - warning("(meter) %s %s needs polling but has no pollinterval set!\n", - name().c_str(), - aesc.c_str()); - } -} - -time_t MeterCommonImplementation::pollInterval() -{ - return poll_interval_; -} - -bool MeterCommonImplementation::usesPolling() -{ - return link_modes_.has(LinkMode::MBUS) || - link_modes_.has(LinkMode::C2) || - link_modes_.has(LinkMode::T2) || - link_modes_.has(LinkMode::S2); -} - -bool driverNeedsPolling(DriverName& dn) -{ - DriverInfo *di = lookupDriver(dn.str()); - - if (di == NULL) return false; - - // Return true for MBUS,S2,C2,T2 meters. Currently only mbus is implemented. - return di->linkModes().has(LinkMode::MBUS) || - di->linkModes().has(LinkMode::C2) || - di->linkModes().has(LinkMode::T2) || - di->linkModes().has(LinkMode::S2); -} - -const char *toString(MeterType type) -{ -#define X(tname) if (type == MeterType::tname) return #tname; -LIST_OF_METER_TYPES -#undef X - return "unknown"; -} - -MeterType toMeterType(std::string type) -{ -#define X(tname) if (type == #tname) return MeterType::tname; -LIST_OF_METER_TYPES -#undef X - return MeterType::UnknownMeter; -} - -std::string toString(DriverInfo &di) -{ - return di.name().str(); -} - -bool MeterCommonImplementation::isTelegramForMeter(Telegram *t, Meter *meter, MeterInfo *mi) -{ - std::string name; - std::vector address_expressions; - std::string driver_name; - - assert((meter && !mi) || - (!meter && mi)); - - if (meter) - { - name = meter->name(); - address_expressions = meter->addressExpressions(); - driver_name = meter->driverName().str(); - } - else - { - name = mi->name; - address_expressions = mi->address_expressions; - driver_name = mi->driver_name.str(); - } - - // Telegram addresses - std::string t_idsc = Address::concat(t->addresses); - // Meter/MeterInfo address expressions - std::string m_idsc = AddressExpression::concat(address_expressions); - debug("(meter) %s: for me? %s in %s\n", name.c_str(), t_idsc.c_str(), m_idsc.c_str()); - - - bool used_wildcard = false; - bool match = doesTelegramMatchExpressions(t->addresses, - address_expressions, - &used_wildcard); - - if (!match) - { - // The id must match. - debug("(meter) %s: not for me: no match\n", name.c_str()); - return false; - } - - debug("(meter) %s: yes for me\n", name.c_str()); - t->meter = meter; - return true; -} - -MeterKeys *MeterCommonImplementation::meterKeys() -{ - return &meter_keys_; -} - -int MeterCommonImplementation::index() -{ - return index_; -} - -void MeterCommonImplementation::setIndex(int i) -{ - index_ = i; -} - -std::string MeterCommonImplementation::bus() -{ - return bus_; -} - -void MeterCommonImplementation::triggerUpdate(Telegram *t) -{ - // Check if processContent has discarded this telegram. - if (t->discard) return; - - datetime_of_poll_ = time(NULL); - datetime_of_update_ = t->about.timestamp ? t->about.timestamp : datetime_of_poll_; - num_updates_++; - t->handled = true; -} - -std::string findField(std::string key, std::vector *extra_constant_fields) -{ - key = key+"="; - for (std::string ecf : *extra_constant_fields) - { - if (startsWith(ecf, key)) - { - return ecf.substr(key.length()); - } - } - return ""; -} - -std::string build_id(Address &a, IdentityMode im) -{ - std::string id = a.id; - if (im == IdentityMode::ID_MFCT || - im == IdentityMode::FULL) - { - id += std::string(".M=")+manufacturerFlag(a.mfct); - } - if (im == IdentityMode::FULL) - { - id += tostrprintf(".V=%02x.T=%02x", a.version, a.type); - } - return id; -} - -// Is the desired field one of the fields common to all meters and telegrams? -bool checkCommonField(std::string *buf, std::string desired_field, Meter *m, Telegram *t, char c, bool human_readable) -{ - if (desired_field == "name") - { - *buf += m->name() + c; - return true; - } - if (desired_field == "id") - { - std::string id = build_id(t->addresses.back(), m->identityMode()); - *buf += id + c; - return true; - } - if (desired_field == "timestamp") - { - *buf += m->datetimeOfUpdateHumanReadable() + c; - return true; - } - if (desired_field == "timestamp_lt") - { - *buf += m->datetimeOfUpdateHumanReadable() + c; - return true; - } - if (desired_field == "timestamp_utc") - { - *buf += m->datetimeOfUpdateRobot() + c; - return true; - } - if (desired_field == "timestamp_ut") - { - *buf += m->unixTimestampOfUpdate() + c; - return true; - } - if (desired_field == "device") - { - *buf += t->about.device + c; - return true; - } - if (desired_field == "rssi_dbm") - { - *buf += std::to_string(t->about.rssi_dbm) + c; - return true; - } - - return false; -} - -// Is the desired field one of the meter printable fields? -bool checkPrintableField(std::string *buf, std::string desired_field, Meter *m, Telegram *t, char c, - std::vector &fields, bool human_readable) -{ - - for (FieldInfo &fi : fields) - { - if (fi.xuantity() == Quantity::Text) - { - // Strings are simply just print them. - if (desired_field == fi.vname()) - { - *buf += m->getStringValue(&fi) + c; - return true; - } - } - else - { - std::string display_unit_s = unitToStringLowerCase(fi.displayUnit()); - std::string var = fi.vname()+"_"+display_unit_s; - if (desired_field != var) continue; - - // We have the correc field. - if (fi.displayUnit() == Unit::DateLT) - { - double d = m->getNumericValue(&fi, Unit::DateLT); - *buf += strdate(d); - *buf += c; - return true; - } - else if (fi.displayUnit() == Unit::DateTimeLT) - { - double d = m->getNumericValue(&fi, Unit::DateTimeLT); - *buf += strdatetime(d); - *buf += c; - return true; - } - else if (fi.displayUnit() == Unit::DateTimeUTC) - { - double d = m->getNumericValue(&fi, Unit::DateTimeUTC); - *buf += strTimestampUTC(d); - *buf += c; - return true; - } - else - { - // Default unit. - *buf += valueToString(m->getNumericValue(&fi, fi.displayUnit()), fi.displayUnit()); - if (human_readable) - { - *buf += " "; - *buf += unitToStringHR(fi.displayUnit()); - } - *buf += c; - return true; - } - } - } - - return false; -} - -// Is the desired field one of the constant fields? -bool checkConstantField(std::string *buf, std::string field, char c, std::vector *extra_constant_fields) -{ - // Ok, lets look for extra constant fields and print any such static information. - std::string v = findField(field, extra_constant_fields); - if (v != "") - { - *buf += v + c; - return true; - } - - return false; -} - -std::string concatFields(Meter *m, Telegram *t, char c, std::vector &prints, bool human_readable, - std::vector *selected_fields, std::vector *extra_constant_fields) -{ - if (selected_fields == NULL || selected_fields->size() == 0) - { - selected_fields = &m->selectedFields(); - } - - std::string buf = ""; - - for (std::string field : *selected_fields) - { - bool handled = checkCommonField(&buf, field, m, t, c, human_readable); - if (handled) continue; - - handled = checkPrintableField(&buf, field, m, t, c, prints, human_readable); - if (handled) continue; - - handled = checkConstantField(&buf, field, c, extra_constant_fields); - if (handled) continue; - - if (!handled) - { - buf += "?"+field+"?"+c; - } - } - if (buf.back() == c) buf.pop_back(); - return buf; -} - -bool MeterCommonImplementation::handleTelegram(AboutTelegram &about, std::vector input_frame, - bool simulated, std::vector
*addresses, - bool *id_match, Telegram *out_analyzed) -{ - Telegram t; - t.about = about; - bool ok = t.parseHeader(input_frame); - - if (simulated) t.markAsSimulated(); - if (out_analyzed != NULL) t.markAsBeingAnalyzed(); - - *addresses = t.addresses; - - if (!ok || !isTelegramForMeter(&t, this, NULL)) - { - // This telegram is not intended for this meter. - return false; - } - - *id_match = true; - - verbose("(meter) %s(%d) %s handling telegram from %s\n", - name().c_str(), - index(), - driverName().str().c_str(), - t.addresses.back().str().c_str()); - - std::string msg = bin2hex(input_frame); - debug("(meter) %s %s \"%s\"\n", name().c_str(), t.addresses.back().str().c_str(), msg.c_str()); - - // For older meters with manufacturer specific data without a nice 0f dif marker. - if (force_mfct_index_ != -1) - { - t.force_mfct_index = force_mfct_index_; - } - - ok = t.parse(input_frame, &meter_keys_, true); - if (!ok) - { - if (out_analyzed != NULL) *out_analyzed = t; - // Ignoring telegram since it could not be parsed. - return false; - } - - // char log_prefix[256]; - // snprintf(log_prefix, 255, "(%s) log", driverName().str().c_str()); - // logTelegram(t.original, t.frame, t.header_size, t.suffix_size); - - // Invoke standardized field extractors! - processFieldExtractors(&t); - if (hasProcessContent()) - { - // Invoke tailor made meter specific parsing! - processContent(&t); - } - // Invoke any calculators working on the extracted fields. - processFieldCalculators(); - - // All done.... - - // snprintf(log_prefix, 255, "(%s)", driverName().str().c_str()); - // t.explainParse(log_prefix, 0); - - triggerUpdate(&t); - - if (out_analyzed != NULL) *out_analyzed = t; - return true; -} - -void MeterCommonImplementation::processFieldExtractors(Telegram *t) -{ - // Multiple dventries can be matched against a single wildcard FieldInfo. - std::map> founds; - - // Sort the dv_entries based on their offset in the telegram. - // I.e. restore the ordering that was implicit in the telegram. - std::vector sorted_entries; - - for (auto &p : t->dv_entries) - { - sorted_entries.push_back(&p.second.second); - } - sort(sorted_entries.begin(), sorted_entries.end(), - [](const DVEntry* a, const DVEntry *b) -> bool { return a->offset < b->offset; }); - - // Now go through each field_info defined by the driver. - for (FieldInfo &fi : field_infos_) - { - int current_match_nr = 0; - - if (!fi.hasMatcher()) - { - // This field_info has not been matched to a dv_entry before! - debug("(meters) skipping field without matcher %s(%s)[%d]...\n", - fi.vname().c_str(), - toString(fi.xuantity()), - fi.index()); - continue; - } - - debug("(meters) trying field info %s(%s)[%d]...\n", - fi.vname().c_str(), - toString(fi.xuantity()), - fi.index()); - - // Iterate through dv_entries in the telegram in the same order the telegram presented them. - for (DVEntry *dve : sorted_entries) - { - if (fi.hasMatcher() && fi.matches(dve)) - { - current_match_nr++; - if (fi.matcher().index_nr != IndexNr(current_match_nr) && - !fi.matcher().expectedToMatchAgainstMultipleEntries()) - { - // This field info did match, but requires another index nr! - // Increment the current index nr and look for the next match. - } - else if (founds[&fi].count(dve) == 0 || fi.matcher().expectedToMatchAgainstMultipleEntries()) - { - debug("(meters) using field info %s(%s)[%d] to extract %s at offset %d\n", - fi.vname().c_str(), - toString(fi.xuantity()), - fi.index(), - dve->dif_vif_key.str().c_str(), - dve->offset); - - dve->addFieldInfo(&fi); - fi.performExtraction(this, t, dve); - founds[&fi].insert(dve); - } - else - { - std::set old = founds[&fi]; - std::string olds; - for (DVEntry *dve : old) - { - olds += std::to_string(dve->offset)+","; - } - olds.pop_back(); - - verbose("(meter) while processing field extractors ignoring dventry %s at offset %d matching since " - "field %s was already matched against offsets %s !\n", - dve->dif_vif_key.str().c_str(), - dve->offset, - fi.vname().c_str(), - olds.c_str()); - } - } - } - } - - // Iterate over the fields that has no matcher rule. Ie the field - // itself does the searching and matching. - for (FieldInfo &fi : field_infos_) - { - if (!fi.hasMatcher()) - { - fi.performExtraction(this, t, NULL); - } - else if (founds.count(&fi) == 0 && fi.printProperties().hasINCLUDETPLSTATUS()) - { - // This is a status field and it joins the tpl status but it also - // has a potential dve match, which did not trigger. Now - // force extraction to get the tpl status. - fi.performExtraction(this, t, NULL); - } - } -} - -void MeterCommonImplementation::processFieldCalculators() -{ - // Iterate over the fields with formulas but no matcher. - for (FieldInfo &fi : field_infos_) - { - if (fi.hasFormula() && !fi.hasMatcher()) - { - debug("(meters) calculating field %s(%s)[%d]\n", - fi.vname().c_str(), - toString(fi.xuantity()), - fi.index()); - fi.performCalculation(this); - } - } -} - -std::string MeterCommonImplementation::getStatusField(FieldInfo *fi) -{ - std::string field_name_no_unit = fi->vname(); - if (string_values_.count(field_name_no_unit) == 0) - { - return "null"; // This is translated to a real(non-string) null in the json. - } - StringField &sf = string_values_[field_name_no_unit]; - std::string value = sf.value; - - // This is >THE< status field, only one is allowed. - // Look for other fields with the JOIN_INTO_STATUS marker. - // These other fields will not be printed, instead - // joined into this status field. - for (FieldInfo &f : field_infos_) - { - if (f.printProperties().hasINJECTINTOSTATUS()) - { - //printf("NOW >%s<\n", value.c_str()); - std::string more = getStringValue(&f); - //printf("MORE >%s<\n", more.c_str()); - std::string joined = joinStatusOKStrings(value, more); - //printf("JOINED >%s<\n", joined.c_str()); - value = joined; - } - } - // Sort all found flags and remove any duplicates. A well designed meter decoder - // should not be able to generate duplicates. - value = sortStatusString(value); - // If it is empty, then translate to OK! - if (value == "") value = "OK"; - return value; -} - -void MeterCommonImplementation::processContent(Telegram *t) -{ -} - -bool MeterCommonImplementation::hasProcessContent() -{ - return has_process_content_; -} - -void MeterCommonImplementation::setNumericValue(FieldInfo *fi, DVEntry *dve, Unit u, double v) -{ - std::string field_name_no_unit; - - if (dve == NULL) - { - std::string field_name_no_unit = fi->vname(); - numeric_values_[std::pair(field_name_no_unit, fi->displayUnit())] = NumericField(u, v, fi); - } - else - { - field_name_no_unit = fi->generateFieldNameNoUnit(this, dve); - numeric_values_[std::pair(field_name_no_unit, fi->displayUnit())] = NumericField(u, v, fi, *dve); - } -} - -void MeterCommonImplementation::setNumericValue(std::string vname, Unit u, double v) -{ - Quantity q = toQuantity(u); - FieldInfo *fi = findFieldInfo(vname, q); - - if (fi == NULL) - { - warning("(meter) cannot set numeric value %g %s for non-existant field \"%s\" %s\n", v, unitToStringLowerCase(u).c_str(), vname.c_str(), toString(q)); - return; - } - setNumericValue(fi, NULL, u, v); -} - -bool MeterCommonImplementation::hasValue(FieldInfo *fi) -{ - return hasStringValue(fi) || hasNumericValue(fi); -} - -bool MeterCommonImplementation::hasNumericValue(FieldInfo *fi) -{ - std::pair key(fi->vname(),fi->displayUnit()); - - return numeric_values_.count(key) != 0; -} - -bool MeterCommonImplementation::hasStringValue(FieldInfo *fi) -{ - return string_values_.count(fi->vname()) != 0; -} - -double MeterCommonImplementation::getNumericValue(FieldInfo *fi, Unit to) -{ - std::string field_name_no_unit = fi->vname(); - std::pair key(field_name_no_unit,fi->displayUnit()); - if (numeric_values_.count(key) == 0) - { - return std::numeric_limits::quiet_NaN(); // This is translated into a null in the json. - } - NumericField &nf = numeric_values_[key]; - return convert(nf.value, nf.unit, to); -} - -double MeterCommonImplementation::getNumericValue(std::string vname, Unit to) -{ - std::pair key(vname,to); - if (numeric_values_.count(key) == 0) - { - return std::numeric_limits::quiet_NaN(); // This is translated into a null in the json. - } - NumericField &nf = numeric_values_[key]; - return convert(nf.value, nf.unit, to); -} - -void MeterCommonImplementation::setStringValue(FieldInfo *fi, std::string v, DVEntry *dve) -{ - std::string field_name_no_unit; - - if (dve == NULL) - { - std::string field_name_no_unit = fi->vname(); - string_values_[field_name_no_unit] = StringField(v, fi); - } - else - { - field_name_no_unit = fi->generateFieldNameNoUnit(this, dve); - string_values_[field_name_no_unit] = StringField(v, fi); - } -} - -void MeterCommonImplementation::setStringValue(std::string vname, std::string v, DVEntry *dve) -{ - FieldInfo *fi = findFieldInfo(vname, Quantity::Text); - - if (fi == NULL) - { - warning("(meter) cannot set std::string value %s for non-existant field \"%s\"\n", v.c_str(), vname.c_str()); - return; - } - setStringValue(fi, v, dve); -} - -std::string MeterCommonImplementation::getStringValue(FieldInfo *fi) -{ - std::string field_name_no_unit = fi->vname(); - if (string_values_.count(field_name_no_unit) == 0) - { - return "null"; // This is translated to a real(non-string) null in the json. - } - StringField &sf = string_values_[field_name_no_unit]; - std::string value = sf.value; - - if (fi->printProperties().hasSTATUS()) - { - // This is >THE< status field, only one is allowed. - // Look for other fields with the JOIN_INTO_STATUS marker. - // These other fields will not be printed, instead - // joined into this status field. - for (FieldInfo &f : field_infos_) - { - if (f.printProperties().hasINJECTINTOSTATUS()) - { - std::string more = getStringValue(&f); - std::string joined = joinStatusOKStrings(value, more); - value = joined; - } - } - // Sort all found flags and remove any duplicates. A well designed meter decoder - // should not be able to generate duplicates. - value = sortStatusString(value); - // If it is empty, then translate to OK! - if (value == "") value = "OK"; - } - - return value; -} - -std::string MeterCommonImplementation::decodeTPLStatusByte(uchar sts) -{ - return ::decodeTPLStatusByteWithMfct(sts, mfct_tpl_status_bits_); -} - -FieldInfo *MeterCommonImplementation::findFieldInfo(std::string vname, Quantity xuantity) -{ - FieldInfo *found = NULL; - for (FieldInfo &p : field_infos_) - { - if (p.vname() == vname && - p.xuantity() == xuantity) - { - found = &p; - break; - } - } - - return found; -} - -std::string MeterCommonImplementation::renderJsonOnlyDefaultUnit(std::string vname, Quantity xuantity) -{ - FieldInfo *fi = findFieldInfo(vname, xuantity); - - if (fi == NULL) return "unknown field "+vname; - return fi->renderJsonOnlyDefaultUnit(this); -} - -std::string MeterCommonImplementation::debugValues() -{ - std::string s; - - for (auto &p : numeric_values_) - { - std::string vname = p.first.first; - std::string us = unitToStringLowerCase(p.first.second); - NumericField& nf = p.second; - - s += tostrprintf("%s_%s = %g\n", vname.c_str(), us.c_str(), nf.value); - } - - for (auto &p : string_values_) - { - std::string vname = p.first; - StringField& nf = p.second; - - s += tostrprintf("%s = \"%s\"\n", vname.c_str(), nf.value.c_str()); - } - - return s; -} - -FieldInfo::~FieldInfo() -{ -} - -FieldInfo::FieldInfo(int index, - std::string vname, - Quantity xuantity, - Unit display_unit, - VifScaling vif_scaling, - DifSignedness dif_signedness, - double scale, - FieldMatcher matcher, - std::string help, - PrintProperties print_properties, - function get_numeric_value_override, - function get_string_value_override, - function set_numeric_value_override, - function set_string_value_override, - Translate::Lookup lookup, - Formula *formula, - Meter *m - ) : - index_(index), - vname_(vname), - xuantity_(xuantity), - display_unit_(display_unit), - vif_scaling_(vif_scaling), - dif_signedness_(dif_signedness), - scale_(scale), - matcher_(matcher), - help_(help), - print_properties_(print_properties), - get_numeric_value_override_(get_numeric_value_override), - get_string_value_override_(get_string_value_override), - set_numeric_value_override_(set_numeric_value_override), - set_string_value_override_(set_string_value_override), - lookup_(lookup), - formula_(formula), - field_name_(newStringInterpolator()), - valid_field_name_(field_name_->parse(m, vname)) -{ - if (!valid_field_name_) - { - warning("(meter) field template \"%s\" could not be parsed!\n", vname.c_str()); - } -} - -std::string FieldInfo::renderJsonOnlyDefaultUnit(Meter *m) -{ - return renderJson(m, NULL); -} - -std::string FieldInfo::renderJsonText(Meter *m, DVEntry *dve) -{ - return renderJson(m, dve); -} - -std::string FieldInfo::generateFieldNameNoUnit(Meter *m, DVEntry *dve) -{ - if (!valid_field_name_) return "bad_field_name"; - - return field_name_->apply(m, dve); -} - -std::string FieldInfo::generateFieldNameWithUnit(Meter *m, DVEntry *dve) -{ - if (!valid_field_name_) return "bad_field_name"; - - if (xuantity_ == Quantity::Text) - { - return field_name_->apply(m, dve); - } - - std::string display_unit_s = unitToStringLowerCase(displayUnit()); - std::string var = field_name_->apply(m, dve); - - return var+"_"+display_unit_s; -} - - -std::string FieldInfo::renderJson(Meter *m, DVEntry *dve) -{ - std::string s; - - std::string display_unit_s = unitToStringLowerCase(displayUnit()); - std::string field_name = generateFieldNameNoUnit(m, dve); - - if (xuantity() == Quantity::Text) - { - std::string v = m->getStringValue(this); - if (v == "null") - { - // Yes, right now a meter cannot send a std::string value "something":"null" it will - // be translated into "something":null in the json, indicating that there is no value. - // This should not be a problem for now. Lets deal with it when a meter decides to send "null" - // as its version std::string for example. - s += "\""+field_name+"\":null"; - } - else - { - // Normally the std::string values are quoted in json. TODO quote the value properly. - // A well crafted meter could send a version std::string with " and break the json format. - s += "\""+field_name+"\":\""+v+"\""; - } - } - else - { - if (displayUnit() == Unit::DateLT) - { - s += "\""+field_name+"_"+display_unit_s+"\":\""+strdate(m->getNumericValue(field_name, Unit::DateLT))+"\""; - } - else if (displayUnit() == Unit::DateTimeLT) - { - s += "\""+field_name+"_"+display_unit_s+"\":\""+strdatetime(m->getNumericValue(field_name, Unit::DateTimeLT))+"\""; - } - else if (displayUnit() == Unit::DateTimeUTC) - { - s += "\""+field_name+"_"+display_unit_s+"\":\""+strTimestampUTC(m->getNumericValue(field_name, Unit::DateTimeUTC))+"\""; - } - else - { - // All numeric values. - s += "\""+field_name+"_"+display_unit_s+"\":"+valueToString(m->getNumericValue(field_name, displayUnit()), displayUnit()); - } - } - - return s; -} - -void MeterCommonImplementation::createMeterEnv(std::string id, - std::vector *envs, - std::vector *extra_constant_fields) -{ - envs->push_back(std::string("METER_ID="+id)); - envs->push_back(std::string("METER_NAME=")+name()); - envs->push_back(std::string("METER_TYPE=")+driverName().str()); - - // If the configuration has supplied json_address=Roodroad 123 - // then the env variable METER_address will available and have the content "Roodroad 123" - for (std::string add_json : meterExtraConstantFields()) - { - envs->push_back(std::string("METER_")+add_json); - } - for (std::string extra_field : *extra_constant_fields) - { - envs->push_back(std::string("METER_")+extra_field); - } -} - -void MeterCommonImplementation::printMeter(Telegram *t, - std::string *human_readable, - std::string *fields, char separator, - std::string *json, - std::vector *envs, - std::vector *extra_constant_fields, - std::vector *selected_fields, - bool pretty_print_json) -{ - bool first = !t->meter->hasReceivedFirstTelegram(); - - if (human_readable) - *human_readable = concatFields(this, t, '\t', field_infos_, true, selected_fields, extra_constant_fields); - if (fields) - *fields = concatFields(this, t, separator, field_infos_, false, selected_fields, extra_constant_fields); - - std::string media; - if (t->tpl_id_found) - { - media = mediaTypeJSON(t->tpl_type, t->tpl_mfct); - } - else if (t->ell_id_found) - { - media = mediaTypeJSON(t->ell_type, t->ell_mfct); - } - else - { - media = mediaTypeJSON(t->dll_type, t->dll_mfct); - } - - std::string id = ""; - if (t->addresses.size() > 0) - { - id = build_id(t->addresses.back(), identityMode()); - } - - if(json) - { - std::string indent = ""; - std::string newline = ""; - - if (pretty_print_json) - { - indent = " "; - newline ="\n"; - } - - std::string s; - s += "{"+newline; - s += indent+"\"_\":\"telegram\","+newline; - s += indent+"\"media\":\""+media+"\","+newline; - s += indent+"\"meter\":\""+driverName().str()+"\","+newline; - s += indent+"\"name\":\""+name()+"\","+newline; - s += indent+"\"id\":\""+id+"\","+newline; - - // Iterate over the meter field infos... - std::map> founds; // Multiple dventries can match to a single field info. - std::set found_vnames; - - for (auto &p : numeric_values_) - { - std::string vname = p.first.first; - NumericField& nf = p.second; - if (nf.field_info->printProperties().hasHIDE()) continue; - - std::string out = nf.field_info->renderJson(this, &nf.dv_entry); - s += indent+out+","+newline; - - if (first && getDetailedFirst()) - { - size_t pos = out.find("\":"); - if (pos != std::string::npos) - { - std::string rule = out.substr(0, pos)+"_field\":"+std::to_string(nf.field_info->index()); - s += indent+rule+","+newline; - } - } - } - - for (auto &p : string_values_) - { - std::string vname = p.first; - StringField& sf = p.second; - std::string out; - - if (sf.field_info->printProperties().hasHIDE()) continue; - if (sf.field_info->printProperties().hasSTATUS()) - { - std::string in = getStatusField(sf.field_info); - out = tostrprintf("\"%s\":\"%s\"", vname.c_str(), in.c_str()); - s += indent+out+","+newline; - } - else - { - if (sf.value == "null") - { - // The std::string "null" translates to actual json null. - out = tostrprintf("\"%s\":null", vname.c_str()); - s += indent+out+","+newline; - } - else - { - out = tostrprintf("\"%s\":\"%s\"", vname.c_str(), sf.value.c_str()); - s += indent+out+","+newline; - } - } - if (first && getDetailedFirst()) - { - size_t pos = out.find("\":"); - if (pos != std::string::npos) - { - std::string rule = out.substr(0, pos)+"_field\":"+std::to_string(sf.field_info->index()); - s += indent+rule+","+newline; - } - } - } - s += indent+"\"timestamp\":\""+datetimeOfUpdateRobot()+"\""; - - if (t->about.device != "") - { - s += ","+newline; - s += indent+"\"device\":\""+t->about.device+"\","+newline; - s += indent+"\"rssi_dbm\":"+std::to_string(t->about.rssi_dbm); - } - for (std::string extra_field : meterExtraConstantFields()) - { - s += ","+newline; - s += indent+makeQuotedJson(extra_field); - } - if(extra_constant_fields) - for (std::string extra_field : *extra_constant_fields) - { - s += ","+newline; - s += indent+makeQuotedJson(extra_field); - } - s += newline; - s += "}"; - *json = s; - } - - if(envs) - { - createMeterEnv(id, envs, extra_constant_fields); - - envs->push_back(std::string("METER_JSON=")+*json); - envs->push_back(std::string("METER_MEDIA=")+media); - envs->push_back(std::string("METER_TIMESTAMP=")+datetimeOfUpdateRobot()); - envs->push_back(std::string("METER_TIMESTAMP_UTC=")+datetimeOfUpdateRobot()); - envs->push_back(std::string("METER_TIMESTAMP_UT=")+unixTimestampOfUpdate()); - envs->push_back(std::string("METER_TIMESTAMP_LT=")+datetimeOfUpdateHumanReadable()); - - for (FieldInfo& fi : field_infos_) - { - if (fi.printProperties().hasHIDE()) continue; - - std::string display_unit_s = unitToStringUpperCase(fi.displayUnit()); - std::string var = fi.vname(); - std::transform(var.begin(), var.end(), var.begin(), ::toupper); - if (fi.xuantity() == Quantity::Text) - { - std::string envvar = "METER_"+var+"="+getStringValue(&fi); - envs->push_back(envvar); - } - else - { - std::string envvar = "METER_"+var+"_"+display_unit_s+"="+valueToString(getNumericValue(&fi, fi.displayUnit()), fi.displayUnit()); - envs->push_back(envvar); - } - } - - if (t->about.device != "") - { - envs->push_back(std::string("METER_DEVICE=")+t->about.device); - envs->push_back(std::string("METER_RSSI_DBM=")+std::to_string(t->about.rssi_dbm)); - } - } -} - -void MeterCommonImplementation::setExpectedTPLSecurityMode(TPLSecurityMode tsm) -{ - expected_tpl_sec_mode_ = tsm; -} - -void MeterCommonImplementation::setExpectedELLSecurityMode(ELLSecurityMode dsm) -{ - expected_ell_sec_mode_ = dsm; -} - -TPLSecurityMode MeterCommonImplementation::expectedTPLSecurityMode() -{ - return expected_tpl_sec_mode_; -} - -ELLSecurityMode MeterCommonImplementation::expectedELLSecurityMode() -{ - return expected_ell_sec_mode_; -} - -void detectMeterDrivers(int manufacturer, int version, int type, std::vector *drivers) -{ - for (DriverInfo *p : allDrivers()) - { - if (p->detect(manufacturer, version, type)) - { - drivers->push_back(p->name().str()); - } - } -} - -bool isMeterDriverReasonableForMedia(std::string driver_name, int media) -{ - if (media == 0x37) return false; // Skip converter meter side since they do not give any useful information. - - for (DriverInfo *p : allDrivers()) - { - if (p->name().str() == driver_name && p->isValidMedia(media)) - { - return true; - } - } - - return false; -} - -DriverInfo driver_unknown_; - -DriverInfo pickMeterDriver(Telegram *t) -{ - int manufacturer = t->dll_mfct; - int version = t->dll_version; - int type = t->dll_type; - - if (t->tpl_id_found) - { - manufacturer = t->tpl_mfct; - version = t->tpl_version; - type = t->tpl_type; - } - - for (DriverInfo *p : allDrivers()) - { - if (p->detect(manufacturer, version, type)) - { - return *p; - } - } - - return driver_unknown_; -} - -shared_ptr createMeter(MeterInfo *mi) -{ - const char *keymsg = (mi->key[0] == 0) ? "not-encrypted" : "encrypted"; - - DriverInfo *di = lookupDriver(mi->driver_name.str()); - - assert(di != NULL); - - shared_ptr newm = di->construct(*mi); - for (string &j : mi->extra_calculated_fields) - { - newm->addExtraCalculatedField(j); - } - newm->setPollInterval(mi->poll_interval); - if (mi->selected_fields.size() > 0) - { - newm->setSelectedFields(mi->selected_fields); - } - else - { - newm->setSelectedFields(di->defaultFields()); - } - - string aesc = AddressExpression::concat(mi->address_expressions); - verbose("(meter) created %s %s %s %s\n", - mi->name.c_str(), - di->name().str().c_str(), - aesc.c_str(), - keymsg); - - return newm; -} - -bool is_driver_and_extras(const std::string& t, DriverName *out_driver_name, std::string *out_extras) -{ - // piigth(jump=foo) - // multical21 - DriverInfo di; - size_t ps = t.find('('); - size_t pe = t.find(')'); - - size_t te = 0; // Position after type end. - - bool found_parentheses = (ps != std::string::npos && pe != std::string::npos); - - if (!found_parentheses) - { - if (lookupDriverInfo(t, &di)) - { - *out_driver_name = di.name(); - // We found a registered driver. - *out_extras = ""; - return true; - } - else - { - error("No such driver %s %s\n", t.c_str(), removedDriverExplanation(t).c_str()); - } - *out_extras = ""; - return true; - } - - // Parentheses must be last. - if (! (ps > 0 && ps < pe && pe == t.length()-1)) return false; - te = ps; - - std::string type = t.substr(0, te); - - bool found = lookupDriverInfo(type, &di); - - if (found) - { - *out_driver_name = di.name(); - } - else - { - error("No such driver %s %s\n", type.c_str(), removedDriverExplanation(type).c_str()); - } - - std::string extras = t.substr(ps+1, pe-ps-1); - *out_extras = extras; - - return true; -} - -std::string MeterInfo::str() -{ - std::string r; - r += driver_name.str(); - if (extras != "") - { - r += "("+extras+")"; - } - r += ":"; - if (bus != "") r += bus+":"; - if (bps != 0) r += std::to_string(bps)+":"; - if (!link_modes.empty()) r += link_modes.hr()+":"; - if (r.size() > 0) r.pop_back(); - - return r; -} - -bool MeterInfo::parse(std::string n, std::string d, std::string aes, std::string k) -{ - clear(); - - name = n; - address_expressions = splitAddressExpressions(aes); - key = k; - bool driverextras_checked = false; - bool bus_checked = false; - bool bps_checked = false; - bool link_modes_checked = false; - - // The : colon is forbidden inside the parts. - std::vector parts = splitString(d, ':'); - - // Example piigth:MAIN:2400 // it is an mbus meter. - // c5isf:MAIN:2400:mbus // attached to mbus instead of t1 - // multical21:c1 - // telco:BUS2:c2 - // @iperl:t1 - // driver ( extras ) : bus_alias : bps : linkmodes - - for (auto& p : parts) - { - if (!driverextras_checked && is_driver_and_extras(p, &driver_name, &extras)) - { - driverextras_checked = true; - } - else if (!link_modes_checked && isValidLinkModes(p)) - { - driverextras_checked = true; - bus_checked = true; - bps_checked = true; - link_modes_checked = true; - link_modes = parseLinkModes(p); - } - else - { - // Unknown part.... - return false; - } - } - - if (!link_modes_checked) - { - // No explicit link mode set, set to the default link modes - // that the meter can transmit on. - // link_modes = toMeterLinkModeSet(driver); - } - - return true; -} - -bool MeterInfo::usesPolling() -{ - return link_modes.has(LinkMode::MBUS) || - link_modes.has(LinkMode::C2) || - link_modes.has(LinkMode::T2) || - link_modes.has(LinkMode::S2); -} - -bool isValidKey(const std::string& key, MeterInfo &mi) -{ - if (key.length() == 0) return true; - if (key == "NOKEY") { - return true; - } - if (mi.driver_name.str() == "izar" || - mi.driver_name.str() == "hydrus") - { - // These meters can either be OMS compatible 128 bit key (32 hex). - // Or using an older proprietary encryption with 64 bit keys (16 hex) - if (key.length() != 16 && key.length() != 32) return false; - } - else - { - // OMS compliant meters have 128 bit AES keys (32 hex). - // There is a deprecated DES mode, but I have not yet - // seen any telegram using that mode. - if (key.length() != 32) return false; - } - std::vector tmp; - return hex2bin(key, &tmp); -} - -void FieldInfo::performExtraction(Meter *m, Telegram *t, DVEntry *dve) -{ - if (xuantity_ == Quantity::Text) - { - // Extract a std::string. - extractString(m, t, dve); - } - else if (hasFormula()) - { - double value = formula_->calculate(displayUnit(), dve, m); - m->setNumericValue(this, dve, displayUnit(), value); - } - else - { - // Extract a numeric. - extractNumeric(m, t, dve); - } -} - -void FieldInfo::performCalculation(Meter *m) -{ - assert(hasFormula()); - - double value = formula_->calculate(displayUnit()); - m->setNumericValue(this, NULL, displayUnit(), value); -} - -bool FieldInfo::hasMatcher() -{ - return matcher_.active == true; -} - -bool FieldInfo::hasFormula() -{ - return formula_ != NULL; -} - -bool FieldInfo::matches(DVEntry *dve) -{ - return matcher_.matches(*dve); -} - -std::string FieldInfo::str() -{ - return tostrprintf("%d %s_%s (%s) %s [%s] \"%s\"", - index_, - vname_.c_str(), - unitToStringLowerCase(display_unit_).c_str(), - toString(xuantity_), - toString(vif_scaling_), - matcher_.str().c_str(), - help_.c_str()); -} - -DriverName MeterInfo::driverName() -{ - return driver_name; -} - -bool FieldInfo::extractNumeric(Meter *m, Telegram *t, DVEntry *dve) -{ - bool found = false; - std::string key = matcher_.dif_vif_key.str(); - - if (dve == NULL) - { - if (key == "") - { - // Search for key. - bool ok = findKeyWithNr(matcher_.measurement_type, - matcher_.vif_range, - matcher_.storage_nr_from.intValue(), - matcher_.tariff_nr_from.intValue(), - matcher_.index_nr.intValue(), - &key, - &t->dv_entries); - // No entry was found. - if (!ok) return false; - } - // No entry with this key was found. - if (t->dv_entries.count(key) == 0) return false; - dve = &t->dv_entries[key].second; - } - assert(dve != NULL); - assert(key == "" || dve->dif_vif_key.str() == key); - - std::string field_name = generateFieldNameWithUnit(m, dve); - - double extracted_double_value = NAN; - - bool auto_vif_scaling = vifScaling() == VifScaling::Auto; - bool force_unsigned = difSignedness() == DifSignedness::Unsigned; - - if (dve->extractDouble(&extracted_double_value, auto_vif_scaling, force_unsigned)) - { - Unit decoded_unit = displayUnit(); - if (matcher_.vif_range == VIFRange::DateTime) - { - struct tm datetime; - dve->extractDate(&datetime); - time_t tmp = mktime(&datetime); - std::string bbb = strdatetime(tmp); - extracted_double_value = tmp; - } - else if (matcher_.vif_range == VIFRange::Date) - { - struct tm date; - dve->extractDate(&date); - time_t tmp = mktime(&date); - extracted_double_value = tmp; - } - else if (matcher_.vif_range == VIFRange::AnyEnergyVIF || - matcher_.vif_range == VIFRange::AnyVolumeVIF || - matcher_.vif_range == VIFRange::AnyPowerVIF) - { - // Find the actual unit used in the telegram. - decoded_unit = toDefaultUnit(dve->vif); - } - else if (matcher_.vif_range != VIFRange::Any && - matcher_.vif_range != VIFRange::None) - { - // Pick the default unit for this range. - decoded_unit = toDefaultUnit(matcher_.vif_range); - } - - debug("(meter) %s %s decoded %s default %s value %g (scale %g)\n", - toString(matcher_.vif_range), - field_name.c_str(), - unitToStringLowerCase(decoded_unit).c_str(), - unitToStringLowerCase(display_unit_).c_str(), - extracted_double_value, - scale()); - - if (scale() != 1.0) - { - // Hardcoded scale factor for this field used for manufacturer specific values without vif units. - extracted_double_value *= scale(); - } - if (overrideConversion(decoded_unit, display_unit_)) - { - // Special case! Transform the decoded unit into the display unit. I.e. kwh was replaced with kvarh. - decoded_unit = display_unit_; - } - m->setNumericValue(this, dve, display_unit_, convert(extracted_double_value, decoded_unit, display_unit_)); - // t->addMoreExplanation(dve->offset, renderJson(m, dve)); - found = true; - } - return found; -} - -static std::string add_tpl_status(std::string existing_status, Meter *m, Telegram *t) -{ - std::string status = m->decodeTPLStatusByte(t->tpl_sts); - // t->addMoreExplanation(t->tpl_sts_offset, "(%s)", status.c_str()); - if (status != "OK") - { - if (existing_status != "OK") - { - // Join the statuses. - if (existing_status != "") - { - existing_status += " "; - } - existing_status += status; - } - else - { - // Overwrite OK. - existing_status = status; - } - } - else - { - // No change to the existing_status - } - - return existing_status; -} - -bool FieldInfo::extractString(Meter *m, Telegram *t, DVEntry *dve) -{ - bool found = false; - std::string key = matcher_.dif_vif_key.str(); - - if (dve == NULL) - { - if (key == "") - { - if (!hasMatcher()) - { - // There is no matcher, only use case is to capture JOIN_TPL_STATUS. - if (print_properties_.hasINCLUDETPLSTATUS()) - { - std::string status = add_tpl_status("OK", m, t); - m->setStringValue(this, status, dve); - return true; - } - } - else - { - // Search for key. - bool ok = findKeyWithNr(matcher_.measurement_type, - matcher_.vif_range, - matcher_.storage_nr_from.intValue(), - matcher_.tariff_nr_from.intValue(), - matcher_.index_nr.intValue(), - &key, - &t->dv_entries); - // No entry was found. - if (!ok) { - // Nothing found, however check if capturing JOIN_TPL_STATUS. - if (print_properties_.hasINCLUDETPLSTATUS()) - { - std::string status = add_tpl_status("OK", m, t); - m->setStringValue(this, status, dve); - return true; - } - return false; - } - } - } - // No entry with this key was found. - if (t->dv_entries.count(key) == 0) - { - // Nothing found, however check if capturing JOIN_TPL_STATUS. - if (print_properties_.hasINCLUDETPLSTATUS()) - { - std::string status = add_tpl_status("OK", m, t); - m->setStringValue(this, status, dve); - return true; - } - return false; - } - dve = &t->dv_entries[key].second; - } - assert(dve != NULL); - assert(key == "" || dve->dif_vif_key.str() == key); - - // Generate the json field name: - std::string field_name = generateFieldNameNoUnit(m, dve); - - uint64_t extracted_bits {}; - if (lookup_.hasLookups() || (print_properties_.hasINCLUDETPLSTATUS())) - { - std::string translated_bits = ""; - // The field has lookups, or the print property JOIN_TPL_STATUS is set, - // this means that we should create a std::string. - if (lookup_.hasLookups() && dve->extractLong(&extracted_bits)) - { - translated_bits = lookup().translate(extracted_bits); - found = true; - } - - if (print_properties_.hasINCLUDETPLSTATUS()) - { - translated_bits = add_tpl_status(translated_bits, m, t); - } - - if (found) - { - m->setStringValue(this, translated_bits, dve); - // t->addMoreExplanation(dve->offset, renderJsonText(m, dve)); - } - } - else if (matcher_.vif_range == VIFRange::DateTime) - { - struct tm datetime; - dve->extractDate(&datetime); - std::string extracted_device_date_time; - - if (dve->value.size() == 12) - { - // A long date time sec + timezone field. TODO add timezone data. - extracted_device_date_time = strdatetimesec(&datetime); - } - else - { - extracted_device_date_time = strdatetime(&datetime); - } - m->setStringValue(this, extracted_device_date_time, dve); - // t->addMoreExplanation(dve->offset, renderJsonText(m, dve)); - found = true; - } - else if (matcher_.vif_range == VIFRange::Date) - { - struct tm date; - dve->extractDate(&date); - std::string extracted_device_date = strdate(&date); - m->setStringValue(this, extracted_device_date, dve); - // t->addMoreExplanation(dve->offset, renderJsonText(m, dve)); - found = true; - } - else if (matcher_.vif_range == VIFRange::Any || - matcher_.vif_range == VIFRange::EnhancedIdentification || - matcher_.vif_range == VIFRange::FabricationNo || - matcher_.vif_range == VIFRange::HardwareVersion || - matcher_.vif_range == VIFRange::FirmwareVersion || - matcher_.vif_range == VIFRange::Medium || - matcher_.vif_range == VIFRange::Manufacturer || - matcher_.vif_range == VIFRange::ModelVersion || - matcher_.vif_range == VIFRange::SoftwareVersion || - matcher_.vif_range == VIFRange::Customer || - matcher_.vif_range == VIFRange::Location || - matcher_.vif_range == VIFRange::SpecialSupplierInformation || - matcher_.vif_range == VIFRange::ParameterSet) - { - std::string extracted_id; - dve->extractReadableString(&extracted_id); - m->setStringValue(this, extracted_id, dve); - // t->addMoreExplanation(dve->offset, renderJsonText(m, dve)); - found = true; - } - else - { - error("Internal error: Cannot extract text std::string from vif %s in %s:%d\n", - toString(matcher_.vif_range), - __FILE__, __LINE__); - - } - return found; -} - -bool checkIf(std::set &fields, const char *s) -{ - if (fields.count(s) > 0) - { - fields.erase(s); - return true; - } - - return false; -} - -bool checkFieldsEmpty(std::set &fields, std::string driver_name) -{ - if (fields.size() > 0) - { - std::string info; - for (auto &s : fields) { info += s+" "; } - - warning("(meter) when adding common fields to driver %s, these fields were not found: %s\n", - driver_name.c_str(), - info.c_str()); - return false; - } - return true; -} - -bool MeterCommonImplementation::addOptionalLibraryFields(std::string field_names) -{ - // Old C++ driver passes fields like: "target_date,target_volume_m3" - // New xmq driver passes single field like: "target_date|Special help for target date." - // or just: "target_date" - - std::set fields = splitStringIntoSet(field_names, ','); - std::string help; - std::vector helps = splitString(field_names, '|'); - if (helps.size() == 2) - { - fields.clear(); - fields.insert(helps[0]); - help = " "+helps[1]; - } - if (helps.size() > 2) - { - error("Bad library field, only zero or one pipe | symbol is allowed: %s", field_names.c_str()); - } - - if (checkIf(fields,"status-tpl-only")) - { - addStringField( - "status", - "Status and error flags."+help, - STATUS | INCLUDE_TPL_STATUS); - markLastFieldAsLibrary(); - } - - if (checkIf(fields, "actuality_duration_s")) - { - addNumericFieldWithExtractor( - "actuality_duration", - "Lapsed time between measurement and transmission."+help, - DEFAULT_PRINT_PROPERTIES, - Quantity::Time, - VifScaling::Auto, - DifSignedness::Signed, - FieldMatcher::build() - .set(MeasurementType::Instantaneous) - .set(VIFRange::ActualityDuration), - Unit::Second - ); - markLastFieldAsLibrary(); - } - - if (checkIf(fields, "actuality_duration_h")) - { - addNumericFieldWithExtractor( - "actuality_duration", - "Lapsed time between measurement and transmission."+help, - DEFAULT_PRINT_PROPERTIES, - Quantity::Time, - VifScaling::Auto, - DifSignedness::Signed, - FieldMatcher::build() - .set(MeasurementType::Instantaneous) - .set(VIFRange::ActualityDuration) - ); - markLastFieldAsLibrary(); - } - - if (checkIf(fields, "fabrication_no")) - { - addStringFieldWithExtractor( - "fabrication_no", - "Fabrication number."+help, - DEFAULT_PRINT_PROPERTIES, - FieldMatcher::build() - .set(MeasurementType::Instantaneous) - .set(VIFRange::FabricationNo) - ); - markLastFieldAsLibrary(); - } - - if (checkIf(fields,"enhanced_id")) - { - addStringFieldWithExtractor( - "enhanced_id", - "Enhanced identification number."+help, - DEFAULT_PRINT_PROPERTIES, - FieldMatcher::build() - .set(MeasurementType::Instantaneous) - .set(VIFRange::EnhancedIdentification) - ); - markLastFieldAsLibrary(); - } - - if (checkIf(fields,"software_version")) - { - addStringFieldWithExtractor( - "software_version", - "Software version."+help, - DEFAULT_PRINT_PROPERTIES, - FieldMatcher::build() - .set(MeasurementType::Instantaneous) - .set(VIFRange::SoftwareVersion) - ); - markLastFieldAsLibrary(); - } - - if (checkIf(fields,"manufacturer")) - { - addStringFieldWithExtractor( - "manufacturer", - "Meter manufacturer."+help, - DEFAULT_PRINT_PROPERTIES, - FieldMatcher::build() - .set(MeasurementType::Instantaneous) - .set(VIFRange::Manufacturer) - ); - markLastFieldAsLibrary(); - } - - if (checkIf(fields,"model_version")) - { - addStringFieldWithExtractor( - "model_version", - "Meter model version."+help, - DEFAULT_PRINT_PROPERTIES, - FieldMatcher::build() - .set(MeasurementType::Instantaneous) - .set(VIFRange::ModelVersion) - ); - markLastFieldAsLibrary(); - } - - if (checkIf(fields,"firmware_version")) - { - addStringFieldWithExtractor( - "firmware_version", - "Meter firmware version."+help, - DEFAULT_PRINT_PROPERTIES, - FieldMatcher::build() - .set(MeasurementType::Instantaneous) - .set(VIFRange::FirmwareVersion) - ); - markLastFieldAsLibrary(); - } - - if (checkIf(fields,"parameter_set")) - { - addStringFieldWithExtractor( - "parameter_set", - "Parameter set for this meter."+help, - DEFAULT_PRINT_PROPERTIES, - FieldMatcher::build() - .set(MeasurementType::Instantaneous) - .set(VIFRange::ParameterSet) - ); - markLastFieldAsLibrary(); - } - - if (checkIf(fields,"customer")) - { - addStringFieldWithExtractor( - "customer", - "Customer name."+help, - DEFAULT_PRINT_PROPERTIES, - FieldMatcher::build() - .set(MeasurementType::Instantaneous) - .set(VIFRange::Customer) - ); - markLastFieldAsLibrary(); - } - - if (checkIf(fields,"location")) - { - addStringFieldWithExtractor( - "location", - "Meter installed at this customer location."+help, - DEFAULT_PRINT_PROPERTIES, - FieldMatcher::build() - .set(MeasurementType::Instantaneous) - .set(VIFRange::Location) - ); - markLastFieldAsLibrary(); - } - - if (checkIf(fields,"operating_time_h")) - { - addNumericFieldWithExtractor( - "operating_time", - "How long the meter has been collecting data."+help, - DEFAULT_PRINT_PROPERTIES, - Quantity::Time, - VifScaling::Auto, - DifSignedness::Signed, - FieldMatcher::build() - .set(MeasurementType::Instantaneous) - .set(VIFRange::OperatingTime) - ); - markLastFieldAsLibrary(); - } - - if (checkIf(fields,"on_time_h")) - { - addNumericFieldWithExtractor( - "on_time", - "How long the meter has been powered up."+help, - DEFAULT_PRINT_PROPERTIES, - Quantity::Time, - VifScaling::Auto, - DifSignedness::Signed, - FieldMatcher::build() - .set(MeasurementType::Instantaneous) - .set(VIFRange::OnTime) - ); - markLastFieldAsLibrary(); - } - - if (checkIf(fields,"on_time_at_error_h")) - { - addNumericFieldWithExtractor( - "on_time_at_error", - "How long the meter has been in an error state while powered up."+help, - DEFAULT_PRINT_PROPERTIES, - Quantity::Time, - VifScaling::Auto, - DifSignedness::Signed, - FieldMatcher::build() - .set(MeasurementType::AtError) - .set(VIFRange::OnTime) - ); - markLastFieldAsLibrary(); - } - - if (checkIf(fields,"meter_date")) - { - addStringFieldWithExtractor( - "meter_date", - "Date when the meter sent the telegram."+help, - DEFAULT_PRINT_PROPERTIES, - FieldMatcher::build() - .set(MeasurementType::Instantaneous) - .set(VIFRange::Date) - ); - markLastFieldAsLibrary(); - } - - if (checkIf(fields,"meter_date_at_error")) - { - addStringFieldWithExtractor( - "meter_date_at_error", - "Date when the meter was in error."+help, - DEFAULT_PRINT_PROPERTIES, - FieldMatcher::build() - .set(MeasurementType::AtError) - .set(VIFRange::Date) - ); - markLastFieldAsLibrary(); - } - - if (checkIf(fields,"meter_datetime")) - { - addStringFieldWithExtractor( - "meter_datetime", - "Date and time when the meter sent the telegram."+help, - DEFAULT_PRINT_PROPERTIES, - FieldMatcher::build() - .set(MeasurementType::Instantaneous) - .set(VIFRange::DateTime) - ); - markLastFieldAsLibrary(); - } - - if (checkIf(fields,"meter_datetime_at_error")) - { - addStringFieldWithExtractor( - "meter_datetime_at_error", - "Date and time when the meter was in error."+help, - DEFAULT_PRINT_PROPERTIES, - FieldMatcher::build() - .set(MeasurementType::AtError) - .set(VIFRange::DateTime) - ); - markLastFieldAsLibrary(); - } - - if (checkIf(fields,"total_m3")) - { - addNumericFieldWithExtractor( - "total", - "The total media volume consumption recorded by this meter."+help, - DEFAULT_PRINT_PROPERTIES, - Quantity::Volume, - VifScaling::Auto, - DifSignedness::Signed, - FieldMatcher::build() - .set(MeasurementType::Instantaneous) - .set(VIFRange::Volume) - ); - markLastFieldAsLibrary(); - } - - if (checkIf(fields,"target_m3")) - { - addNumericFieldWithExtractor( - "target", - "The volume recorded by this meter at the target date."+help, - DEFAULT_PRINT_PROPERTIES, - Quantity::Volume, - VifScaling::Auto, - DifSignedness::Signed, - FieldMatcher::build() - .set(MeasurementType::Instantaneous) - .set(VIFRange::Volume) - .set(StorageNr(1)) - ); - markLastFieldAsLibrary(); - } - - if (checkIf(fields,"target_date")) - { - addNumericFieldWithExtractor( - "target", - "The target date. Usually the end of the previous billing period."+help, - DEFAULT_PRINT_PROPERTIES, - Quantity::PointInTime, - VifScaling::Auto, - DifSignedness::Signed, - FieldMatcher::build() - .set(MeasurementType::Instantaneous) - .set(VIFRange::Date) - .set(StorageNr(1)), - Unit::DateLT - ); - markLastFieldAsLibrary(); - } - - if (checkIf(fields,"total_forward_m3")) - { - addNumericFieldWithExtractor( - "total_forward", - "The total media volume flowing forward."+help, - DEFAULT_PRINT_PROPERTIES, - Quantity::Volume, - VifScaling::Auto, - DifSignedness::Signed, - FieldMatcher::build() - .set(MeasurementType::Instantaneous) - .set(VIFRange::Volume) - .add(VIFCombinable::ForwardFlow) - ); - markLastFieldAsLibrary(); - } - - if (checkIf(fields,"total_backward_m3")) - { - addNumericFieldWithExtractor( - "total_backward", - "The total media volume flowing backward."+help, - DEFAULT_PRINT_PROPERTIES, - Quantity::Volume, - VifScaling::Auto, - DifSignedness::Signed, - FieldMatcher::build() - .set(MeasurementType::Instantaneous) - .set(VIFRange::Volume) - .add(VIFCombinable::BackwardFlow) - ); - markLastFieldAsLibrary(); - } - - if (checkIf(fields,"flow_temperature_c")) - { - addNumericFieldWithExtractor( - "flow_temperature", - "Forward media temperature."+help, - DEFAULT_PRINT_PROPERTIES, - Quantity::Temperature, - VifScaling::Auto, - DifSignedness::Signed, - FieldMatcher::build() - .set(MeasurementType::Instantaneous) - .set(VIFRange::FlowTemperature) - ); - markLastFieldAsLibrary(); - } - - if (checkIf(fields,"external_temperature_c")) - { - addNumericFieldWithExtractor( - "external_temperature", - "Temperature outside of meter."+help, - DEFAULT_PRINT_PROPERTIES, - Quantity::Temperature, - VifScaling::Auto, - DifSignedness::Signed, - FieldMatcher::build() - .set(MeasurementType::Instantaneous) - .set(VIFRange::ExternalTemperature) - ); - markLastFieldAsLibrary(); - } - - if (checkIf(fields,"return_temperature_c")) - { - addNumericFieldWithExtractor( - "return_temperature", - "Return media temperature.", - DEFAULT_PRINT_PROPERTIES, - Quantity::Temperature, - VifScaling::Auto, - DifSignedness::Signed, - FieldMatcher::build() - .set(MeasurementType::Instantaneous) - .set(VIFRange::ReturnTemperature) - ); - markLastFieldAsLibrary(); - } - - if (checkIf(fields,"flow_return_temperature_difference_c")) - { - addNumericFieldWithExtractor( - "flow_return_temperature_difference", - "The difference between flow and return media temperatures."+help, - DEFAULT_PRINT_PROPERTIES, - Quantity::Temperature, - VifScaling::Auto, - DifSignedness::Signed, - FieldMatcher::build() - .set(MeasurementType::Instantaneous) - .set(VIFRange::TemperatureDifference) - ); - markLastFieldAsLibrary(); - } - - if (checkIf(fields,"volume_flow_m3h")) - { - addNumericFieldWithExtractor( - "volume_flow", - "Media volume flow."+help, - DEFAULT_PRINT_PROPERTIES, - Quantity::Flow, - VifScaling::Auto, - DifSignedness::Signed, - FieldMatcher::build() - .set(MeasurementType::Instantaneous) - .set(VIFRange::VolumeFlow) - ); - markLastFieldAsLibrary(); - } - - if (checkIf(fields,"access_counter")) - { - addNumericFieldWithExtractor( - "access", - "Meter access counter."+help, - DEFAULT_PRINT_PROPERTIES, - Quantity::Dimensionless, - VifScaling::None, - DifSignedness::Unsigned, - FieldMatcher::build() - .set(MeasurementType::Instantaneous) - .set(VIFRange::AccessNumber) - ); - markLastFieldAsLibrary(); - } - - if (checkIf(fields,"consumption_hca")) - { - addNumericFieldWithExtractor( - "consumption", - "The current heat cost allocation for this meter."+help, - DEFAULT_PRINT_PROPERTIES, - Quantity::HCA, - VifScaling::Auto, - DifSignedness::Signed, - FieldMatcher::build() - .set(MeasurementType::Instantaneous) - .set(VIFRange::HeatCostAllocation) - ); - markLastFieldAsLibrary(); - } - - if (checkIf(fields,"target_hca")) - { - addNumericFieldWithExtractor( - "target", - "The heat cost allocation recorded by this meter at the target date."+help, - DEFAULT_PRINT_PROPERTIES, - Quantity::HCA, - VifScaling::Auto, - DifSignedness::Signed, - FieldMatcher::build() - .set(MeasurementType::Instantaneous) - .set(VIFRange::HeatCostAllocation) - .set(StorageNr(1)) - ); - markLastFieldAsLibrary(); - } - - if (!checkFieldsEmpty(fields, name())) - { - return false; - } - return true; -} - -const char *toString(VifScaling s) -{ - switch (s) - { - case VifScaling::Auto: return "Auto"; - case VifScaling::None: return "None"; - case VifScaling::Unknown: return "Unknown"; - } - return "?"; -} - -VifScaling toVifScaling(const char *s) -{ - if (!s) return VifScaling::Unknown; - if (!strcmp(s, "Auto")) return VifScaling::Auto; - if (!strcmp(s, "None")) return VifScaling::None; - if (!strcmp(s, "Unknown")) return VifScaling::Unknown; - return VifScaling::Unknown; -} - -const char *toString(DifSignedness s) -{ - switch (s) - { - case DifSignedness::Signed: return "Signed"; - case DifSignedness::Unsigned: return "Unsigned"; - case DifSignedness::Unknown: return "Unknown"; - } - return "?"; -} - -DifSignedness toDifSignedness(const char *s) -{ - if (!s) return DifSignedness::Unknown; - if (!strcmp(s, "Signed")) return DifSignedness::Signed; - if (!strcmp(s, "Unsigned")) return DifSignedness::Unsigned; - if (!strcmp(s, "Unknown")) return DifSignedness::Unknown; - return DifSignedness::Unknown; -} - -const char* toString(PrintProperty p) -{ - switch(p) - { - case PrintProperty::REQUIRED: return "REQUIRED"; - case PrintProperty::DEPRECATED: return "DEPRECATED"; - case PrintProperty::STATUS: return "STATUS"; - case PrintProperty::INCLUDE_TPL_STATUS: return "INCLUDE_TPL_STATUS"; - case PrintProperty::INJECT_INTO_STATUS: return "INJECT_INTO_STATUS"; - case PrintProperty::HIDE: return "HIDE"; - case PrintProperty::Unknown: return "Unknown"; - } - - return "Unknown"; -} - -PrintProperty toPrintProperty(const char *s) -{ - if (!strcmp(s, "REQUIRED")) return PrintProperty::REQUIRED; - if (!strcmp(s, "DEPRECATED")) return PrintProperty::DEPRECATED; - if (!strcmp(s, "STATUS")) return PrintProperty::STATUS; - if (!strcmp(s, "INCLUDE_TPL_STATUS")) return PrintProperty::INCLUDE_TPL_STATUS; - if (!strcmp(s, "INJECT_INTO_STATUS")) return PrintProperty::INJECT_INTO_STATUS; - if (!strcmp(s, "HIDE")) return PrintProperty::HIDE; - if (!strcmp(s, "Unknown")) return PrintProperty::Unknown; - - return PrintProperty::Unknown; -} - -PrintProperties toPrintProperties(std::string s) -{ - auto fields = splitString(s, ','); - - int bits = 0; - for (auto p : fields) - { - bits |= toPrintProperty(p.c_str()); - } - - return bits; -} - -char available_meter_types_[2048]; - -const char *availableMeterTypes() -{ - if (available_meter_types_[0]) return available_meter_types_; - -#define X(m) if (MeterType::m != MeterType::AutoMeter && MeterType::m != MeterType::UnknownMeter) { \ - strcat(available_meter_types_, #m); strcat(available_meter_types_, "\n"); \ - assert(strlen(available_meter_types_) < 1024); } -LIST_OF_METER_TYPES -#undef X - - // Remove last , - available_meter_types_[strlen(available_meter_types_)-1] = 0; - return available_meter_types_; -} \ No newline at end of file diff --git a/components/wmbus_common/meters.h b/components/wmbus_common/meters.h deleted file mode 100644 index ef7ad52e..00000000 --- a/components/wmbus_common/meters.h +++ /dev/null @@ -1,462 +0,0 @@ -/* - Copyright (C) 2017-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 . -*/ - -#ifndef METER_H_ -#define METER_H_ - -#include"address.h" -#include"dvparser.h" -#include"formula.h" -#include"util.h" -#include"units.h" -#include"translatebits.h" -#include"wmbus.h" - -#include -#include -#include -#include -#include - - -#define LIST_OF_METER_TYPES \ - X(AutoMeter) \ - X(UnknownMeter) \ - X(DoorWindowDetector) \ - X(ElectricityMeter) \ - X(GasMeter) \ - X(HeatCoolingMeter) \ - X(HeatCostAllocationMeter) \ - X(HeatMeter) \ - X(PressureSensor) \ - X(PulseCounter) \ - X(Repeater) \ - X(SmokeDetector) \ - X(TempHygroMeter) \ - X(WaterMeter) \ - -enum class MeterType { -#define X(name) name, -LIST_OF_METER_TYPES -#undef X -}; - -struct DriverName -{ - DriverName() {}; - DriverName(std::string s) : name_(s) {}; - const std::string &str() const { return name_; } - bool operator==(const DriverName &dn) const { return name_ == dn.name_; } - -private: - std::string name_; -}; - -// Return a list of matching drivers, like: multical21 -void detectMeterDrivers(int manufacturer, int media, int version, std::vector *drivers); -// For an unknown telegram, when analyzing check if the media type is reasonable in relation to the driver. -// Ie. do not try to decode a door sensor telegram with a water meter driver. -bool isMeterDriverReasonableForMedia(std::string driver_name, int media); - -struct MeterInfo; -bool isValidKey(const std::string& key, MeterInfo &mt); - -using namespace std; - - -struct MeterInfo -{ - std::string bus; // The bus used to communicate with this meter. A device like /dev/ttyUSB0 or an alias like BUS1. - // A bus can be an mbus or a wmbus dongle. - // The bus can be the empty std::string, which means that it will fallback to the first defined bus. - std::string name; // User specified name of this (group of) meters. - DriverName driver_name; // Will replace MeterDriver. - std::string extras; // Extra driver specific settings. - std::vector address_expressions; // Match expressions for ids. - IdentityMode identity_mode {}; // How to group telegram content into objects with state. Default is by id. - std::string key; // Decryption key. - LinkModeSet link_modes; - int bps {}; // For mbus communication you need to know the baud rate. - std::vector shells; - std::vector new_meter_shells; - std::vector extra_constant_fields; // Additional static fields that are added to each message. - std::vector extra_calculated_fields; // Additional field calculated using formulas. - std::vector selected_fields; // Usually set to the default fields, but can be override in meter config. - - // If this is a meter that needs to be polled. - int poll_interval; // Poll every x seconds. - - MeterInfo() - { - } - - std::string str(); - DriverName driverName(); - - MeterInfo(std::string b, std::string n, std::string e, std::vector aes, std::string k, LinkModeSet lms, int baud, std::vector &s, std::vector &ms, std::vector &j, std::vector &calcfs) - { - bus = b; - name = n; - extras = e, - address_expressions = aes; - key = k; - shells = s; - new_meter_shells = ms; - extra_constant_fields = j; - extra_calculated_fields = calcfs; - link_modes = lms; - bps = baud; - } - - void clear() - { - bus = ""; - name = ""; - address_expressions.clear(); - key = ""; - shells.clear(); - new_meter_shells.clear(); - extra_constant_fields.clear(); - extra_calculated_fields.clear(); - link_modes.clear(); - bps = 0; - } - - bool parse(std::string name, std::string driver, std::string id, std::string key); - bool usesPolling(); -}; - -///////////////////////////////////////////////////////////////////////////////////////////////////////////// -// -// Dynamic loading of drivers based on the driver info. -// -//////////////////////////////////////////////////////////////////////////////////////////////////////////// - -struct MVT -{ - uint16_t mfct; - uchar version; - uchar type; -}; - -struct DriverInfo -{ -private: - - DriverName name_; // auto, unknown, amiplus, lse_07_17, multical21 etc - std::vector name_aliases_; // Secondary names that will map to this driver. - LinkModeSet linkmodes_; // C1, T1, S1 or combinations thereof. - Translate::Lookup mfct_tpl_status_bits_; // Translate any mfct specific bits in tpl status. - MeterType type_; // Water, Electricity etc. - function(MeterInfo&,DriverInfo&di)> constructor_; // Invoke this to create an instance of the driver. - std::vector mvts_; - std::vector default_fields_; - int force_mfct_index_ = -1; // Used for meters not declaring mfct specific data using the dif 0f. - bool has_process_content_ = false; // Mark this driver as having mfct specific decoding. - -public: - ~DriverInfo(); - DriverInfo() {}; - void setName(std::string n) { name_ = n; } - void addNameAlias(std::string n) { name_aliases_.push_back(n); } - void setMeterType(MeterType t) { type_ = t; } - void setAliases(std::string f); - void setDefaultFields(std::string f) { default_fields_ = splitString(f, ','); } - void addLinkMode(LinkMode lm) { linkmodes_.addLinkMode(lm); } - void forceMfctIndex(int i) { force_mfct_index_ = i; } - void setConstructor(function(MeterInfo&,DriverInfo&)> c) { constructor_ = c; } - void addMVT(uint16_t mfct, uchar type, uchar ver) { mvts_.push_back({ mfct, ver, type }); } - void usesProcessContent() { has_process_content_ = true; } - - vector &mvts() { return mvts_; } - - DriverName name() { return name_; } - std::vector& nameAliases() { return name_aliases_; } - bool hasDriverName(DriverName dn) { - if (name_ == dn) return true; - for (auto &i : name_aliases_) if (i == dn) return true; - return false; - } - - MeterType type() { return type_; } - std::vector& defaultFields() { return default_fields_; } - LinkModeSet linkModes() { return linkmodes_; } - Translate::Lookup &mfctTPLStatusBits() { return mfct_tpl_status_bits_; } - shared_ptr construct(MeterInfo& mi) { return constructor_(mi, *this); } - bool detect(uint16_t mfct, uchar version, uchar type); - bool isValidMedia(uchar type); - bool isCloseEnoughMedia(uchar type); - int forceMfctIndex() { return force_mfct_index_; } - bool hasProcessContent() { return has_process_content_; } -}; - -bool staticRegisterDriver(function setup); -// Lookup (and load if necessary) driver from memory or disk. -DriverInfo *lookupDriver(std::string name); -bool lookupDriverInfo(const std::string& driver, DriverInfo *di = NULL); -// Return the best driver match for a telegram. -DriverInfo pickMeterDriver(Telegram *t); -// Return true for mbus and S2/C2/T2 drivers. -bool driverNeedsPolling(DriverName& dn); - -std::string loadDriver(const std::string &file, const char *content); - -std::vector& allDrivers(); -std::string removedDriverExplanation(const std::string &name); - -//////////////////////////////////////////////////////////////////////////////////////////////////////////// - -enum class VifScaling -{ - Auto, // Scale to normalized VIF unit (ie kwh, m3, m3h etc) - None, // No auto scaling. - Unknown -}; - -const char* toString(VifScaling s); -VifScaling toVifScaling(const char *s); - -enum class DifSignedness -{ - Signed, // By default the binary values are interpreted as signed. - Unsigned, // We can override for non-compliant meters. - Unknown -}; - -const char* toString(DifSignedness s); -DifSignedness toDifSignedness(const char *s); - -enum PrintProperty -{ - REQUIRED = 1, // If no data has arrived, then print this field anyway with NaN or null. - DEPRECATED = 2, // This field is about to be removed or changed in a newer driver, which will have a new name. - STATUS = 4, // This is >the< status field and it should read OK of not error flags are set. - INCLUDE_TPL_STATUS = 8, // This text field also includes the tpl status decoding. multiple OK:s collapse to a single OK. - INJECT_INTO_STATUS = 16, // This text field is injected into the already defined status field. multiple OK:s collapse. - HIDE = 32, // This field is only used in calculations, do not print it! - Unknown = 1024 -}; - -int toBit(PrintProperty p); -const char* toString(PrintProperty p); -PrintProperty toPrintProperty(const char *s); - -struct PrintProperties -{ - PrintProperties(int x) : props_(x) {} - - bool hasREQUIRED() { return props_ & PrintProperty::REQUIRED; } - bool hasDEPRECATED() { return props_ & PrintProperty::DEPRECATED; } - bool hasSTATUS() { return props_ & PrintProperty::STATUS; } - bool hasINCLUDETPLSTATUS() { return props_ & PrintProperty::INCLUDE_TPL_STATUS; } - bool hasINJECTINTOSTATUS() { return props_ & PrintProperty::INJECT_INTO_STATUS; } - bool hasHIDE() { return props_ & PrintProperty::HIDE; } - bool hasUnknown() { return props_ & PrintProperty::Unknown; } - -private: - int props_; -}; - -// Parse a std::string like DEPRECATED,HIDE into bits. -PrintProperties toPrintProperties(std::string s); - -#define DEFAULT_PRINT_PROPERTIES 0 - -struct FieldInfo -{ - ~FieldInfo(); - FieldInfo(int index, - std::string vname, - Quantity quantity, - Unit display_unit, - VifScaling vif_scaling, - DifSignedness dif_signedness, - double scale, - FieldMatcher matcher, - std::string help, - PrintProperties print_properties, - function get_numeric_value_override, - function get_string_value_override, - function set_numeric_value_override, - function set_string_value_override, - Translate::Lookup lookup, - Formula *formula, - Meter *m - ); - - int index() { return index_; } - std::string vname() { return vname_; } - Quantity xuantity() { return xuantity_; } - Unit displayUnit() { return display_unit_; } - VifScaling vifScaling() { return vif_scaling_; } - DifSignedness difSignedness() { return dif_signedness_; } - double scale() { return scale_; } - FieldMatcher& matcher() { return matcher_; } - std::string help() { return help_; } - PrintProperties printProperties() { return print_properties_; } - - bool extractNumeric(Meter *m, Telegram *t, DVEntry *dve = NULL); - bool extractString(Meter *m, Telegram *t, DVEntry *dve = NULL); - bool hasMatcher(); - bool hasFormula(); - bool matches(DVEntry *dve); - void performExtraction(Meter *m, Telegram *t, DVEntry *dve); - - void performCalculation(Meter *m); - - std::string renderJsonOnlyDefaultUnit(Meter *m); - std::string renderJson(Meter *m, DVEntry *dve); - std::string renderJsonText(Meter *m, DVEntry *dve); - // Render the field name based on the actual field from the telegram. - // A FieldInfo can be declared to handle any number of storage fields of a certain range. - // The vname is then a pattern total_at_month_{storage_counter} that gets translated into - // total_at_month_2 (for the dventry with storage nr 2.) - std::string generateFieldNameWithUnit(Meter *m, DVEntry *dve); - std::string generateFieldNameNoUnit(Meter *m, DVEntry *dve); - // Check if the meter object stores a value for this field. - bool hasValue(Meter *m); - - Translate::Lookup& lookup() { return lookup_; } - - std::string str(); - - void markAsLibrary() { from_library_ = true; index_ = -1; } - -private: - - int index_; // The field infos for a meter are ordered. - std::string vname_; // Value name, like: total current previous target, ie no unit suffix. - Quantity xuantity_; // Quantity: Energy, Volume - Unit display_unit_; // Selected display unit for above quantity: KWH, M3 - VifScaling vif_scaling_; - DifSignedness dif_signedness_; - double scale_; // A hardcoded scale factor. Used only for manufacturer specific values with unknown units for the vifs. - FieldMatcher matcher_; - std::string help_; // Helpful information on this meters use of this value. - PrintProperties print_properties_; - - // Normally the values are stored inside the meter object using its setNumeric/setString/getNumeric/getString - // But for special fields we can override this default location with these setters/getters. - function get_numeric_value_override_; // Callback to fetch the value from the meter. - function get_string_value_override_; // Callback to fetch the value from the meter. - function set_numeric_value_override_; // Call back to set the value in the c++ object - function set_string_value_override_; // Call back to set the value std::string in the c++ object - - // Lookup bits to std::strings. - Translate::Lookup lookup_; - - // For calculated fields. - shared_ptr formula_; - - // For the generated field name. - shared_ptr field_name_; - - // If the field name template could not be parsed. - bool valid_field_name_ {}; - - // If true then this field was fetched from the library. - bool from_library_ {}; -}; - -struct Meter -{ - // Meters are instantiated on the fly from a template, when a telegram arrives - // and no exact meter exists. Index 1 is the first meter created etc. - virtual int index() = 0; - virtual void setIndex(int i) = 0; - // Use this bus to send messages to the meter. - virtual std::string bus() = 0; - // This meter listens to these address expressions. - virtual std::vector& addressExpressions() = 0; - virtual IdentityMode identityMode() = 0; - // This meter can report these fields, like total_m3, temp_c. - virtual std::vector &fieldInfos() = 0; - virtual std::vector &extraConstantFields() = 0; - // Either the default fields specified in the driver, or override fields in the meter configuration file. - virtual std::vector &selectedFields() = 0; - virtual void setSelectedFields (std::vector &f) = 0; - virtual std::string name() = 0; - virtual DriverName driverName() = 0; - virtual DriverInfo *driverInfo() = 0; - virtual bool hasReceivedFirstTelegram() = 0; - virtual void markFirstTelegramReceived() = 0; - - virtual std::string datetimeOfUpdateHumanReadable() = 0; - virtual std::string datetimeOfUpdateRobot() = 0; - virtual std::string unixTimestampOfUpdate() = 0; - virtual time_t timestampLastUpdate() = 0; - virtual void setPollInterval(time_t interval) = 0; - virtual time_t pollInterval() = 0; - virtual bool usesPolling() = 0; - - virtual void setNumericValue(std::string vname, Unit u, double v) = 0; - virtual void setNumericValue(FieldInfo *fi, DVEntry *dve, Unit u, double v) = 0; - virtual double getNumericValue(std::string vname, Unit u) = 0; - virtual double getNumericValue(FieldInfo *fi, Unit u) = 0; - virtual void setStringValue(FieldInfo *fi, std::string v, DVEntry *dve) = 0; - virtual void setStringValue(std::string vname, std::string v, DVEntry *dve = NULL) = 0; - virtual std::string getStringValue(FieldInfo *fi) = 0; - virtual std::string decodeTPLStatusByte(uchar sts) = 0; - - virtual int numUpdates() = 0; - - virtual void createMeterEnv(std::string id, - std::vector *envs, - std::vector *more_json) = 0; - virtual void printMeter(Telegram *t, - std::string *human_readable, - std::string *fields, char separator, - std::string *json, - std::vector *envs, - std::vector *more_json, - std::vector *selected_fields, - bool pretty_print_json) = 0; - - // The handleTelegram expects an input_frame where the DLL crcs have been removed. - // Returns true of this meter handled this telegram! - // Sets id_match to true, if there was an id match, even though the telegram could not be properly handled. - virtual bool handleTelegram(AboutTelegram &about, std::vector input_frame, - bool simulated, std::vector
*addresses, - bool *id_match, Telegram *out_t = NULL) = 0; - virtual MeterKeys *meterKeys() = 0; - - virtual void addExtraCalculatedField(std::string ecf) = 0; - virtual void addShellMeterAdded(std::string cmdline) = 0; - virtual void addShellMeterUpdated(std::string cmdline) = 0; - virtual std::vector &shellCmdlinesMeterAdded() = 0; - virtual std::vector &shellCmdlinesMeterUpdated() = 0; - - virtual FieldInfo *findFieldInfo(std::string vname, Quantity xuantity) = 0; - virtual std::string renderJsonOnlyDefaultUnit(std::string vname, Quantity xuantity) = 0; - - virtual std::string debugValues() = 0; - - virtual ~Meter() = default; -}; - -const char *toString(MeterType type); -MeterType toMeterType(std::string type); -std::string toString(DriverInfo &driver); -LinkModeSet toMeterLinkModeSet(const std::string& driver); - -struct Configuration; -struct MeterInfo; -shared_ptr createMeter(MeterInfo *mi); - -const char *availableMeterTypes(); - -#endif diff --git a/components/wmbus_common/meters_common_implementation.h b/components/wmbus_common/meters_common_implementation.h deleted file mode 100644 index 3f747c5d..00000000 --- a/components/wmbus_common/meters_common_implementation.h +++ /dev/null @@ -1,261 +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 . -*/ - -#ifndef METERS_COMMON_IMPLEMENTATION_H_ -#define METERS_COMMON_IMPLEMENTATION_H_ - -#include"dvparser.h" -#include"meters.h" -#include"units.h" - -#include -#include - - -// Values in a meter are stored based on vname + Quantity. -// I.e. you can have a total_m3 and a total_kwh even though they share the same "total" vname -// since they have two different quantities (Volume and Energy). -// The field total_l refers to the same field storage in the meter as total_m3. -// If a wacko meter sends different values, one m3 and one l. then you -// have to name the fields using different vnames. -struct NumericField -{ - Unit unit {}; - double value {}; - FieldInfo *field_info {}; - DVEntry dv_entry {}; - - NumericField() {} - NumericField(Unit u, double v, FieldInfo *f) : unit(u), value(v), field_info(f) {} - NumericField(Unit u, double v, FieldInfo *f, DVEntry &dve) : unit(u), value(v), field_info(f), dv_entry(dve) {} -}; - -struct StringField -{ - std::string value; - FieldInfo *field_info {}; - - StringField() {} - StringField(std::string v, FieldInfo *f) : value(v), field_info(f) {} -}; - -struct MeterCommonImplementation : public virtual Meter -{ - int index(); - void setIndex(int i); - std::string bus(); - std::vector& addressExpressions(); - IdentityMode identityMode(); - std::vector &fieldInfos(); - std::vector &extraConstantFields(); - std::string name(); - DriverName driverName(); - DriverInfo *driverInfo(); - bool hasProcessContent(); - - ELLSecurityMode expectedELLSecurityMode(); - TPLSecurityMode expectedTPLSecurityMode(); - - std::string datetimeOfUpdateHumanReadable(); - std::string datetimeOfUpdateRobot(); - std::string unixTimestampOfUpdate(); - time_t timestampLastUpdate(); - void setPollInterval(time_t interval); - time_t pollInterval(); - bool usesPolling(); - void addExtraCalculatedField(std::string ef); - - void onUpdate(function cb); - int numUpdates(); - - static bool isTelegramForMeter(Telegram *t, Meter *meter, MeterInfo *mi); - MeterKeys *meterKeys(); - - MeterCommonImplementation(MeterInfo &mi, DriverInfo &di); - - ~MeterCommonImplementation() = default; - -protected: - - void triggerUpdate(Telegram *t); - void setExpectedELLSecurityMode(ELLSecurityMode dsm); - void setExpectedTPLSecurityMode(TPLSecurityMode tsm); - void addShellMeterAdded(std::string cmdline); - void addShellMeterUpdated(std::string cmdline); - void addExtraConstantField(std::string ecf); - std::vector &shellCmdlinesMeterAdded(); - std::vector &shellCmdlinesMeterUpdated(); - std::vector &meterExtraConstantFields(); - void setMeterType(MeterType mt); - void addLinkMode(LinkMode lm); - void setMfctTPLStatusBits(Translate::Lookup &lookup); - - void markLastFieldAsLibrary(); - - void addNumericFieldWithExtractor( - std::string vname, // Name of value without unit, eg "total" "total_month{storagenr}" - std::string help, // Information about this field. - PrintProperties print_properties, // Should this be printed by default in fields,json and hr. - Quantity vquantity, // Value belongs to this quantity, this quantity determines the default unit. - VifScaling vif_scaling, // How should any Vif value be scaled. - DifSignedness dif_signedness, // Should we override the default signed assumption for binary values? - FieldMatcher matcher, - Unit display_unit = Unit::Unknown, // If specified use this unit for the json field instead instead of the default unit. - double scale = 1.0); // A hard coded extra scale factor. Useful for manufacturer specific values. - - void addNumericFieldWithCalculator( - std::string vname, // Name of value without unit, eg "total" "total_month{storagenr}" - std::string help, // Information about this field. - PrintProperties print_properties, // Should this be printed by default in fields,json and hr. - Quantity vquantity, // Value belongs to this quantity, this quantity determines the default unit. - std::string formula, // The formula can reference the other fields and + them together. - Unit display_unit = Unit::Unknown); // If specified use this unit for the json field instead instead of the default unit. - - void addNumericFieldWithCalculatorAndMatcher( - std::string vname, // Name of value without unit, eg "total" "total_month{storagenr}" - std::string help, // Information about this field. - PrintProperties print_properties, // Should this be printed by default in fields,json and hr. - Quantity vquantity, // Value belongs to this quantity, this quantity determines the default unit. - std::string formula, // The formula can reference the other fields and + them together. - FieldMatcher matcher, // We can generate a calculated field per match. - Unit display_unit = Unit::Unknown); // If specified use this unit for the json field instead instead of the default unit. - - void addNumericField( - std::string vname, // Name of value without unit, eg total - Quantity vquantity, // Value belongs to this quantity. - PrintProperties print_properties, // Should this be printed by default in fields,json and hr. - std::string help, - Unit display_unit = Unit::Unknown); // If specified use this unit for the json field instead instead of the default unit. - - void addStringFieldWithExtractor( - std::string vname, - std::string help, - PrintProperties print_properties, - FieldMatcher matcher); - - void addStringFieldWithExtractorAndLookup( - std::string vname, - std::string help, - PrintProperties print_properties, - FieldMatcher matcher, - Translate::Lookup lookup); // Translate the bits/indexes. - - // Used only for status field from tpl_status only. - void addStringField( - std::string vname, - std::string help, - PrintProperties print_properties); - - bool handleTelegram(AboutTelegram &about, std::vector frame, - bool simulated, std::vector
*addresses, - bool *id_match, Telegram *out_analyzed = NULL); - void createMeterEnv(std::string id, - std::vector *envs, - std::vector *more_json); // Add this json "key"="value" std::strings. - void printMeter(Telegram *t, - std::string *human_readable, - std::string *fields, char separator, - std::string *json, - std::vector *envs, - std::vector *more_json, // Add this json "key"="value" std::strings. - std::vector *selected_fields, // Only print these fields. - bool pretty_print); // Insert newlines and indentation. - // Json fields include all values except timestamp_ut, timestamp_utc, timestamp_lt - // since Json is assumed to be decoded by a program and the current timestamp which is the - // same as timestamp_utc, can always be decoded/recoded into local time or a unix timestamp. - - FieldInfo *findFieldInfo(std::string vname, Quantity xuantity); - std::string renderJsonOnlyDefaultUnit(std::string vname, Quantity xuantity); - std::string debugValues(); - - void processFieldExtractors(Telegram *t); - void processFieldCalculators(); - std::string getStatusField(FieldInfo *fi); - - virtual void processContent(Telegram *t); - - void setNumericValue(std::string vname, Unit u, double v); - void setNumericValue(FieldInfo *fi, DVEntry *dve, Unit u, double v); - double getNumericValue(std::string vname, Unit u); - double getNumericValue(FieldInfo *fi, Unit u); - void setStringValue(std::string vname, std::string v, DVEntry *dve = NULL); - void setStringValue(FieldInfo *fi, std::string v, DVEntry *dve); - std::string getStringValue(FieldInfo *fi); - - // Check if the meter has received a value for this field. - bool hasValue(FieldInfo *fi); - bool hasNumericValue(FieldInfo *fi); - bool hasStringValue(FieldInfo *fi); - - std::string decodeTPLStatusByte(uchar sts); - - bool addOptionalLibraryFields(std::string fields); - - std::vector &selectedFields() { return selected_fields_; } - void setSelectedFields (std::vector &f) { selected_fields_ = f; } - - void forceMfctIndex(int i) { force_mfct_index_ = i; } - bool hasReceivedFirstTelegram() { return has_received_first_telegram_; } - void markFirstTelegramReceived() { has_received_first_telegram_ = true; } - -private: - - int index_ {}; - MeterType type_ {}; - DriverName driver_name_; - DriverInfo *driver_info_ {}; - std::string bus_ {}; - MeterKeys meter_keys_ {}; - ELLSecurityMode expected_ell_sec_mode_ {}; - TPLSecurityMode expected_tpl_sec_mode_ {}; - std::string name_; - std::vector address_expressions_; - IdentityMode identity_mode_; - int num_updates_ {}; - time_t datetime_of_update_ {}; - time_t datetime_of_poll_ {}; - LinkModeSet link_modes_ {}; - std::vector shell_cmdlines_added_; - std::vector shell_cmdlines_updated_; - std::vector extra_constant_fields_; - time_t poll_interval_ {}; - Translate::Lookup mfct_tpl_status_bits_ = NoLookup; - int force_mfct_index_ = -1; - bool has_process_content_ = false; - bool has_received_first_telegram_ = false; - -protected: - - std::vector field_infos_; - // This is the number of fields in the driver, not counting the used library fields. - size_t num_driver_fields_ {}; - std::vector field_names_; - // Defaults to a setting specified in the driver. Can be overridden in the meter file. - // There is also a global selected_fields that can be set on the command line or in the conf file. - std::vector selected_fields_; - // Map difvif key to hex values from telegrams. - std::map> hex_values_; - // Map field name+Unit to Numeric field which includes the value. - std::map,NumericField> numeric_values_; - // Map field name (at_date) to std::string value. - std::map string_values_; - // If the telegram ends with 0x1f then set this to true, and the poll - // code will poll again with 0x7b instead of 0x5b. - bool more_records_follow_; -}; - -#endif diff --git a/components/wmbus_common/quirks.cpp b/components/wmbus_common/quirks.cpp new file mode 100644 index 00000000..407f34fe --- /dev/null +++ b/components/wmbus_common/quirks.cpp @@ -0,0 +1,252 @@ +#include "quirks.h" + +#include +#include +#include + +#include "crypto.h" +#include "esphome/core/helpers.h" + +namespace wmbus::quirks { + +enum class DiehlFrameInterpretation { NA, REAL_DATA, OMS, PRIOS, SAP_PRIOS, SAP_PRIOS_STD, PRIOS_SCR, RESERVED }; + +static bool is_diehl_manufacturer(int m) { + return m == manufacturer_code('D', 'M', 'E') || m == manufacturer_code('E', 'W', 'T') || + m == manufacturer_code('H', 'Y', 'D') || m == manufacturer_code('S', 'A', 'P') || + m == manufacturer_code('S', 'P', 'L'); +} + +static DiehlFrameInterpretation diehl_interpretation(uint8_t c_field, int m_field, uint8_t ci_field, int tpl_cfg) { + if (!is_diehl_manufacturer(m_field)) + return DiehlFrameInterpretation::NA; + if (c_field != 0x44 && c_field != 0x46) + return DiehlFrameInterpretation::NA; + + switch (ci_field) { + case 0x71: + return DiehlFrameInterpretation::REAL_DATA; + case 0x7A: + if (((tpl_cfg >> 8) & 0x10) == 0x10) + return DiehlFrameInterpretation::REAL_DATA; + return DiehlFrameInterpretation::OMS; + case 0xA0: + case 0xA1: + case 0xA2: + case 0xA3: + case 0xA4: + case 0xA5: + case 0xA6: + case 0xA7: + if (m_field == manufacturer_code('S', 'A', 'P')) + return DiehlFrameInterpretation::SAP_PRIOS; + return DiehlFrameInterpretation::PRIOS; + case 0xB0: + if (m_field == manufacturer_code('S', 'A', 'P')) + return DiehlFrameInterpretation::SAP_PRIOS_STD; + return DiehlFrameInterpretation::RESERVED; + case 0xA8: + case 0xA9: + case 0xAA: + case 0xAB: + case 0xAC: + case 0xAD: + case 0xAE: + case 0xAF: + case 0xB4: + case 0xB5: + case 0xB6: + case 0xB7: + return DiehlFrameInterpretation::RESERVED; + case 0xB1: + case 0xB2: + case 0xB3: + return DiehlFrameInterpretation::PRIOS_SCR; + default: + return DiehlFrameInterpretation::OMS; + } +} + +static DiehlFrameInterpretation diehl_interpretation(const Telegram &t) { + if (t.frame.size() < 15) + return DiehlFrameInterpretation::NA; + const std::span f = t.frame; + return diehl_interpretation(f[1], esphome::encode_uint16(f[3], f[2]), f[10], esphome::encode_uint16(f[14], f[13])); +} + +void diehl_preprocess(Telegram &t) { + DiehlFrameInterpretation fi = diehl_interpretation(t); + bool swap = fi == DiehlFrameInterpretation::PRIOS || fi == DiehlFrameInterpretation::PRIOS_SCR || + fi == DiehlFrameInterpretation::REAL_DATA; + bool sap = fi == DiehlFrameInterpretation::SAP_PRIOS; + if (!swap && !sap) + return; + + if (swap) { + uint8_t version = t.frame[4]; + uint8_t type = t.frame[5]; + for (int i = 4; i < 8; i++) + t.frame[i] = t.frame[i + 2]; + t.frame[8] = version; + t.frame[9] = type; + } else { + t.frame[8] = 0x00; // on an Izar the version is part of the id + t.frame[9] = 0x07; // water meter + } +} + +static uint32_t uint32_from_bytes(std::span data, size_t offset) { + return esphome::encode_uint32(data[offset], data[offset + 1], data[offset + 2], data[offset + 3]); +} + +static uint32_t convert_key(std::span bytes) { + return uint32_from_bytes(bytes, 0) ^ uint32_from_bytes(bytes, 4); +} + +static const uint8_t PRIOS_DEFAULT_KEY1[8] = {0x39, 0xBC, 0x8A, 0x10, 0xE6, 0x6D, 0x83, 0xF8}; +static const uint8_t PRIOS_DEFAULT_KEY2[8] = {0x51, 0x72, 0x89, 0x10, 0xE6, 0x6D, 0x83, 0xF8}; + +static std::vector diehl_candidates(const Key &key) { + if (key) + return {convert_key(std::span(*key).first<8>())}; + return {convert_key(PRIOS_DEFAULT_KEY1), convert_key(PRIOS_DEFAULT_KEY2)}; +} + +enum class DiehlLfsrCheck { CHECKSUM_AND_0XEF, HEADER_1_BYTE }; + +// Empty when the key does not fit. +static std::vector decode_diehl_lfsr(std::span origin, std::span frame, + uint32_t key, DiehlLfsrCheck check, uint32_t check_value) { + if (frame.size() < 15) + return {}; + key ^= uint32_from_bytes(origin, 2); + key ^= uint32_from_bytes(origin, 6); + key ^= uint32_from_bytes(frame, 10); + + std::vector out(frame.size() - 15); + for (size_t i = 0; i < out.size(); ++i) { + for (int j = 0; j < 8; ++j) { + uint8_t bit = ((key & 0x2) != 0) ^ ((key & 0x4) != 0) ^ ((key & 0x800) != 0) ^ ((key & 0x80000000) != 0); + key = (key << 1) | bit; + } + out[i] = frame[i + 15] ^ (key & 0xFF); + + if (check == DiehlLfsrCheck::HEADER_1_BYTE) { + if (out[0] != 0x4B) + return {}; + } else if (i == out.size() - 1) { + if ((std::accumulate(out.begin(), out.end(), 0u) & 0xEF) != check_value) + return {}; + } + } + return out; +} + +void add_default_key(Key &key, const Telegram &t) { + if (!key && t.tpl->sec_mode == TPL_AES_CBC_IV && diehl_interpretation(t) == DiehlFrameInterpretation::OMS) { + key.emplace(); + memcpy(key->data(), PRIOS_DEFAULT_KEY2, 8); + memcpy(key->data() + 8, PRIOS_DEFAULT_KEY2, 8); + } +} + +bool diehl_decrypt_real_data(Telegram &t, size_t pos, const Key &key) { + std::vector decoded; + for (uint32_t candidate : diehl_candidates(key)) { + decoded = + decode_diehl_lfsr(t.dll_as_received, t.frame, candidate, DiehlLfsrCheck::CHECKSUM_AND_0XEF, t.frame[14] & 0xEF); + if (!decoded.empty()) + break; + } + if (decoded.empty()) + return false; + t.frame.resize(pos + decoded.size()); + std::copy(decoded.begin(), decoded.end(), t.frame.begin() + pos); + return true; +} + +bool diehl_prios_decode(Telegram &t, const Key &key) { + std::vector decoded; + for (uint32_t candidate : diehl_candidates(key)) { + decoded = decode_diehl_lfsr(t.dll_as_received, t.frame, candidate, DiehlLfsrCheck::HEADER_1_BYTE, 0x4B); + if (!decoded.empty()) + break; + } + if (decoded.empty()) + return false; + + // ixml expects four bytes past the header, then the decoded payload. + const size_t hs = t.header_size; + const size_t head = std::min(4, t.frame.size() > hs ? t.frame.size() - hs : 0); + std::vector &out = t.derived.emplace_back(t.frame.begin() + hs, t.frame.begin() + hs + head); + out.insert(out.end(), decoded.begin(), decoded.end()); + return true; +} + +std::optional sap_prios_identity(const Telegram &t) { + if (diehl_interpretation(t) != DiehlFrameInterpretation::SAP_PRIOS) + return {}; + const std::span o = t.dll_as_received; + const uint32_t number = esphome::encode_uint32(o[7] & 0x03, o[6], o[5], o[4]); + // Eight digits: two-digit year of manufacture, then the serial number. + const uint32_t yy = (number / 1000000) % 100; + const uint32_t serial = number % 1000000; + const char supplier_code = (char) ('@' + (((o[9] & 0x0F) << 1) | (o[8] >> 7))); + const char meter_type = (char) ('@' + ((o[8] & 0x7C) >> 2)); + const char diameter = (char) ('@' + (((o[8] & 0x03) << 3) | (o[7] >> 5))); + return SapPriosIdentity{ + esphome::str_sprintf("%c%02u%c%c", supplier_code, (unsigned) yy, meter_type, diameter), + esphome::str_sprintf("%06u", (unsigned) serial), + esphome::str_sprintf("%u", (unsigned) (yy > 70 ? 1900 + yy : 2000 + yy)), + }; +} + +bool diehl_is_real_data(const Telegram &t) { return diehl_interpretation(t) == DiehlFrameInterpretation::REAL_DATA; } + +// 0x609b is a marker, not noise: captures under different access counters all carry it. +bool sanxing_609b_ok(std::span frame, size_t pos) { + return frame[pos] == 0x60 && frame[pos + 1] == 0x9b && frame[frame.size() - 2] == 0x2f && + frame[frame.size() - 1] == 0x2f; +} + +// Qundis Q 5.5 walk-by block (0DFF5F): mode 5 under the meter key when byte [4] is 0x35 (plaintext: 0x00). +// A CI 0x78 frame has no TPL header, so the IV's access number is the block's rolling counter in byte [2]. +std::optional> qundis_walk_by_decode(Telegram &t, const Key &key, std::span block) { + if (block.size() < 9 || block[0] != 0x00 || block[1] != 0x82 || block[4] != 0x35) + return {}; + if (!key) { + // The grammar's header guard then rejects the ciphertext rather than decode it. + t.decoding_errors.add("MISSING_KEY"); + return {}; + } + const std::span cipher = block.subspan(5); + if (cipher.empty() || cipher.size() % 16 != 0) + return {}; + + t.tpl->acc = block[2]; + std::array iv = t.tpl_iv(); + // With an enhanced identification record (0779), the block is encrypted for that id, not the converter's. + for (const Record &r : t.records) { + if (r.dif == 0x07 && r.vif == 0x79 && r.bytes().size() >= 8) { + std::span b = r.bytes(); + iv[0] = b[4]; + iv[1] = b[5]; + iv[2] = b[0]; + iv[3] = b[1]; + iv[4] = b[2]; + iv[5] = b[3]; + iv[6] = b[6]; + iv[7] = b[7]; + break; + } + } + + std::vector plain(block.begin(), block.end()); + // A plaintext block has 0x00 here. + plain[4] = 0x00; + std::span blocks = std::span(plain).subspan(5); + wmbus::aes_cbc_decrypt(*key, iv, blocks); + return plain; +} + +} // namespace wmbus::quirks diff --git a/components/wmbus_common/quirks.h b/components/wmbus_common/quirks.h new file mode 100644 index 00000000..6b9f6631 --- /dev/null +++ b/components/wmbus_common/quirks.h @@ -0,0 +1,44 @@ +// Diehl's quirks are frame-level: its header must be fixed before the address is readable. +// The rest are named by a driver's `transform_payload` and reach the decoder as PayloadQuirk. + +#pragma once + +#include +#include +#include +#include +#include + +#include "telegram.h" + +namespace wmbus::quirks { + +// As izarv2 prints it. +struct SapPriosIdentity { + std::string prefix; + std::string serial_number; + std::string manufacture_y; +}; + +// Restores Diehl's rearranged DLL header; runs on every frame. +void diehl_preprocess(Telegram &t); + +// A Diehl frame whose payload is the LFSR stream rather than records. +bool diehl_is_real_data(const Telegram &t); +// Unrolls that payload over the frame in place; false when no key fits. +bool diehl_decrypt_real_data(Telegram &t, size_t pos, const Key &key); + +// An OMS frame from a Diehl meter is encrypted with a published key. +void add_default_key(Key &key, const Telegram &t); + +// PRIOS: unrolls the LFSR payload into `derived`; false when no key fits. +bool diehl_prios_decode(Telegram &t, const Key &key); +std::optional sap_prios_identity(const Telegram &t); + +// Sanxing S34U28: a fixed 0x609b marker replaces the 2f2f check; trailing 2f2f padding checks the key. +bool sanxing_609b_ok(std::span frame, size_t pos); + +// Nothing when the block is not encrypted; MISSING_KEY in the status when there is no key. +std::optional> qundis_walk_by_decode(Telegram &t, const Key &key, std::span block); + +} // namespace wmbus::quirks diff --git a/components/wmbus_common/records.cpp b/components/wmbus_common/records.cpp new file mode 100644 index 00000000..f5020af9 --- /dev/null +++ b/components/wmbus_common/records.cpp @@ -0,0 +1,505 @@ +#include "records.h" + +#include +#include +#include +#include + +#include "esphome/core/alloc_helpers.h" +#include "esphome/core/helpers.h" + +namespace wmbus { + +// A two-byte VIF opens with one of these. +static bool is_vif_extension(uint8_t vif) { return vif == 0xfb || vif == 0xfd || vif == 0xef || vif == 0xff; } + +std::optional parse_record_header(std::span bytes) { + RecordHeader h{}; + size_t i = 0; + + auto take = [&]() -> std::optional { + if (i >= bytes.size()) + return {}; + return bytes[i++]; + }; + + const std::optional dif = take(); + if (!dif) + return {}; + h.dif = *dif; + h.measurement_type = dif_measurement_type(h.dif); + h.storage_nr = (h.dif & 0x40) >> 6; + + int difenr = 0; + bool more = (h.dif & 0x80) != 0; + while (more && difenr < 10) { + const std::optional dife = take(); + if (!dife) + return {}; + h.subunit_nr |= ((*dife & 0x40) >> 6) << difenr; + h.tariff_nr |= ((*dife & 0x30) >> 4) << (difenr * 2); + h.storage_nr |= (*dife & 0x0f) << (1 + difenr * 4); + more = (*dife & 0x80) != 0; + difenr++; + } + + const std::optional vif = take(); + if (!vif) + return {}; + h.vif = *vif & 0x7f; + bool extension_vif = is_vif_extension(*vif); + if (extension_vif) + h.vif <<= 8; + + // A VIF written as text: a length byte and the characters. + if (*vif == 0x7c || *vif == 0xfc) { + const std::optional len = take(); + if (!len) + return {}; + for (uint8_t k = 0; k < *len; ++k) { + if (!take()) + return {}; + } + } + + int combinable = 0; + bool combinable_extension = false; + more = (*vif & 0x80) != 0; + int vifenr = 0; + while (more && vifenr < 10) { + const std::optional vife = take(); + if (!vife) + return {}; + more = (*vife & 0x80) != 0; + vifenr++; + + if (extension_vif) { + h.vif |= *vife & 0x7f; + extension_vif = false; + } else if (combinable_extension) { + combinable |= *vife & 0x7f; + combinable_extension = false; + h.combinables.push_back((uint16_t) combinable); + } else { + combinable = *vife & 0x7f; + if (combinable == 0x7c || combinable == 0x7f) { + combinable <<= 8; + combinable_extension = true; + } else { + h.combinables.push_back((uint16_t) combinable); + } + } + } + + h.length = (uint8_t) i; + return h; +} + +void Record::set_header(std::span key, const RecordHeader &h) { + id.assign(key.begin(), key.begin() + h.length); + dif = h.dif; + vif = h.vif; + measurement_type = h.measurement_type; + storage_nr = h.storage_nr; + tariff_nr = h.tariff_nr; + subunit_nr = h.subunit_nr; + combinables.clear(); + for (uint16_t raw : h.combinables) + push_combinable(raw); +} + +std::span Record::bytes() const { + if (!computed.empty()) + return computed; + return slice; +} + +bool Record::key_equals(const char *key) const { + for (uint8_t byte : id) { + if (key[0] != esphome::format_hex_pretty_char(byte >> 4) || key[1] != esphome::format_hex_pretty_char(byte & 0x0f)) + return false; + key += 2; + } + return *key == 0; +} + +bool Record::has_combinable(uint16_t raw) const { return std::ranges::find(combinables, raw) != combinables.end(); } + +bool Record::has_combinable_in(VifSpan span) const { + return std::ranges::any_of(combinables, [span](uint16_t listed) { return span.contains(listed); }); +} + +void Record::push_combinable(uint16_t raw) { + if (has_combinable(raw)) + return; + combinables.push_back(raw); +} + +bool Record::same_id(std::span other) const { return std::ranges::equal(id, other); } + +// As upstream reads a BCD digit out of hex text: 'A'-'0' is 17, not 10. +static int bcd_digit(int nibble) { return nibble < 10 ? nibble : nibble + 7; } + +struct BcdValue { + uint64_t raw; + bool negative; +}; + +// Little-endian BCD; a top nibble of 0xF means negative. +static BcdValue bcd_value(std::span data) { + const size_t len = data.size(); + BcdValue out{0, false}; + int top = data[len - 1] >> 4; + if (top == 0xF) { + out.negative = true; + top = 0; + } + for (size_t i = len; i > 0; --i) { + int hi = (i == len) ? top : (data[i - 1] >> 4); + int lo = data[i - 1] & 0x0f; + out.raw = out.raw * 100 + bcd_digit(hi) * 10 + bcd_digit(lo); + } + return out; +} + +static uint64_t little_endian(std::span data) { + uint64_t raw = 0; + for (size_t i = 0; i < data.size(); ++i) + raw |= ((uint64_t) data[i]) << (8 * i); + return raw; +} + +std::optional Record::extract_double(bool auto_scale, bool force_unsigned) const { + const std::span data = bytes(); + const int t = dif & 0xf; + // Nothing to extract: a selection, a variable length or a special function. + if (t == 0x0 || t == 0x8 || t == 0xd || t == 0xf) + return {}; + + double draw = 0; + if (dif_is_binary(t)) { + if (data.size() != (size_t) dif_len_bytes(t)) + return {}; + const uint64_t raw = little_endian(data); + const int bits = (int) data.size() * 8; + const bool negate = !force_unsigned && (raw & ((uint64_t) 1 << (bits - 1))) != 0; + draw = (double) raw; + if (negate) { + const uint64_t mask = bits == 64 ? 0 : (~((uint64_t) 0) << bits); + draw = (double) ((int64_t) (mask | raw)); + } + } else if (dif_is_bcd(t)) { + if (all_ff(data)) + return {}; + if (data.size() != (size_t) dif_len_bytes(t)) + return {}; + const BcdValue bcd = bcd_value(data); + draw = (double) bcd.raw; + if (bcd.negative) + draw = draw * -1; + } else if (t == 0x5) { + if (data.size() != 4) + return {}; + const uint32_t bits = (uint32_t) little_endian(data); + float f; + memcpy(&f, &bits, sizeof(f)); + draw = f; + } else { + return {}; + } + + const double scale = auto_scale ? vif_scale(vif) : 1.0; + return draw / scale; +} + +std::optional Record::extract_long() const { + const std::span data = bytes(); + const int t = dif & 0xf; + if (dif_is_binary(t)) { + if (data.size() != (size_t) dif_len_bytes(t)) + return {}; + return little_endian(data); + } + if (dif_is_bcd(t)) { + if (all_ff(data)) + return {}; + if (data.size() != (size_t) dif_len_bytes(t)) + return {}; + const BcdValue bcd = bcd_value(data); + if (bcd.negative) + return (uint64_t) (((int64_t) bcd.raw) * -1); + return bcd.raw; + } + return {}; +} + +static bool extract_date_bytes(uint8_t hi, uint8_t lo, struct tm &date) { + int day = 0x1f & lo; + int year1 = (0xe0 & lo) >> 5; + int month = 0x0f & hi; + int year2 = (0xf0 & hi) >> 1; + int year = 2000 + year1 + year2; + date.tm_mday = day; + date.tm_mon = month - 1; + date.tm_year = year - 1900; + return month <= 12; +} + +static bool extract_time_bytes(uint8_t hi, uint8_t lo, struct tm &date) { + int min = 0x3f & lo; + int hour = 0x1f & hi; + date.tm_min = min; + date.tm_hour = hour; + return min <= 59 && hour <= 23; +} + +DecodedDate Record::extract_date() const { + const std::span data = bytes(); + DecodedDate out{}; + out.value.tm_isdst = -1; + out.valid = true; + if (data.size() == 2) { + out.valid &= extract_date_bytes(data[1], data[0], out.value); + } else if (data.size() == 4) { + out.valid &= extract_date_bytes(data[3], data[2], out.value); + out.valid &= extract_time_bytes(data[1], data[0], out.value); + } else if (data.size() == 6) { + out.valid &= extract_date_bytes(data[4], data[3], out.value); + out.valid &= extract_time_bytes(data[2], data[1], out.value); + out.value.tm_sec = 0x3f & data[0]; + } + return out; +} + +// Leading zero bytes are padding; what follows must all be printable. +static bool is_likely_ascii(std::span data) { + const auto text = std::ranges::find_if(data, [](uint8_t b) { return b != 0; }); + return text != data.end() && + std::all_of(text, data.end(), [](uint8_t b) { return (b >= 20 && b <= 126) || b == 0x0C || b == 0x0A; }); +} + +static std::string safe_string(std::span data, bool reversed) { + std::string s; + const auto append = [&s](uint8_t ch) { + if (ch >= 32 && ch < 127 && ch != '<' && ch != '>') { + s += (char) ch; + } else { + s += '<'; + s += esphome::format_hex_pretty_char(ch >> 4); + s += esphome::format_hex_pretty_char(ch & 0x0f); + s += '>'; + } + }; + if (reversed) + std::ranges::for_each(data | std::views::reverse, append); + else + std::ranges::for_each(data, append); + return s; +} + +static std::string reverse_bcd(std::span data) { + std::string s; + for (uint8_t b : data | std::views::reverse) { + s += esphome::format_hex_pretty_char(b >> 4); + s += esphome::format_hex_pretty_char(b & 0x0f); + } + return s; +} + +std::string Record::readable_string(bool reversed) const { + const std::span data = bytes(); + const int t = dif & 0xf; + const bool binary_or_varlen = dif_is_binary(t) || t == 0xD; + + if (reversed) { + if (binary_or_varlen && is_likely_ascii(data)) + return safe_string(data, false); + return hex_string(); + } + if (binary_or_varlen) { + if (is_likely_ascii(data)) + return safe_string(data, true); + return reverse_bcd(data); + } + if (dif_is_bcd(t)) + return reverse_bcd(data); + return hex_string(); +} + +std::string Record::hex_string() const { + const std::span data = bytes(); + return esphome::format_hex_pretty(data.data(), data.size(), 0, false); +} + +// -1: the length is the first data byte; -2: no data of its own. +int dif_len_bytes(uint8_t dif) { + static const int8_t LEN[16] = {0, 1, 2, 3, 4, 4, 6, 8, 0, 1, 2, 3, 4, -1, 6, -2}; + // The idle filler 2F is the one F type that stands for a byte. + if (dif == 0x2f) + return 1; + return LEN[dif & 0x0f]; +} + +bool dif_is_binary(uint8_t type) { + return type == 0x1 || type == 0x2 || type == 0x3 || type == 0x4 || type == 0x6 || type == 0x7; +} + +bool dif_is_bcd(uint8_t type) { return type == 0x9 || type == 0xA || type == 0xB || type == 0xC || type == 0xE; } + +MeasurementType dif_measurement_type(uint8_t dif) { + switch (dif & 0x30) { + case 0x00: + return MeasurementType::Instantaneous; + case 0x10: + return MeasurementType::Maximum; + case 0x20: + return MeasurementType::Minimum; + default: + return MeasurementType::AtError; + } +} + +// Not pow10_int(): float is not precise enough down to 1e-12. +static double pow10(int exp) { + double p = 1.0; + for (int i = exp < 0 ? -exp : exp; i > 0; --i) + p *= 10.0; + return exp < 0 ? 1.0 / p : p; +} + +// Seconds, minutes, hours, days. +static const double TIME_SCALE[4] = {3600.0, 60.0, 1.0, 1.0 / 24.0}; + +// Primary VIFs: the block picks the starting decade, the low bits step down from it. +double vif_scale(uint16_t vif) { + vif &= 0x7f7f; + + // Energy Wh/J, volume m3, mass kg, power W and J/h, volume flow. + if (vif <= 0x17) + return pow10(6 - (vif & 0x07)); + if (vif <= 0x1f) + return pow10(3 - (vif & 0x07)); + if (vif <= 0x27) + return TIME_SCALE[vif & 0x03]; + if (vif <= 0x3f) + return pow10(6 - (vif & 0x07)); + // Volume flow per minute and per hour: sixths of a decade, and J/h. + if (vif <= 0x47) + return 6 * pow10(8 - (vif & 0x07)); + if (vif <= 0x4f) + return 3600 * pow10(9 - (vif & 0x07)); + // Temperatures, pressure, temperature difference: decades of four codes. + if (vif <= 0x57) + return pow10(3 - (vif & 0x07)); + if (vif <= 0x6b) + return pow10(3 - (vif & 0x03)); + // Dates, HCA units and the fabrication number have no scale. + if (vif <= 0x6e) + return 1.0; + if (vif <= 0x6f) + return -1.0; + if (vif <= 0x77) + return TIME_SCALE[vif & 0x03]; + if (vif == 0x7c) + return 1.0; + if (vif <= 0x7f) + return -1.0; + + // FB: energy in MWh and GJ, one or two decimals. + if (vif == 0x7b00 || vif == 0x7b01 || vif == 0x7b08 || vif == 0x7b09) + return pow10(-((vif & 0x1) + 2)); + if (vif == 0x7b1a) + return 10.0; + if (vif == 0x7b1b) + return 1.0; + + // FD: access number, durations, voltage, current, firmware date. + if (vif == 0x7d08 || vif == 0x7d3a || vif == 0x7d61 || vif == 0x7d74) + return 1.0; + if ((vif >= 0x7d2c && vif <= 0x7d2f) || (vif >= 0x7d31 && vif <= 0x7d33)) + return TIME_SCALE[vif & 0x03]; + if (vif >= 0x7d40 && vif <= 0x7d4f) + return pow10(9 - (vif & 0xf)); + if (vif >= 0x7d50 && vif <= 0x7d5f) + return pow10(12 - (vif & 0xf)); + return -1; +} + +static const char *strftime_format(TimeFormat f) { + switch (f) { + case TimeFormat::Date: + return "%Y-%m-%d"; + case TimeFormat::DateTime: + return "%Y-%m-%d %H:%M"; + case TimeFormat::DateTimeSec: + return "%Y-%m-%d %H:%M:%S"; + case TimeFormat::TimestampUTC: + return "%FT%TZ"; + } + return ""; +} + +std::string format_time(const struct tm &t, TimeFormat f) { + char buf[40]; + strftime(buf, sizeof(buf), strftime_format(f), &t); + return std::string(buf); +} + +std::string format_time(double epoch_seconds, TimeFormat f) { + time_t t = (time_t) epoch_seconds; + struct tm broken_down; + gmtime_r(&t, &broken_down); + return format_time(broken_down, f); +} + +static bool is_leap_year(int year) { + year += 1900; + if (year % 4 != 0) + return false; + if (year % 400 == 0) + return true; + if (year % 100 == 0) + return false; + return true; +} + +static int get_days_in_month(int year, int month) { + static const int days_in_months[] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; + if (month < 0 || month >= 12) + month = 0; + int days = days_in_months[month]; + if (month == 1 && is_leap_year(year)) + days += 1; + return days; +} + +// The last day of a month stays the last day of the month it lands in. +void add_months(struct tm &date, int months) { + const bool is_last_day_in_month = date.tm_mday == get_days_in_month(date.tm_year, date.tm_mon); + + int year = date.tm_year + months / 12; + int month = date.tm_mon + months % 12; + while (month > 11) { + year += 1; + month -= 12; + } + while (month < 0) { + year -= 1; + month += 12; + } + + date.tm_year = year; + date.tm_mon = month; + date.tm_mday = + is_last_day_in_month ? get_days_in_month(year, month) : std::min(date.tm_mday, get_days_in_month(year, month)); +} + +double add_months(double t, int months) { + time_t ut = (time_t) t; + struct tm time; + gmtime_r(&ut, &time); + add_months(time, months); + return (double) mktime(&time); +} + +} // namespace wmbus diff --git a/components/wmbus_common/records.h b/components/wmbus_common/records.h new file mode 100644 index 00000000..30553c40 --- /dev/null +++ b/components/wmbus_common/records.h @@ -0,0 +1,95 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +namespace wmbus { + +enum class MeasurementType : uint8_t { Any, Instantaneous, Minimum, Maximum, AtError }; + +// Codegen resolves upstream's named VIF ranges to these. +struct VifSpan { + uint16_t from; + uint16_t to; + + constexpr bool contains(uint16_t v) const { return v >= from && v <= to; } +}; + +// Filled in as far as it parsed; callers use the partial date too. +struct DecodedDate { + struct tm value; + bool valid; +}; + +struct RecordHeader { + uint8_t dif; + uint16_t vif; // the full vif: 0x13, or 0xFD17 + MeasurementType measurement_type; + int32_t storage_nr; + int32_t tariff_nr; + int32_t subunit_nr; + std::vector combinables; + uint8_t length; // of the header alone +}; + +std::optional parse_record_header(std::span bytes); + +struct Record { + // `computed` holds a compact profile's bytes, since a Record leaves the walk by value. + std::span slice; + std::vector computed; + uint16_t offset; + uint8_t dif; + uint16_t vif; // the full vif: 0x13, or 0xFD17 + MeasurementType measurement_type; + int32_t storage_nr; + int32_t tariff_nr; + int32_t subunit_nr; + std::vector id; // the dif/dife/vif/vife bytes the key is made of + std::vector combinables; + + std::span bytes() const; + + // Against a key in upstream's notation: "0C13", "8440FF22". + bool key_equals(const char *key) const; + bool same_id(std::span other) const; + + void push_combinable(uint16_t raw); + bool has_combinable(uint16_t raw) const; + bool has_combinable_in(VifSpan span) const; + + std::optional extract_double(bool auto_scale, bool force_unsigned) const; + std::optional extract_long() const; + DecodedDate extract_date() const; + std::string readable_string(bool reversed) const; + std::string hex_string() const; + + void set_header(std::span key, const RecordHeader &header); + // dif with the storage nr, dife..., vif, and the Synthetic combinable. + void set_synthetic_id(uint8_t dif_nibble, int storage_nr, uint8_t vif); +}; + +MeasurementType dif_measurement_type(uint8_t dif); +int dif_len_bytes(uint8_t dif); +bool dif_is_binary(uint8_t type); +bool dif_is_bcd(uint8_t type); +// An all-FF field is "no value", in a record and in a profile's slot alike. +inline bool all_ff(std::span data) { + return std::ranges::all_of(data, [](uint8_t b) { return b == 0xff; }); +} +double vif_scale(uint16_t vif); + +// Formatted according to TimeFormat, all printed in UTC (as-is from meter). +enum class TimeFormat { Date, DateTime, DateTimeSec, TimestampUTC }; + +std::string format_time(const struct tm &t, TimeFormat f); +std::string format_time(double epoch_seconds, TimeFormat f); +void add_months(struct tm &date, int months); +double add_months(double t, int months); + +} // namespace wmbus diff --git a/components/wmbus_common/tables/quantities.yaml b/components/wmbus_common/tables/quantities.yaml new file mode 100644 index 00000000..16e6ba18 --- /dev/null +++ b/components/wmbus_common/tables/quantities.yaml @@ -0,0 +1,25 @@ +# Generated by scripts/wmbus_common/fetch_upstream.py from wmbusmeters 3.0.0-188-gac3d579; do not edit. +Time: Hour +Length: M +Mass: KG +Amperage: Ampere +Temperature: C +AmountOfSubstance: MOL +LuminousIntensity: CD +Energy: KWH +Reactive_Energy: KVARH +Apparent_Energy: KVAH +Power: KW +Reactive_Power: KVAR +Apparent_Power: KVA +Volume: M3 +Flow: M3H +Voltage: Volt +Frequency: Hertz +Pressure: BAR +PointInTime: DateTimeLT +RelativeHumidity: RH +HCA: HCA +Text: TXT +Angle: DEGREE +Dimensionless: COUNTER diff --git a/components/wmbus_common/tables/units.yaml b/components/wmbus_common/tables/units.yaml new file mode 100644 index 00000000..f48162ce --- /dev/null +++ b/components/wmbus_common/tables/units.yaml @@ -0,0 +1,403 @@ +# Generated by scripts/wmbus_common/fetch_upstream.py from wmbusmeters 3.0.0-188-gac3d579; do not edit. +Second: + symbol: s + display: s + quantity: Time + si: + scale: 1.0 + s: 1 +M: + symbol: m + display: m + quantity: Length + si: + scale: 1.0 + m: 1 +KG: + symbol: kg + display: kg + quantity: Mass + si: + scale: 1.0 + kg: 1 +Ampere: + symbol: a + display: A + quantity: Amperage + si: + scale: 1.0 + a: 1 +K: + symbol: k + display: K + quantity: Temperature + si: + scale: 1.0 + k: 1 +MOL: + symbol: mol + display: mol + quantity: AmountOfSubstance + si: + scale: 1.0 + mol: 1 +CD: + symbol: cd + display: cd + quantity: LuminousIntensity + si: + scale: 1.0 + cd: 1 +WH: + symbol: wh + display: Wh + quantity: Energy + si: + scale: 3600.0 + kg: 1 + m: 2 + s: -2 +KWH: + symbol: kwh + display: kWh + quantity: Energy + si: + scale: 3600000.0 + kg: 1 + m: 2 + s: -2 +MJ: + symbol: mj + display: MJ + quantity: Energy + si: + scale: 1000000.0 + kg: 1 + m: 2 + s: -2 +GJ: + symbol: gj + display: GJ + quantity: Energy + si: + scale: 1000000000.0 + kg: 1 + m: 2 + s: -2 +GCAL: + symbol: gcal + display: Gcal + quantity: Energy + si: + scale: 4186800000.0 + kg: 1 + m: 2 + s: -2 +KVARH: + symbol: kvarh + display: kVARh + quantity: Reactive_Energy + si: + scale: 3600000.0 + kg: 1 + m: 2 + s: -2 +KVAH: + symbol: kvah + display: kVAh + quantity: Apparent_Energy + si: + scale: 3600000.0 + kg: 1 + m: 2 + s: -2 +M3C: + symbol: m3c + display: m³°C + quantity: Energy + si: + scale: 1.0 + m: 3 + c: 1 +W: + symbol: w + display: W + quantity: Power + si: + scale: 1.0 + kg: 1 + m: 2 + s: -3 +KW: + symbol: kw + display: kW + quantity: Power + si: + scale: 1000.0 + kg: 1 + m: 2 + s: -3 +JH: + symbol: jh + display: J/h + quantity: Power + si: + scale: 0.0002777777777777778 + kg: 1 + m: 2 + s: -3 +MJH: + symbol: mjh + display: MJ/h + quantity: Power + si: + scale: 277.77777777777777 + kg: 1 + m: 2 + s: -3 +KVAR: + symbol: kvar + display: kVAR + quantity: Reactive_Power + si: + scale: 1000.0 + kg: 1 + m: 2 + s: -3 +KVA: + symbol: kva + display: kVA + quantity: Apparent_Power + si: + scale: 1000.0 + kg: 1 + m: 2 + s: -3 +M3CH: + symbol: m3ch + display: m³°C/h + quantity: Power + si: + scale: 3600.0 + m: 3 + c: 1 + s: -1 +DBM: + symbol: dbm + display: dBm + quantity: Power + si: + scale: 1.0 + kg: 1 + m: 2 + s: -3 + nonlinear: true +M3: + symbol: m3 + display: m³ + quantity: Volume + si: + scale: 1.0 + m: 3 +L: + symbol: l + display: l + quantity: Volume + si: + scale: 0.001 + m: 3 +M3H: + symbol: m3h + display: m³/h + quantity: Flow + si: + scale: 3600.0 + m: 3 + s: -1 +LH: + symbol: lh + display: l/h + quantity: Flow + si: + scale: 3.6 + m: 3 + s: -1 +C: + symbol: c + display: °C + quantity: Temperature + si: + scale: 1.0 + c: 1 +F: + symbol: f + display: °F + quantity: Temperature + si: + scale: 1.0 + f: 1 +Volt: + symbol: v + display: V + quantity: Voltage + si: + scale: 1.0 + kg: 1 + m: 2 + s: -3 + a: -1 +Hertz: + symbol: hz + display: Hz + quantity: Frequency + si: + scale: 1.0 + s: -1 +PA: + symbol: pa + display: pa + quantity: Pressure + si: + scale: 1.0 + kg: 1 + m: -1 + s: -2 +BAR: + symbol: bar + display: bar + quantity: Pressure + si: + scale: 100000.0 + kg: 1 + m: -1 + s: -2 +Minute: + symbol: min + display: min + quantity: Time + si: + scale: 60.0 + s: 1 +Hour: + symbol: h + display: h + quantity: Time + si: + scale: 3600.0 + s: 1 +Day: + symbol: d + display: d + quantity: Time + si: + scale: 86400.0 + s: 1 +Month: + symbol: month + display: month + quantity: Time + si: + scale: 1.0 + month: 1 +Year: + symbol: y + display: y + quantity: Time + si: + scale: 1.0 + year: 1 +UnixTimestamp: + symbol: ut + display: ut + quantity: PointInTime + si: + scale: 1.0 + ut: 1 +DateTimeUTC: + symbol: utc + display: utc + quantity: PointInTime + si: + scale: 1.0 + ut: 1 +DateTimeLT: + symbol: datetime + display: datetime + quantity: PointInTime + si: + scale: 1.0 + ut: 1 +DateLT: + symbol: date + display: date + quantity: PointInTime + si: + scale: 1.0 + ut: 1 +TimeLT: + symbol: time + display: time + quantity: PointInTime + si: + scale: 1.0 + ut: 1 +RH: + symbol: rh + display: RH + quantity: RelativeHumidity + si: + scale: 1.0 +HCA: + symbol: hca + display: hca + quantity: HCA + si: + scale: 1.0 +TXT: + symbol: txt + display: txt + quantity: Text + si: + scale: 1.0 +DEGREE: + symbol: deg + display: ° + quantity: Angle + si: + scale: 1.0 +RADIAN: + symbol: rad + display: rad + quantity: Angle + si: + scale: 57.29577951308232 +COUNTER: + symbol: counter + display: counter + quantity: Dimensionless + si: + scale: 1.0 +FACTOR: + symbol: factor + display: factor + quantity: Dimensionless + si: + scale: 1.0 +NUMBER: + symbol: nr + display: number + quantity: Dimensionless + si: + scale: 1.0 +PERCENTAGE: + symbol: pct + display: percentage + quantity: Dimensionless + si: + scale: 0.01 +PPM: + symbol: ppm + display: ppm + quantity: Dimensionless + si: + scale: 1.0e-06 diff --git a/components/wmbus_common/tables/vif_combinables.yaml b/components/wmbus_common/tables/vif_combinables.yaml new file mode 100644 index 00000000..60f5d973 --- /dev/null +++ b/components/wmbus_common/tables/vif_combinables.yaml @@ -0,0 +1,298 @@ +# Generated by scripts/wmbus_common/fetch_upstream.py from wmbusmeters 3.0.0-188-gac3d579; do not edit. +Reserved: + from: 0x00 + to: 0x11 +Average: + from: 0x12 + to: 0x12 +InverseCompactProfile: + from: 0x13 + to: 0x13 +RelativeDeviation: + from: 0x14 + to: 0x14 +RecordErrorCodeMeterToController: + from: 0x15 + to: 0x1c +StandardConformantDataContent: + from: 0x1d + to: 0x1d +CompactProfileWithRegister: + from: 0x1e + to: 0x1e +CompactProfile: + from: 0x1f + to: 0x1f +PerSecond: + from: 0x20 + to: 0x20 +PerMinute: + from: 0x21 + to: 0x21 +PerHour: + from: 0x22 + to: 0x22 +PerDay: + from: 0x23 + to: 0x23 +PerWeek: + from: 0x24 + to: 0x24 +PerMonth: + from: 0x25 + to: 0x25 +PerYear: + from: 0x26 + to: 0x26 +PerRevolutionMeasurement: + from: 0x27 + to: 0x27 +IncrPerInputPulseChannel0: + from: 0x28 + to: 0x28 +IncrPerInputPulseChannel1: + from: 0x29 + to: 0x29 +IncrPerOutputPulseChannel0: + from: 0x2a + to: 0x2a +IncrPerOutputPulseChannel1: + from: 0x2b + to: 0x2b +PerLitre: + from: 0x2c + to: 0x2c +PerM3: + from: 0x2d + to: 0x2d +PerKg: + from: 0x2e + to: 0x2e +PerKelvin: + from: 0x2f + to: 0x2f +PerKWh: + from: 0x30 + to: 0x30 +PerGJ: + from: 0x31 + to: 0x31 +PerKW: + from: 0x32 + to: 0x32 +PerKelvinLitreW: + from: 0x33 + to: 0x33 +PerVolt: + from: 0x34 + to: 0x34 +PerAmpere: + from: 0x35 + to: 0x35 +MultipliedByS: + from: 0x36 + to: 0x36 +MultipliedBySDivV: + from: 0x37 + to: 0x37 +MultipliedBySDivA: + from: 0x38 + to: 0x38 +StartDateTimeOfAB: + from: 0x39 + to: 0x39 +UncorrectedMeterUnit: + from: 0x3a + to: 0x3a +ForwardFlow: + from: 0x3b + to: 0x3b +BackwardFlow: + from: 0x3c + to: 0x3c +ReservedNonMetric: + from: 0x3d + to: 0x3d +ValueAtBaseCondC: + from: 0x3e + to: 0x3e +ObisDeclaration: + from: 0x3f + to: 0x3f +LowerLimit: + from: 0x40 + to: 0x40 +ExceedsLowerLimit: + from: 0x41 + to: 0x41 +DateTimeExceedsLowerFirstBegin: + from: 0x42 + to: 0x42 +DateTimeExceedsLowerFirstEnd: + from: 0x43 + to: 0x43 +DateTimeExceedsLowerLastBegin: + from: 0x46 + to: 0x46 +DateTimeExceedsLowerLastEnd: + from: 0x47 + to: 0x47 +UpperLimit: + from: 0x48 + to: 0x48 +ExceedsUpperLimit: + from: 0x49 + to: 0x49 +DateTimeExceedsUpperFirstBegin: + from: 0x4a + to: 0x4a +DateTimeExceedsUpperFirstEnd: + from: 0x4b + to: 0x4b +DateTimeExceedsUpperLastBegin: + from: 0x4d + to: 0x4d +DateTimeExceedsUpperLastEnd: + from: 0x4e + to: 0x4e +DurationExceedsLowerFirst: + from: 0x50 + to: 0x53 +DurationExceedsLowerLast: + from: 0x54 + to: 0x57 +DurationExceedsUpperFirst: + from: 0x58 + to: 0x5b +DurationExceedsUpperLast: + from: 0x5c + to: 0x5f +DurationOfDFirst: + from: 0x60 + to: 0x63 +DurationOfDLast: + from: 0x64 + to: 0x67 +ValueDuringLowerLimitExceeded: + from: 0x68 + to: 0x68 +LeakageValues: + from: 0x69 + to: 0x69 +OverflowValues: + from: 0x6a + to: 0x6a +ValueDuringUpperLimitExceeded: + from: 0x6c + to: 0x6c +DateTimeOfDEFirstBegin: + from: 0x6a + to: 0x6a +DateTimeOfDEFirstEnd: + from: 0x6b + to: 0x6b +DateTimeOfDELastBegin: + from: 0x6e + to: 0x6e +DateTimeOfDELastEnd: + from: 0x6f + to: 0x6f +MultiplicativeCorrectionFactorForValue: + from: 0x70 + to: 0x77 +AdditiveCorrectionConstant: + from: 0x78 + to: 0x7b +CombinableVIFExtension: + from: 0x7c + to: 0x7c +MultiplicativeCorrectionFactorForValue103: + from: 0x7d + to: 0x7d +FutureValue: + from: 0x7e + to: 0x7e +MfctSpecific: + from: 0x7f + to: 0x7f +AtPhase1: + from: 0x7c01 + to: 0x7c01 +AtPhase2: + from: 0x7c02 + to: 0x7c02 +AtPhase3: + from: 0x7c03 + to: 0x7c03 +AtNeutral: + from: 0x7c04 + to: 0x7c04 +BetweenPhaseL1AndL2: + from: 0x7c05 + to: 0x7c05 +BetweenPhaseL2AndL3: + from: 0x7c06 + to: 0x7c06 +BetweenPhaseL3AndL1: + from: 0x7c07 + to: 0x7c07 +AtQuadrantQ1: + from: 0x7c08 + to: 0x7c08 +AtQuadrantQ2: + from: 0x7c09 + to: 0x7c09 +AtQuadrantQ3: + from: 0x7c0a + to: 0x7c0a +AtQuadrantQ4: + from: 0x7c0b + to: 0x7c0b +DeltaBetweenImportAndExport: + from: 0x7c0c + to: 0x7c0c +AccumulationOfAbsoluteValue: + from: 0x7c10 + to: 0x7c10 +DataPresentedWithTypeC: + from: 0x7c11 + to: 0x7c11 +DataPresentedWithTypeD: + from: 0x7c12 + to: 0x7c12 +Mfct00: + from: 0x7f00 + to: 0x7f00 +Mfct01: + from: 0x7f01 + to: 0x7f01 +Mfct02: + from: 0x7f02 + to: 0x7f02 +Mfct03: + from: 0x7f03 + to: 0x7f03 +Mfct04: + from: 0x7f04 + to: 0x7f04 +Mfct05: + from: 0x7f05 + to: 0x7f05 +Mfct06: + from: 0x7f06 + to: 0x7f06 +Mfct07: + from: 0x7f07 + to: 0x7f07 +Mfct08: + from: 0x7f08 + to: 0x7f08 +Mfct21: + from: 0x7f21 + to: 0x7f21 +Mfct72: + from: 0x7f72 + to: 0x7f72 +Synthetic: + from: 0x7f77 + to: 0x7f77 diff --git a/components/wmbus_common/tables/vif_ranges.yaml b/components/wmbus_common/tables/vif_ranges.yaml new file mode 100644 index 00000000..661fcad3 --- /dev/null +++ b/components/wmbus_common/tables/vif_ranges.yaml @@ -0,0 +1,189 @@ +# Generated by scripts/wmbus_common/fetch_upstream.py from wmbusmeters 3.0.0-188-gac3d579; do not edit. +Volume: + from: 0x10 + to: 0x17 + unit: M3 +OnTime: + from: 0x20 + to: 0x23 + unit: Hour +OperatingTime: + from: 0x24 + to: 0x27 + unit: Hour +VolumeFlow: + from: 0x38 + to: 0x3f + unit: M3H +FlowTemperature: + from: 0x58 + to: 0x5b + unit: C +ReturnTemperature: + from: 0x5c + to: 0x5f + unit: C +TemperatureDifference: + from: 0x60 + to: 0x63 + unit: C +ExternalTemperature: + from: 0x64 + to: 0x67 + unit: C +Pressure: + from: 0x68 + to: 0x6b + unit: BAR +HeatCostAllocation: + from: 0x6e + to: 0x6e + unit: HCA +Date: + from: 0x6c + to: 0x6c + unit: DateLT +DateTime: + from: 0x6d + to: 0x6d + unit: DateTimeLT +EnergyMJ: + from: 0x08 + to: 0x0f + unit: MJ +EnergyWh: + from: 0x00 + to: 0x07 + unit: KWH +PowerW: + from: 0x28 + to: 0x2f + unit: KW +PowerJh: + from: 0x30 + to: 0x37 + unit: MJH +ActualityDuration: + from: 0x74 + to: 0x77 + unit: Hour +FabricationNo: + from: 0x78 + to: 0x78 + unit: TXT +EnhancedIdentification: + from: 0x79 + to: 0x79 + unit: TXT +EnergyMWh: + from: 0x7b00 + to: 0x7b01 + unit: KWH +EnergyGJ: + from: 0x7b08 + to: 0x7b09 + unit: MJ +RelativeHumidity: + from: 0x7b1a + to: 0x7b1b + unit: RH +AccessNumber: + from: 0x7d08 + to: 0x7d08 + unit: COUNTER +Medium: + from: 0x7d09 + to: 0x7d09 + unit: TXT +Manufacturer: + from: 0x7d0a + to: 0x7d0a + unit: TXT +ParameterSet: + from: 0x7d0b + to: 0x7d0b + unit: TXT +ModelVersion: + from: 0x7d0c + to: 0x7d0c + unit: TXT +HardwareVersion: + from: 0x7d0d + to: 0x7d0d + unit: TXT +FirmwareVersion: + from: 0x7d0e + to: 0x7d0e + unit: TXT +SoftwareVersion: + from: 0x7d0f + to: 0x7d0f + unit: TXT +Location: + from: 0x7d10 + to: 0x7d10 + unit: TXT +Customer: + from: 0x7d11 + to: 0x7d11 + unit: TXT +ErrorFlags: + from: 0x7d17 + to: 0x7d17 + unit: TXT +DigitalOutput: + from: 0x7d1a + to: 0x7d1a + unit: TXT +DigitalInput: + from: 0x7d1b + to: 0x7d1b + unit: TXT +DurationSinceReadout: + from: 0x7d2c + to: 0x7d2f + unit: Hour +DurationOfTariff: + from: 0x7d31 + to: 0x7d33 + unit: Hour +Dimensionless: + from: 0x7d3a + to: 0x7d3a + unit: COUNTER +Voltage: + from: 0x7d40 + to: 0x7d4f + unit: Volt +Amperage: + from: 0x7d50 + to: 0x7d5f + unit: Ampere +ResetCounter: + from: 0x7d60 + to: 0x7d60 + unit: COUNTER +CumulationCounter: + from: 0x7d61 + to: 0x7d61 + unit: COUNTER +SpecialSupplierInformation: + from: 0x7d67 + to: 0x7d67 + unit: TXT +RemainingBattery: + from: 0x7d74 + to: 0x7d74 + unit: Day +AnyVolumeVIF: + from: 0x00 + to: 0x00 + unit: Unknown +AnyEnergyVIF: + from: 0x00 + to: 0x00 + unit: Unknown +AnyPowerVIF: + from: 0x00 + to: 0x00 + unit: Unknown diff --git a/components/wmbus_common/telegram.cpp b/components/wmbus_common/telegram.cpp new file mode 100644 index 00000000..d7733c5f --- /dev/null +++ b/components/wmbus_common/telegram.cpp @@ -0,0 +1,670 @@ +#include "telegram.h" + +#include +#include + +#include "crypto.h" +#include "quirks.h" +#include "walk.h" +#include "esphome/core/helpers.h" + +namespace wmbus { + +struct MediaType { + uint8_t type; + const char *name; +}; + +static constexpr MediaType MEDIA_TYPES[] = { + {0x00, "other"}, + {0x01, "oil"}, + {0x02, "electricity"}, + {0x03, "gas"}, + {0x04, "heat"}, + {0x05, "steam"}, + {0x06, "warm water"}, + {0x07, "water"}, + {0x08, "heat cost allocation"}, + {0x09, "compressed air"}, + {0x0a, "cooling load volume at outlet"}, + {0x0b, "cooling load volume at inlet"}, + {0x0c, "heat volume at inlet"}, + {0x0d, "heat/cooling load"}, + {0x0e, "bus/system component"}, + {0x0f, "unknown"}, + {0x15, "hot water"}, + {0x16, "cold water"}, + {0x17, "hot/cold water"}, + {0x18, "pressure"}, + {0x19, "a/d converter"}, + {0x1a, "smoke detector"}, + {0x1b, "room sensor"}, + {0x1c, "gas detector"}, + {0x20, "breaker"}, + {0x21, "valve"}, + {0x25, "customer unit (display device)"}, + {0x28, "waste water"}, + {0x29, "garbage"}, + {0x36, "radio converter (system side)"}, + {0x37, "radio converter (meter side)"}, +}; + +// Techem numbers its media outside the standard. +static constexpr MediaType TCH_MEDIA_TYPES[] = { + {0x62, "warm water"}, {0x72, "cold water"}, {0x80, "heat cost allocator"}, + {0xc3, "heat"}, {0x43, "heat"}, {0xf0, "smoke detector"}, +}; + +static const char *media_name(std::span table, uint8_t type) { + const auto m = std::ranges::find(table, type, &MediaType::type); + return m == table.end() ? nullptr : m->name; +} + +static const char *media_type_of(uint8_t type, uint16_t mfct) { + if (const char *name = media_name(MEDIA_TYPES, type)) + return name; + if (type >= 0x1d && type <= 0x3f) + return "reserved"; + if (mfct == manufacturer_code('T', 'C', 'H')) { + if (const char *name = media_name(TCH_MEDIA_TYPES, type)) + return name; + } + return "Unknown"; +} + +const Address &Telegram::sender() const { + if (tpl && tpl->address) + return *tpl->address; + if (ell && ell->address) + return *ell->address; + return dll.address; +} + +const Address &Telegram::meter_address() const { return tpl && tpl->address ? *tpl->address : dll.address; } + +const char *Telegram::media_type() const { return media_type_of(sender().type, sender().mfct); } + +static uint32_t id_of(std::span bytes) { + return esphome::encode_uint32(bytes[3], bytes[2], bytes[1], bytes[0]); +} + +Address Address::parse(std::span b) { + return {esphome::encode_uint16(b[1], b[0]), id_of(b.subspan<2, 4>()), b[6], b[7]}; +} + +static std::array bytes_of(const Address &a) { + return {(uint8_t) a.mfct, (uint8_t) (a.mfct >> 8), (uint8_t) a.id, (uint8_t) (a.id >> 8), + (uint8_t) (a.id >> 16), (uint8_t) (a.id >> 24), a.version, a.type}; +} + +enum class CiType : uint8_t { ELL, NWL, AFL, TPL }; + +struct CiField { + uint8_t value; + int8_t length; // -1: ELL V, not supported + CiType type; +}; + +static const CiField CI_FIELDS[] = { + {0x51, 0, CiType::TPL}, {0x72, 0, CiType::TPL}, {0x73, 0, CiType::TPL}, {0x78, 0, CiType::TPL}, + {0x79, 0, CiType::TPL}, {0x7A, 0, CiType::TPL}, {0x7B, 0, CiType::TPL}, {0x81, 0, CiType::NWL}, + {0x8C, 2, CiType::ELL}, {0x8D, 8, CiType::ELL}, {0x8E, 10, CiType::ELL}, {0x8F, 16, CiType::ELL}, + {0x86, -1, CiType::ELL}, {0x90, 10, CiType::AFL}, +}; + +static bool is_ci_of_type(uint8_t ci, CiType type) { + return std::ranges::any_of(CI_FIELDS, [&](const CiField &f) { return f.value == ci && f.type == type; }); +} + +static int ci_length(uint8_t ci) { + const CiField *f = std::ranges::find(CI_FIELDS, ci, &CiField::value); + return f == std::end(CI_FIELDS) ? -2 : f->length; +} + +static bool is_ci_mfct_specific(uint8_t ci) { return ci >= 0xA0 && ci <= 0xB7; } + +// The rest are manufacturer-specific payloads: no records. +static bool carries_records(uint8_t ci) { + return ci == 0x72 || ci == 0x73 || ci == 0x7A || ci == 0x7B || ci == 0x78 || ci == 0x79; +} + +static bool payload_crc_ok(std::span frame, size_t at) { + const uint16_t crc = esphome::encode_uint16(frame[at + 1], frame[at]); + return crc == wmbus::crc16_en13757(frame.subspan(at + 2)); +} + +std::span Telegram::payload() const { + if (frame.size() < (size_t) header_size + suffix_size) + return {}; + return std::span(frame).subspan(header_size, frame.size() - header_size - suffix_size); +} + +std::optional> CompactFormats::lookup(uint16_t signature) const { + const auto signed_as = [signature](std::span format) { + return wmbus::crc16_en13757(format) == signature; + }; + if (const auto f = std::ranges::find_if(registered, signed_as); f != registered.end()) + return *f; + if (const auto f = std::ranges::find_if(learned, signed_as); f != learned.end()) + return std::span(*f); + return {}; +} + +void CompactFormats::learn(std::span bytes) { + if (bytes.empty() || lookup(wmbus::crc16_en13757(bytes))) + return; + + if (learned.size() < LEARNED_FORMATS_KEPT) + learned.emplace_back(); + learned[learned_next].assign(bytes.begin(), bytes.end()); + learned_next = (uint8_t) ((learned_next + 1) % LEARNED_FORMATS_KEPT); +} + +size_t Telegram::left() const { return frame.size() - pos_; } + +std::optional Dll::parse(std::span frame) { + if (frame.size() < SIZE || frame.size() < frame[0]) + return {}; + return Dll{Address::parse(frame.subspan<2, 8>())}; +} + +std::optional Ell::parse(std::span frame, size_t at) { + const uint8_t ci = frame[at]; + const int len = ci_length(ci); + size_t pos = at + 1; + if (len < 0 || frame.size() - pos < (size_t) len) + return {}; + + Ell ell{.cc = frame[pos]}; + pos += 2; // CC and the access counter + + if (ci == 0x8E || ci == 0x8F) { + ell.address = Address::parse(frame.subspan(pos).first<8>()); + pos += 8; + } + if (ci == 0x8D || ci == 0x8F) { + std::copy_n(frame.begin() + pos, ell.session_number.size(), ell.session_number.begin()); + pos += 4; + ell.encrypted = ((id_of(ell.session_number) >> 29) & 0x7) == 1; + // An encrypted payload's CRC is checked after the decryption. + if (!ell.encrypted) { + if (!payload_crc_ok(frame, pos)) + return {}; + pos += 2; + } + } + ell.size = (uint8_t) (pos - at); + return ell; +} + +std::optional Afl::parse(std::span frame, size_t at) { + if (frame.size() - at < (size_t) ci_length(frame[at])) + return {}; + size_t pos = at + 2; // CI and the AFL length + const auto left = [&] { return frame.size() - pos; }; + + const uint16_t fc = esphome::encode_uint16(frame[pos + 1], frame[pos]); + pos += 2; + const bool has_key_info = fc & 0x0200; + const bool has_mac = fc & 0x0400; + const bool has_counter = fc & 0x0800; + const bool has_control = fc & 0x2000; + + Afl afl{}; + if (has_control) { + if (left() == 0) + return {}; + afl.mcl = frame[pos++]; + } + if (has_key_info) { + if (left() < 2) + return {}; + pos += 2; + } + if (has_counter) { + if (left() < 4) + return {}; + std::copy_n(frame.begin() + pos, afl.counter.size(), afl.counter.begin()); + pos += 4; + } + if (has_mac) { + static const int8_t MAC_LEN[] = {0, 0, 0, 2, 4, 8, 12, 16, 12}; + const int at_len = afl.mcl & 0x0f; + const int len = at_len < 9 ? MAC_LEN[at_len] : 0; + if (len == 0 || (int) left() < len) + return {}; + afl.mac.assign(frame.begin() + pos, frame.begin() + pos + len); + pos += len; + } + afl.size = (uint8_t) (pos - at); + return afl; +} + +std::optional Tpl::parse(std::span frame, size_t at) { + Tpl tpl{.start = (uint16_t) at, .ci = frame[at]}; + size_t pos = at + 1; + const auto left = [&] { return frame.size() - pos; }; + + switch (tpl.ci) { + case 0x72: + case 0x73: { + if (left() < 8) + return {}; + // id(4) M(2) version type + const std::span b = frame.subspan(pos).first<8>(); + tpl.address = Address{esphome::encode_uint16(b[5], b[4]), id_of(b.first<4>()), b[6], b[7]}; + pos += 8; + [[fallthrough]]; + } + case 0x7A: + case 0x7B: { + if (left() < 4) + return {}; + tpl.acc = frame[pos++]; + tpl.sts = frame[pos++]; + const uint16_t cfg = esphome::encode_uint16(frame[pos + 1], frame[pos]); + pos += 2; + + tpl.sec_mode = (cfg >> 8) & 0x1f; + if (tpl.sec_mode == TPL_AES_CBC_IV || tpl.sec_mode == TPL_AES_CBC_NO_IV) + tpl.num_encr_blocks = (cfg >> 4) & 0x0f; + + if (tpl.sec_mode == TPL_AES_CBC_NO_IV) { + // Mode 7 extension byte: bits 4-5 select the KDF. + if (left() == 0) + return {}; + tpl.kdf = ((frame[pos++] >> 4) & 3) == 1; + } else if (tpl.sec_mode == TPL_AES_CCM) { + // Mode 10 extension: tag size, KDF, key id, key version flag; then a 4 byte message counter. + if (left() < 2) + return {}; + const uint16_t cfe = esphome::encode_uint16(frame[pos + 1], frame[pos]); + pos += 2; + CcmHeader ccm{}; + ccm.tag_size = (uint8_t) (4 + ((cfe >> 8) & 0x03) * 4); + tpl.kdf = ((cfe >> 4) & 0x03) == 1; + if (((cfe >> 6) & 0x01) != 0) { + if (left() == 0) + return {}; + pos++; // the key version + } + // 0xff in the low cfg byte: everything is encrypted. + if ((cfg & 0xff) != 0xff) + ccm.encrypted_bytes = (uint8_t) (cfg & 0xff); + // The tag authenticates the header from the CI up to here; the counter goes into the nonce. + ccm.aad_len = (uint8_t) (pos - at); + if (left() < 4) + return {}; + std::copy_n(frame.begin() + pos, 4, ccm.counter.begin()); + pos += 4; + tpl.ccm = ccm; + } + break; + } + default: + break; + } + tpl.size = (uint8_t) (pos - at); + return tpl; +} + +bool Telegram::parse_below_ell() { + if (left() > 0 && is_ci_of_type(frame[pos_], CiType::NWL)) { + if (left() < 2) + return false; + pos_ += 2; // CI and the one NWL byte + } + if (left() > 0 && is_ci_of_type(frame[pos_], CiType::AFL)) { + afl = Afl::parse(frame, pos_); + if (!afl) + return false; + pos_ += afl->size; + } + if (left() == 0 || !(is_ci_of_type(frame[pos_], CiType::TPL) || is_ci_mfct_specific(frame[pos_]))) + return false; + tpl = Tpl::parse(frame, pos_); + if (!tpl) + return false; + pos_ += tpl->size; + if (!carries_records(tpl->ci)) + header_size = (uint16_t) pos_; + return true; +} + +bool Telegram::parse_envelope(std::span data) { + frame.assign(data.begin(), data.end()); + std::copy_n(data.begin(), std::min(data.size(), dll_as_received.size()), dll_as_received.begin()); + + quirks::diehl_preprocess(*this); + const std::optional link = Dll::parse(frame); + if (!link) + return false; + dll = *link; + pos_ = Dll::SIZE; + + if (left() == 0) + return false; + if (is_ci_of_type(frame[pos_], CiType::ELL)) { + ell = Ell::parse(frame, pos_); + if (!ell) + return false; + pos_ += ell->size; + // The layers below wait for the key. + if (ell->encrypted) + return true; + } + return parse_below_ell(); +} + +bool Telegram::check_2f2f() { + if (left() < 2) + return false; + const bool ok = frame[pos_] == 0x2f && frame[pos_ + 1] == 0x2f; + const bool permitted = quirk_ == PayloadQuirk::Sanxing609B && quirks::sanxing_609b_ok(frame, pos_); + pos_ += 2; + return ok || permitted; +} + +bool Telegram::potentially_decrypt(Key &key) { + switch (tpl->sec_mode) { + case TPL_NO_SECURITY: + // Unencrypted although a key is configured: rejected, as upstream does. + return !key; + + case TPL_AES_CBC_IV: + if (!key) + quirks::add_default_key(key, *this); + return aes_cbc_iv_decrypt(std::span(frame).subspan(pos_), key) && check_2f2f(); + + case TPL_AES_CBC_NO_IV: { + if (!tpl->kdf || !key || !afl) + return false; + const DerivedKeys derived = derive_keys(*key); + if (!check_mac(derived)) + return false; + wmbus::aes_cbc_decrypt(derived.key, std::array{}, encrypted_blocks(std::span(frame).subspan(pos_))); + return check_2f2f(); + } + + case TPL_AES_CCM: + // Set even without a key: the header alone gives the tag size. + suffix_size = tpl->ccm->tag_size; + return tpl->kdf && key && aes_ccm_decrypt(derive_keys(*key).key); + + default: + // 16-31 are manufacturer-specific; only Diehl's real data is read. + if (tpl->sec_mode >= TPL_SPECIFIC_16_31) + return !quirks::diehl_is_real_data(*this) || quirks::diehl_decrypt_real_data(*this, pos_, key); + return false; + } +} + +bool Telegram::parse_compact_apl(CompactFormats &formats) { + if (left() < 2) + return false; + const uint16_t signature = esphome::encode_uint16(frame[pos_ + 1], frame[pos_]); + pos_ += 2; + + // Unknown until a full frame from this meter has arrived. + const std::optional> format = formats.lookup(signature); + if (!format || left() < 2) + return false; + pos_ += 2; // the crc of the full frame + header_size = (uint16_t) pos_; + read_records(format, formats); + return true; +} + +bool Telegram::decode(Key &key, CompactFormats &formats, std::optional quirk) { + quirk_ = quirk; + if (!tpl) { + if (!key) + return false; + // AES-CTR always "succeeds"; a wrong key yields junk and a bad CRC. + aes_ctr_decrypt(pos_, *key); + if (!payload_crc_ok(frame, pos_)) { + decryption_failed = true; + return false; + } + pos_ += 2; + if (!parse_below_ell()) + return false; + } + if (!carries_records(tpl->ci)) + return true; + + const bool no_header = tpl->ci == 0x78 || tpl->ci == 0x79; + if (!no_header && !potentially_decrypt(key)) { + decryption_failed = true; + return false; + } + + if (tpl->ci == 0x73 || tpl->ci == 0x79 || tpl->ci == 0x7B) + return parse_compact_apl(formats); + header_size = (uint16_t) pos_; + read_records(std::nullopt, formats); + return true; +} + +std::array Telegram::tpl_iv() const { + std::array iv; + std::ranges::copy(bytes_of(meter_address()), iv.begin()); + std::fill(iv.begin() + 8, iv.end(), tpl->acc); + return iv; +} + +std::span Telegram::encrypted_blocks(std::span buffer) const { + size_t num = buffer.size(); + if (tpl->num_encr_blocks) + num = std::min(tpl->num_encr_blocks * 16, num); + return buffer.first(num - num % 16); +} + +bool Telegram::aes_cbc_iv_decrypt(std::span buffer, const Key &key) const { + if (!key) + return false; + const std::span blocks = encrypted_blocks(buffer); + if (blocks.empty()) + return false; + wmbus::aes_cbc_decrypt(*key, tpl_iv(), blocks); + return true; +} + +// The 13 byte mode 10 nonce: M(2) ID(4) Ver(1) Type(1) 00 and the message +// counter, which travels little endian but enters the nonce big endian. +std::array Telegram::ccm_nonce() const { + std::array nonce; + std::ranges::copy(bytes_of(meter_address()), nonce.begin()); + nonce[8] = 0x00; + std::ranges::reverse_copy(tpl->ccm->counter, nonce.begin() + 9); + return nonce; +} + +// RFC 3610's counter block A_i: flags 01, the nonce, i. +static std::array ccm_counter_block(std::span nonce, uint16_t i) { + std::array a{0x01}; + std::ranges::copy(nonce, a.begin() + 1); + a[14] = (uint8_t) (i >> 8); + a[15] = (uint8_t) i; + return a; +} + +// RFC 3610 CBC-MAC with a two byte length field, masked with S0. +std::array Telegram::ccm_tag(std::span key, std::span nonce, + std::span plain) const { + std::array x{}; + size_t n = 0; + const auto absorb = [&](uint8_t b) { + x[n++] ^= b; + if (n < x.size()) + return; + x = wmbus::aes_ecb_encrypt(key, x); + n = 0; + }; + const auto pad = [&]() { + while (n != 0) + absorb(0); + }; + + absorb((uint8_t) (0x40 | ((tpl->ccm->tag_size - 2) / 2) << 3 | 0x01)); + for (uint8_t b : nonce) + absorb(b); + absorb((uint8_t) (plain.size() >> 8)); + absorb((uint8_t) plain.size()); + + const std::span aad = std::span(frame).subspan(tpl->start, tpl->ccm->aad_len); + absorb((uint8_t) (aad.size() >> 8)); + absorb((uint8_t) aad.size()); + for (uint8_t b : aad) + absorb(b); + pad(); + for (uint8_t b : plain) + absorb(b); + pad(); + + xor_into(x, wmbus::aes_ecb_encrypt(key, ccm_counter_block(nonce, 0))); + return x; +} + +// Mode 10 (OMS profile D): AES-CTR, keystream block i = E(key, 01 || nonce || i), i from 1. +bool Telegram::aes_ccm_decrypt(std::span key) { + const std::span rest = std::span(frame).subspan(pos_); + if (rest.size() < tpl->ccm->tag_size) + return false; + const std::span body = rest.first(rest.size() - tpl->ccm->tag_size); + const std::span cipher = + tpl->ccm->encrypted_bytes ? body.first(std::min(*tpl->ccm->encrypted_bytes, body.size())) : body; + + const std::array nonce = ccm_nonce(); + for (size_t off = 0; off < cipher.size(); off += 16) + xor_into(cipher.subspan(off), wmbus::aes_ecb_encrypt(key, ccm_counter_block(nonce, (uint16_t) (off / 16 + 1)))); + + // Mode 10 has no 2f2f: the tag checks both key and integrity. A mismatch is still read, marked. + const std::array computed = ccm_tag(key, nonce, cipher); + if (!std::equal(rest.end() - tpl->ccm->tag_size, rest.end(), computed.begin())) + decoding_errors.add("FAILED_DECODE"); + return true; +} + +void Telegram::aes_ctr_decrypt(size_t pos, std::span key) { + std::array iv{}; + std::ranges::copy(bytes_of(dll.address), iv.begin()); + iv[8] = ell->cc & ~(0x10) & ~(0x02); // without the hop count and repeated access bits + std::ranges::copy(ell->session_number, iv.begin() + 9); + + for (size_t off = pos; off < frame.size(); off += 16) { + xor_into(std::span(frame).subspan(off), wmbus::aes_ecb_encrypt(key, iv)); + for (int p = 15; p >= 0; --p) { + if (++iv[p] != 0) + break; + } + } +} + +bool Telegram::check_mac(const DerivedKeys &derived) const { + if (afl->mac.empty()) + return false; + + std::vector input{afl->mcl}; + input.insert(input.end(), afl->counter.begin(), afl->counter.end()); + input.insert(input.end(), frame.begin() + tpl->start, frame.end()); + + const std::array mac = wmbus::aes_cmac(derived.mac_key, input); + return std::equal(afl->mac.begin(), afl->mac.end(), mac.begin()); +} + +DerivedKeys Telegram::derive_keys(std::span key) const { + std::array input; + int n = 0; + input[n++] = 0x00; // DC 00: the encryption key + // Mode 10 counts its messages in the TPL header, mode 7 in the AFL. + for (uint8_t b : tpl->ccm ? tpl->ccm->counter : afl->counter) + input[n++] = b; + const uint32_t id = meter_address().id; + for (int shift = 0; shift < 32; shift += 8) + input[n++] = (uint8_t) (id >> shift); + for (int i = 0; i < 7; ++i) + input[n++] = 0x07; + + DerivedKeys derived; + derived.key = wmbus::aes_cmac(key, input); + input[0] = 0x01; // DC 01: the MAC key + derived.mac_key = wmbus::aes_cmac(key, input); + return derived; +} + +void Telegram::read_records(std::optional> format, CompactFormats &formats) { + records = parse_records(frame, payload(), format); + if (format) + return; + // Its CRC is the signature later compact frames arrive under; upstream leaves the 0x0f marker out. + std::vector layout; + for (const Record &r : records) { + if (r.dif != 0x0f) + layout.insert(layout.end(), r.id.begin(), r.id.end()); + } + formats.learn(layout); +} + +IxmlEntry &Telegram::ixml_entry_for(std::span key) { + const auto held = + std::ranges::find_if(ixml_entries, [key](const IxmlEntry &e) { return std::ranges::equal(e.id, key); }); + return held != ixml_entries.end() ? *held : ixml_entries.emplace_back(); +} + +bool Telegram::add_ixml_records(int base_offset, std::span bytes, const IxmlGrammar &grammar) { + const std::optional> found = grammar.decode(bytes); + if (!found) + return false; + + for (const IxmlMatch &match : *found) { + // A frame entry under the same key is not looked for here; the walk substitutes it. + IxmlEntry &e = ixml_entry_for(match.key); + e.id = match.key; + e.data = match.data; + e.offset = (uint16_t) (base_offset + (match.data.data() - bytes.data())); + } + return true; +} + +void Status::add(std::string_view text) { + size_t start = 0; + while (start <= text.size()) { + size_t end = text.find(' ', start); + if (end == std::string_view::npos) + end = text.size(); + const std::string_view flag = text.substr(start, end - start); + start = end + 1; + if (flag.empty()) + continue; + if (flag == "OK") { + ok_ = true; + continue; + } + const auto at = std::lower_bound(flags_.begin(), flags_.end(), flag); + if (at == flags_.end() || *at != flag) + flags_.insert(at, std::string(flag)); + } +} + +void Status::add(const Status &other) { + ok_ |= other.ok_; + for (const std::string &flag : other.flags_) + add(flag); +} + +std::string Status::str() const { + if (flags_.empty()) + return ok_ ? "OK" : ""; + std::string result; + for (const std::string &flag : flags_) { + if (!result.empty()) + result += ' '; + result += flag; + } + // Only for em24's deprecated error field. + std::replace(result.begin(), result.end(), '~', ' '); + return result; +} + +} // namespace wmbus diff --git a/components/wmbus_common/telegram.h b/components/wmbus_common/telegram.h new file mode 100644 index 00000000..df7e176b --- /dev/null +++ b/components/wmbus_common/telegram.h @@ -0,0 +1,214 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "ixml.h" +#include "records.h" + +namespace wmbus { + +// The L byte plus the 255 bytes it can count. + +// Per meter; the oldest is overwritten. +constexpr size_t LEARNED_FORMATS_KEPT = 4; + +// EN 13757-7 TPL cfg modes. DES (2, 3) is left out: it needs CONFIG_MBEDTLS_DES_C firmware-wide. +enum TplSecurityMode : uint8_t { + TPL_NO_SECURITY = 0, + TPL_AES_CBC_IV = 5, + TPL_AES_CBC_NO_IV = 7, + TPL_AES_CCM = 10, + TPL_SPECIFIC_16_31 = 16, +}; + +constexpr uint16_t manufacturer_code(char a, char b, char c) { + return (uint16_t) (((a - 64) << 10) | ((b - 64) << 5) | (c - 64)); +} + +struct Telegram; + +using Key = std::optional>; + +// Declared by the driver, plus those learned from this meter's full frames; a compact frame +// names its format by the format's CRC. +struct CompactFormats { + std::span> registered; + std::vector> learned; + uint8_t learned_next = 0; + + std::optional> lookup(uint16_t signature) const; + void learn(std::span bytes); +}; + +// No dif/vif in the frame; `id` points into the grammar's key bytes, `data` into the telegram. +struct IxmlEntry { + std::span id; + uint16_t offset; + std::span data; +}; + +// Sorted, unique flags. "OK" is shown only when no other flag is set. +class Status { + public: + // Space-separated flags. + void add(std::string_view text); + void add(const Status &other); + bool empty() const { return flags_.empty(); } + std::string str() const; + + private: + std::vector flags_; + bool ok_ = false; +}; + +// Mode 10 cfg extension; encrypted_bytes is empty when the whole payload is encrypted. +struct CcmHeader { + std::array counter; + uint8_t tag_size; + uint8_t aad_len; + std::optional encrypted_bytes; +}; + +// Named by a driver's `transform_payload`; see quirks.h. +enum class PayloadQuirk : uint8_t { + DiehlPrios, + Sanxing609B, + TryQundisDecode, +}; + +struct Address { + uint16_t mfct; + uint32_t id; + uint8_t version; + uint8_t type; + + // M(2) id(4) version type: the DLL and ELL order, and the one IVs and nonces take. + static Address parse(std::span bytes); +}; + +// Each layer's parse reads the layer whose CI is at `at`; nullopt when it is malformed. + +struct Dll { + static constexpr size_t SIZE = 10; // L, C, address + Address address; + + static std::optional parse(std::span frame); +}; + +struct Ell { + uint8_t cc; + std::optional
address; // CI 8E and 8F + std::array session_number = {}; + bool encrypted = false; + uint8_t size = 0; // up to the payload, and its CRC unless encrypted + + static std::optional parse(std::span frame, size_t at); +}; + +struct Afl { + uint8_t mcl = 0; + std::array counter = {}; + std::vector mac; + uint8_t size = 0; + + static std::optional parse(std::span frame, size_t at); +}; + +struct Tpl { + uint16_t start; + uint8_t ci; + std::optional
address; // the long header only + uint8_t acc = 0; + uint8_t sts = 0; + uint8_t sec_mode = TPL_NO_SECURITY; + uint8_t num_encr_blocks = 0; + bool kdf = false; // mode 7 and 10 with derived keys + std::optional ccm; // mode 10 only + uint8_t size = 0; + + static std::optional parse(std::span frame, size_t at); +}; + +struct DerivedKeys { + std::array key; + std::array mac_key; +}; + +struct Telegram { + std::vector frame; + // Before diehl_preprocess rearranges it. + std::array dll_as_received = {}; + + Dll dll = {}; + std::optional ell; + std::optional afl; + // Parsed by decode when the ELL is encrypted. + std::optional tpl; + + uint16_t header_size = 0; + uint16_t suffix_size = 0; + + bool decryption_failed = false; + Status decoding_errors; + + std::vector records; + // Only ixml drivers fill them. + std::vector ixml_entries; + + // Payloads outside the frame; one buffer per producer, since ixml entries point into them. + std::vector> derived; + + std::span payload() const; + + // The layers up to and including the addresses. + bool parse_envelope(std::span data); + // `key` may be replaced by a manufacturer default key. + bool decode(Key &key, CompactFormats &formats, std::optional quirk); + + bool add_ixml_records(int base_offset, std::span bytes, const IxmlGrammar &grammar); + + // The deepest layer's address. + const Address &sender() const; + // wmbusmeters naming, e.g. "water". + const char *media_type() const; + + // Mode 5; drivers' transform_payload uses it too. + bool aes_cbc_iv_decrypt(std::span buffer, const Key &key) const; + std::array tpl_iv() const; + + private: + size_t pos_ = 0; + std::optional quirk_; + + // What the TPL encryption binds to: the long header's address, else the DLL's. + const Address &meter_address() const; + + size_t left() const; + bool parse_below_ell(); + bool potentially_decrypt(Key &key); + bool check_2f2f(); + bool parse_compact_apl(CompactFormats &formats); + + // An entry with the same key is overwritten, as upstream does. + IxmlEntry &ixml_entry_for(std::span key); + // Also learns a full frame's layout for later compact frames. + void read_records(std::optional> format, CompactFormats &formats); + + std::array ccm_nonce() const; + std::array ccm_tag(std::span key, std::span nonce, + std::span plain) const; + bool aes_ccm_decrypt(std::span key); + std::span encrypted_blocks(std::span buffer) const; + void aes_ctr_decrypt(size_t pos, std::span key); + bool check_mac(const DerivedKeys &derived) const; + DerivedKeys derive_keys(std::span key) const; +}; + +} // namespace wmbus diff --git a/components/wmbus_common/translatebits.cpp b/components/wmbus_common/translatebits.cpp deleted file mode 100644 index ff82c04c..00000000 --- a/components/wmbus_common/translatebits.cpp +++ /dev/null @@ -1,277 +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"translatebits.h" -#include"util.h" - -#include -#include - -using namespace Translate; - -TriggerBits AlwaysTrigger(~(uint64_t)0); -MaskBits AutoMask(0); - -void handleBitToString(Rule& rule, std::string &out_s, uint64_t bits) -{ - std::string s; - - if (rule.trigger != AlwaysTrigger && (bits & rule.trigger.intValue()) == 0 ) - { - // The trigger bits are needed and there are no trigger bits. Ignore this rule. - return; - } - - uint64_t mask = rule.mask.intValue(); - - if (rule.mask == AutoMask) - { - mask = 0; - for (Map& m : rule.map) - { - // Collect all listed bits as the mask. - mask |= m.from; - } - } - - bits = bits & mask; - for (Map& m : rule.map) - { - if ((~mask & m.from) != 0) - { - // Check that the match rule does not extend outside of the mask! - // If mask is 0xff then a match for 0x100 will trigger this bad warning! - std::string tmp = tostrprintf("BAD_RULE_%s(from=0x%x mask=0x%x)", rule.name.c_str(), m.from, mask); - s += tmp+" "; - } - - uint64_t from = m.from & mask; // Better safe than sorry. - - if (m.test == TestBit::Set) - { - if ((bits & from) != 0 ) - { - s += m.to+" "; - bits = bits & ~m.from; // Remove the handled bit. - } - } - - if (m.test == TestBit::NotSet) - { - if ((bits & from) == 0) - { - s += m.to+" "; - } - else - { - bits = bits & ~m.from; // Remove the handled bit. - } - } - } - if (bits != 0) - { - // Oups, there are set bits that we have not handled.... - std::string tmp; - strprintf(&tmp, "%s_%X", rule.name.c_str(), bits); - s += tmp+" "; - } - - if (s == "") - { - s = rule.default_message.stringValue()+" "; - } - - out_s += s; -} - -void handleIndexToString(Rule& rule, std::string &out_s, uint64_t bits) -{ - std::string s; - - if (rule.trigger != AlwaysTrigger && (bits & rule.trigger.intValue()) == 0 ) - { - // The trigger bits are needed and there are no trigger bits. Ignore this rule. - return; - } - - uint64_t mask = rule.mask.intValue(); - - if (rule.mask == AutoMask) - { - mask = 0; - for (Map& m : rule.map) - { - // Collect all listed bits as the mask. - mask |= m.from; - } - } - - bits = bits & mask; - bool found = false; - for (Map& m : rule.map) - { - assert(m.test == TestBit::Set); - - if ((~mask & m.from) != 0) - { - std::string tmp; - strprintf(&tmp, "BAD_RULE_%s(from=0x%x mask=0x%x)", rule.name.c_str(), m.from, rule.mask); - s += tmp+" "; - } - uint64_t from = m.from & mask; // Better safe than sorry. - if (bits == from) - { - s += m.to+" "; - found = true; - } - } - if (!found) - { - // Oups, this index has not been found. - std::string tmp; - strprintf(&tmp, "%s_%X", rule.name.c_str(), bits); - s += tmp+" "; - } - - out_s += s; -} - -void handleDecimalsToString(Rule& rule, std::string &out_s, uint64_t bits) -{ - std::string s; - - if (rule.trigger != AlwaysTrigger && (bits & rule.trigger.intValue()) == 0 ) - { - // The trigger bits are needed and there are no trigger bits. Ignore this rule. - return; - } - - uint64_t mask = rule.mask.intValue(); - - if (rule.mask == AutoMask) - { - mask = 0; - for (Map& m : rule.map) - { - // Collect all listed bits as the mask. - mask |= m.from; - } - } - - // Switch to signed number here. - int number = bits % mask; - if (number == 0) - { - s += rule.default_message.stringValue()+" "; - } - for (Map& m : rule.map) - { - assert(m.test == TestBit::Set); - - if ((m.from - (m.from % mask)) != 0) - { - std::string tmp; - strprintf(&tmp, "BAD_RULE_%s(from=%d modulomask=%d)", rule.name.c_str(), m.from, rule.mask); - s += tmp+" "; - } - int num = m.from % mask; // Better safe than sorry. - if ((number - num) >= 0) - { - s += m.to+" "; - number -= num; - } - } - if (number > 0) - { - // Oups, this number has not been fully understood. - std::string tmp; - strprintf(&tmp, "%s_%d", rule.name.c_str(), number); - s += tmp+" "; - } - - out_s += s; -} - -void handleRule(Rule& rule, std::string &s, uint64_t bits) -{ - switch (rule.type) - { - case MapType::BitToString: - handleBitToString(rule, s, bits); - break; - - case MapType::IndexToString: - handleIndexToString(rule, s, bits); - break; - - case MapType::DecimalsToString: - handleDecimalsToString(rule, s, bits); - break; - - default: - assert(0); - } -} - -std::string Lookup::translate(uint64_t bits) -{ - std::string total = ""; - - for (Rule& r : rules) - { - std::string s; - handleRule(r, s, bits); - total = joinStatusEmptyStrings(total, s); - } - - while (total.size() > 0 && total.back() == ' ') total.pop_back(); - - return sortStatusString(total); -} - -std::string Lookup::str() -{ - std::string x = " Lookup {\n"; - - for (Rule& r : rules) - { - x += " Rulex {\n"; - x += " name = "+r.name+"\n"; - x += " }\n"; - } - - x += "}\n"; - - return x; -} - -Translate::MapType toMapType(const char *s) -{ - if (!strcmp(s, "BitToString")) return Translate::MapType::BitToString; - if (!strcmp(s, "IndexToString")) return Translate::MapType::IndexToString; - if (!strcmp(s, "DecimalsToString")) return Translate::MapType::DecimalsToString; - return Translate::MapType::Unknown; -} - -Lookup NoLookup = {}; - - -Map m = { 123, "howdy" }; -std::vector vm = { { 123, "howdy" } }; - -Rule r = { "name", Translate::MapType::IndexToString, - AlwaysTrigger, MaskBits(0xe000), "", { } }; diff --git a/components/wmbus_common/translatebits.h b/components/wmbus_common/translatebits.h deleted file mode 100644 index b7841072..00000000 --- a/components/wmbus_common/translatebits.h +++ /dev/null @@ -1,124 +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 . -*/ - -#ifndef TRANSLATEBITS_H -#define TRANSLATEBITS_H - -#include -#include -#include - -#include"util.h" - -struct TriggerBits -{ - TriggerBits() : bits_(0) {} - TriggerBits(uint64_t b) : bits_(b) {} - int intValue() const { return bits_; } - bool operator==(const TriggerBits &tb) const { return bits_ == tb.bits_; } - bool operator!=(const TriggerBits &tb) const { return bits_ != tb.bits_; } - -private: - uint64_t bits_; -}; - -extern TriggerBits AlwaysTrigger; - -struct MaskBits -{ - MaskBits() : bits_(0) {} - MaskBits(uint64_t b) : bits_(b) {} - int intValue() { return bits_; } - bool operator==(const MaskBits &tb) const { return bits_ == tb.bits_; } - bool operator!=(const MaskBits &tb) const { return bits_ != tb.bits_; } - -private: - uint64_t bits_; -}; - -extern MaskBits AutoMask; - -struct DefaultMessage -{ - DefaultMessage() : message_("") {} - DefaultMessage(std::string m) : message_(m) {} - const std::string &stringValue() { return message_; } - bool operator==(const DefaultMessage &dm) const { return message_ == dm.message_; } - bool operator!=(const DefaultMessage &dm) const { return message_ != dm.message_; } - -private: - std::string message_; -}; - -namespace Translate -{ - enum class MapType - { - Unknown, - BitToString, // A bit translates to a text std::string. - IndexToString, // A masked set of bits (a number) translates to a lookup index with text std::strings. - DecimalsToString // Numbers are successively subtracted from input, each successfull subtraction translate into a text std::string. - }; - - struct Map - { - uint64_t from; - std::string to; - TestBit test; - - Map(uint64_t f, std::string t, TestBit b) : from(f), to(t), test(b) {}; - Map(uint64_t f, std::string t) : from(f), to(t), test(TestBit::Set) {}; - }; - - struct Rule - { - std::string name; - MapType type; - TriggerBits trigger; // Bits that must be set. - MaskBits mask; // Bits to be used are set as 1. - DefaultMessage default_message; // If no bits are set print this, typically "OK" or "". - std::vector map; - - Rule() {}; - Rule(std::string n, MapType t, TriggerBits tr, MaskBits mb, std::string dm, std::vector m) - : name(n), type(t), trigger(tr), mask(mb), default_message(dm), map(m) {} - Rule(std::string n, MapType t) : - name(n), type(t), trigger(AlwaysTrigger), mask(AutoMask), default_message(DefaultMessage("")) {} - Rule &set(TriggerBits t) { trigger = t; return *this; } - Rule &set(MaskBits m) { mask = m; return *this; } - Rule &set(DefaultMessage m) { default_message = m; return *this; } - Rule &add(Map m) { map.push_back(m); return *this; } - }; - - struct Lookup - { - std::vector rules; - - std::string translate(uint64_t bits); - bool hasLookups() { return rules.size() > 0; } - - Lookup &add(Rule r) { rules.push_back(r); return *this; } - - std::string str(); - }; -}; - -Translate::MapType toMapType(const char *s); - -extern Translate::Lookup NoLookup; - -#endif diff --git a/components/wmbus_common/units.cpp b/components/wmbus_common/units.cpp deleted file mode 100644 index 5e284fe1..00000000 --- a/components/wmbus_common/units.cpp +++ /dev/null @@ -1,900 +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"units.h" -#include"util.h" -#include -#include -#include -#include - -double from_dbm_to_w(double dbm) { return pow(10.0, dbm/10.0)/1000.0; } -double to_dbm_from_w(double w) { return 10.0*log10(w*1000.0); } - -#define LIST_OF_CONVERSIONS \ - X(Second, Minute, {vto=vfrom/60.0;}) \ - X(Minute, Second, {vto=vfrom*60.0;}) \ - X(Second, Hour, {vto=vfrom/3600.0;}) \ - X(Hour, Second, {vto=vfrom*3600.0;}) \ - X(Year, Second, {vto=vfrom*3600.0*24.0*365.2425;}) \ - X(Second, Year, {vto=vfrom/3600.0/24.0/365.2425;}) \ - X(Minute, Hour, {vto=vfrom/60.0;}) \ - X(Hour, Minute, {vto=vfrom*60.0;}) \ - X(Minute, Year, {vto=vfrom/60.0/24.0/365.2425;}) \ - X(Year, Minute, {vto=vfrom*60.0*24.0*365.2425;}) \ - X(Hour, Year, {vto=vfrom/24.0/365.2425;}) \ - X(Year, Hour, {vto=vfrom*24.0*365.2425;}) \ - X(Hour, Day, {vto=vfrom/24.0;}) \ - X(Day, Hour, {vto=vfrom*24.0;}) \ - X(Day, Year, {vto=vfrom/365.2425;}) \ - X(Year, Day, {vto=vfrom*365.2425;}) \ - X(WH, KWH, {vto=vfrom/1000.0;}) \ - X(KWH, GJ, {vto=vfrom*0.0036;}) \ - X(KWH, MJ, {vto=vfrom*0.0036*1000.0;}) \ - X(GJ, KWH,{vto=vfrom/0.0036;}) \ - X(MJ, GJ, {vto=vfrom/1000.0;}) \ - X(MJ, KWH,{vto=vfrom/1000.0/0.0036;}) \ - X(GJ, MJ, {vto=vfrom*1000.0;}) \ - X(W, KW, {vto=vfrom/1000.0;}) \ - X(JH, W, {vto=vfrom/3600.0;}) \ - X(W, JH, {vto=vfrom*3600.0;}) \ - X(MJH, KW, {vto=vfrom/1000.0/0.0036;}) \ - X(KW, MJH,{vto=vfrom*0.0036*1000.0;}) \ - X(DBM, W,{vto=from_dbm_to_w(vfrom);}) \ - X(W, DBM,{vto=to_dbm_from_w(vfrom);}) \ - X(M3, L, {vto=vfrom*1000.0;}) \ - X(M3H, LH, {vto=vfrom*1000.0;}) \ - X(L, M3, {vto=vfrom/1000.0;}) \ - X(LH, M3H,{vto=vfrom/1000.0;}) \ - X(C, K, {vto=vfrom+273.15;}) \ - X(K, C, {vto=vfrom-273.15;}) \ - X(C, F, {vto=(vfrom*9.0/5.0)+32.0;}) \ - X(F, C, {vto=(vfrom-32)*5.0/9.0;}) \ - X(PA, BAR,{vto=vfrom/100000.0;}) \ - X(BAR, PA, {vto=vfrom*100000.0;}) \ - X(COUNTER, FACTOR,{vto=vfrom;}) \ - X(FACTOR, COUNTER, {vto=vfrom;}) \ - X(COUNTER, NUMBER,{vto=vfrom;}) \ - X(NUMBER, COUNTER, {vto=vfrom;}) \ - X(FACTOR, NUMBER, {vto=vfrom;}) \ - X(NUMBER, FACTOR, {vto=vfrom;}) \ - X(UnixTimestamp,DateTimeLT, {vto=vfrom; }) \ - X(DateTimeLT,UnixTimestamp, {vto=vfrom; }) \ - X(DateLT,UnixTimestamp, {vto=vfrom; }) \ - X(DateTimeLT, DateLT, {vto=vfrom; }) \ - X(DateLT, DateTimeLT, {vto=vfrom; }) \ - X(DEGREE, RADIAN, {vto=vfrom*M_PI/180.0;}) \ - X(RADIAN, DEGREE, {vto=vfrom*180.0/M_PI;}) \ - - -#define LIST_OF_SI_CONVERSIONS \ - X(Second, 1.0, SIExp().s(1)) \ - X(M, 1.0, SIExp().m(1)) \ - X(KG, 1.0, SIExp().kg(1)) \ - X(Ampere, 1.0, SIExp().a(1)) \ - X(K, 1.0, SIExp().k(1)) \ - X(MOL, 1.0, SIExp().mol(1)) \ - X(CD, 1.0, SIExp().cd(1)) \ - \ - X(WH, 3.6e+03, SIExp().kg(1).m(2).s(-2)) \ - X(KWH, 3.6e+06, SIExp().kg(1).m(2).s(-2)) \ - X(MJ, 1.0e+06, SIExp().kg(1).m(2).s(-2)) \ - X(GJ, 1.0e+09, SIExp().kg(1).m(2).s(-2)) \ - X(KVARH, 3.6e+06, SIExp().kg(1).m(2).s(-2)) \ - X(KVAH, 3.6e+06, SIExp().kg(1).m(2).s(-2)) \ - X(M3C, 1.0, SIExp().m(3).c(1)) \ - \ - X(W, 1.0, SIExp().kg(1).m(2).s(-3)) \ - X(KW, 1000.0, SIExp().kg(1).m(2).s(-3)) \ - X(JH, 1.0/3600.0, SIExp().kg(1).m(2).s(-3)) \ - X(MJH, 1000000.0/3600.0, SIExp().kg(1).m(2).s(-3)) \ - X(KVAR, 1000.0, SIExp().kg(1).m(2).s(-3)) \ - X(KVA, 1000.0, SIExp().kg(1).m(2).s(-3)) \ - X(M3CH, 3600.0, SIExp().m(3).c(1).s(-1)) \ - X(DBM, 1.0, /* Nonlinear! */ SIExp().kg(1).m(2).s(-3).nonlinear(from_dbm_to_w, to_dbm_from_w)) \ -\ - X(M3, 1.0, SIExp().m(3)) \ - X(L, 1.0/1000.0, SIExp().m(3)) \ - X(M3H, 3600.0, SIExp().m(3).s(-1)) \ - X(LH, 3.600, SIExp().m(3).s(-1)) \ - \ - X(C, 1.0, SIExp().c(1)) \ - X(F, 1.0, SIExp().f(1)) \ - \ - X(Volt, 1.0, SIExp().kg(1).m(2).s(-3).a(-1)) \ - X(HZ, 1.0, SIExp().s(-1)) \ - X(PA, 1.0, SIExp().kg(1).m(-1).s(-2)) \ - X(BAR, 100000.0, SIExp().kg(1).m(-1).s(-2)) \ - \ - X(Minute, 60.0, SIExp().s(1)) \ - X(Hour, 3600.0, SIExp().s(1)) \ - X(Day, 3600.0*24, SIExp().s(1)) \ - X(Month, 1, SIExp().month(1)) \ - X(Year, 1, SIExp().year(1)) \ - X(UnixTimestamp,1.0, SIExp().unixTimestamp(1)) \ - X(DateTimeUTC, 1.0, SIExp().unixTimestamp(1)) \ - X(DateTimeLT, 1.0, SIExp().unixTimestamp(1)) \ - X(DateLT, 1.0, SIExp().unixTimestamp(1)) \ - X(TimeLT, 1.0, SIExp().unixTimestamp(1)) \ - \ - X(RH, 1.0, SIExp()) \ - X(HCA, 1.0, SIExp()) \ - X(DEGREE, 1.0, SIExp()) \ - X(RADIAN, 180.0/M_PI, SIExp()) \ - X(COUNTER, 1.0, SIExp()) \ - X(FACTOR, 1.0, SIExp()) \ - X(NUMBER, 1.0, SIExp()) \ - X(PERCENTAGE, 0.01, SIExp()) \ - X(PPM, 0.000001, SIExp()) \ - X(TXT, 1.0, SIExp()) \ - - -// 3600.0*24*365.2425 -// 3600.0*24*30.437 - -#define X(cname,lcname,hrname,quantity,explanation) const SIUnit SI_##cname(Unit::cname); -LIST_OF_UNITS -#undef X - -const SIUnit SI_Unknown(Unit::Unknown); - -const SIUnit &toSIUnit(Unit u) -{ - switch (u) - { -#define X(cname,lcname,hrname,quantity,explanation) case Unit::cname: return SI_##cname; -LIST_OF_UNITS -#undef X - default: break; - } - - return SI_Unknown; -} - -bool overrideConversion(Unit from, Unit to) -{ - // The mbus protocol lacks kvarh and kva. Some meters, like the abbb23 uses kwh for kvarh. - // Permit 1 to 1 conversion from kwh to kvarh and kva in extractNumeric. - if (from == Unit::KWH && (to == Unit::KVARH || to == Unit::KVAH)) return true; - return false; -} - -bool canConvert(Unit ufrom, Unit uto) -{ - if (ufrom == uto) return true; -#define X(from,to,code) if (Unit::from == ufrom && Unit::to == uto) return true; -LIST_OF_CONVERSIONS -#undef X - return false; -} - -// This is the old style hardcoded conversions, deprecated in favor of -// the new advanced SIUnit conversions. -double convert(double vfrom, Unit ufrom, Unit uto) -{ - double vto = -4711.0; - if (ufrom == uto) { { vto = vfrom; } return vto; } - -#define X(from,to,code) if (Unit::from == ufrom && Unit::to == uto) { code return vto; } -LIST_OF_CONVERSIONS -#undef X - - std::string from = unitToStringHR(ufrom); - std::string to = unitToStringHR(uto); - - fprintf(stderr, "Cannot convert between units! from %s to %s\n", from.c_str(), to.c_str()); - assert(0); - return 0; -} - -bool isKCF(const SIExp &e) -{ - return - e == SI_K.exp() || - e == SI_C.exp() || - e == SI_F.exp(); -} - -bool is_S_MONTH_YEAR_UT(const SIExp &e) -{ - return - e == SI_Second.exp() || - e == SI_UnixTimestamp.exp() || - e == SI_Month.exp() || - e == SI_Year.exp(); -} - -void getScaleOffset(const SIExp &e, double *scale, double *offset) -{ - if (e == SI_K.exp()) - { - *scale = 1.0; - *offset = 0.0; - return; - } - if (e == SI_C.exp()) - { - *scale = 1.0; - *offset = 273.15; - return; - } - if (e == SI_F.exp()) - { - *scale = 5.0/9.0; - *offset = -32.0+(273.15*9.0/5.0); - return; - } - assert(0); -} - -bool SIUnit::convertTo(double left, const SIUnit &out_siunit, double *out) const -{ - if (exp().equalIgnoreNonLinear(out_siunit.exp())) - { - if (exp().isLinear() && out_siunit.exp().isLinear()) - { - // Its a linear relation, just scale. - if (out != NULL) *out = (left*scale_)/out_siunit.scale_; - return true; - } - - // It's non-linear, use the conversion functions. - // From is is non-null if - NonLinearConversionFunc from_nonlinear_to_linear = exp().nonlinearFrom(); - NonLinearConversionFunc to_nonlinear_from_linear = out_siunit.exp().nonlinearTo(); - - if (from_nonlinear_to_linear) - { - left = from_nonlinear_to_linear(left); - } - double right = (left*scale_)/out_siunit.scale_; - if (to_nonlinear_from_linear) - { - right = to_nonlinear_from_linear(right); - } - - if (out) *out = right; - return right; - } - - // Now the special cases. K-C-F - if (isKCF(exp()) && isKCF(out_siunit.exp())) - { - double from_scale {}; - double from_offset {}; - - getScaleOffset(exp(), &from_scale, &from_offset); - from_scale *= scale(); - - double to_offset {}; - double to_scale {}; - - getScaleOffset(out_siunit.exp(), &to_scale, &to_offset); - to_scale *= out_siunit.scale(); - - if (out != NULL) *out = ((left+from_offset)*from_scale)/to_scale-to_offset; - return true; - } - - if (out != NULL) *out = std::numeric_limits::quiet_NaN(); - return false; -} - -bool forbidden_op(MathOp op, const SIExp &a, const SIExp &b) -{ - // Two unix timestamps cannot be added together. They can be subtracted though! - if (op == MathOp::ADD && a == SI_UnixTimestamp.exp() && b == SI_UnixTimestamp.exp()) return true; - - return false; -} - -double do_op(MathOp op, double left, double right) -{ - if (op == MathOp::ADD) return left+right; - if (op == MathOp::SUB) return left-right; - assert(0); -} - -bool SIUnit::mathOpTo(MathOp op, double left, double right, const SIUnit &right_siunit, SIUnit *out_siunit, double *out) const -{ - // Adding all values with the same units. - if (exp() == right_siunit.exp()) - { - if (forbidden_op(op, exp(), right_siunit.exp())) - { - if (out_siunit != NULL) *out_siunit = SI_COUNTER; - if (out != NULL) *out = std::numeric_limits::quiet_NaN(); - return false; - } - double left_converted {}; - convertTo(left, right_siunit, &left_converted); - double result = do_op(op, left_converted, right); - if (out_siunit != NULL) *out_siunit = right_siunit; - if (out != NULL) *out = result; - return true; - } - - // Adding temperatures. - if (isKCF(exp()) && isKCF(right_siunit.exp())) - { - double left_converted {}; - convertTo(left, right_siunit, &left_converted); - double result = do_op(op, left_converted, right); - if (out_siunit != NULL) *out_siunit = right_siunit; - if (out != NULL) *out = result; - return true; - } - - // Operating on unix timestamps - if (exp() == SI_UnixTimestamp.exp() || right_siunit.exp() == SI_UnixTimestamp.exp()) - { - if (right_siunit.exp() == SI_UnixTimestamp.exp()) - { - // The timestamp is right, flip the arguments. - return right_siunit.mathOpTo(op, right, left, *this, out_siunit, out); - } - assert(exp() == SI_UnixTimestamp.exp() && right_siunit.exp() != SI_UnixTimestamp.exp()); - - // The timestamp is left. Lets handle all permitted additions to UnixTimestamp. - if (right_siunit.exp() == SI_Second.exp()) - { - // Move right argument (day, hour, min, s) to seconds. - double right_converted {}; - right_siunit.convertTo(right, SI_Second, &right_converted); - // Add the seconds to the unix timestamp. - double result = do_op(op, left, right_converted); - if (out_siunit != NULL) *out_siunit = SI_UnixTimestamp; - if (out != NULL) *out = result; - return true; - } - if (right_siunit.exp() == SI_Month.exp()) - { - // Move right argument (day, hour, min, s) to seconds. - if (op == MathOp::SUB) right = -right; - double result = addMonths(left, right); - if (out_siunit != NULL) *out_siunit = SI_UnixTimestamp; - if (out != NULL) *out = result; - return true; - } - } - - // Oups, should not get here.... - return false; -} - -SIUnit SIUnit::mul(const SIUnit &m) const -{ - // Multipliying the SIUnits adds the exponents. - SIExp exps = exponents_.mul(m.exponents_); - - double new_scale = scale_*m.scale_; - SIUnit tmp(Quantity::Unknown, - new_scale, - exps); - - Unit u = tmp.asUnit(Quantity::Unknown); - Quantity q = toQuantity(u); - - return SIUnit(q, new_scale, exps); -} - -SIUnit SIUnit::div(const SIUnit &m) const -{ - // Dividing with a SIUnit subtracts the exponents. - SIExp exps = exponents_.div(m.exponents_); - - double new_scale = scale_/m.scale_; - - SIUnit tmp(Quantity::Unknown, - new_scale, - exps); - - Unit u = tmp.asUnit(Quantity::Unknown); - Quantity q = toQuantity(u); - - return SIUnit(q, new_scale, exps); -} - -SIUnit SIUnit::sqrt() const -{ - // Square rooting SIUnit halfs the exponents. - SIExp exps = exponents_.sqrt(); - - double new_scale = ::sqrt(scale_); - SIUnit tmp(Quantity::Unknown, - new_scale, - exps); - - Unit u = tmp.asUnit(Quantity::Unknown); - Quantity q = toQuantity(u); - - return SIUnit(q, new_scale, exps); -} - -SIUnit whenMultiplied(SIUnit left, SIUnit right) -{ - return Unit::Unknown; -} - -double multiply(double l, SIUnit left, double r, SIUnit right) -{ - return 0; -} - -bool isQuantity(Unit u, Quantity q) -{ -#define X(cname,lcname,hrname,quantity,explanation) if (u == Unit::cname) return Quantity::quantity == q; -LIST_OF_UNITS -#undef X - - return false; -} - -Quantity toQuantity(Unit u) -{ - switch(u) - { -#define X(cname,lcname,hrname,quantity,explanation) case Unit::cname: return Quantity::quantity; -LIST_OF_UNITS -#undef X - default: - break; - } - return Quantity::Unknown; -} - -Quantity toQuantity(std::string q) -{ -#define X(qname,qunit) if (q == #qname) return Quantity::qname; -LIST_OF_QUANTITIES -#undef X - return Quantity::Unknown; -} - -void assertQuantity(Unit u, Quantity q) -{ - if (!isQuantity(u, q)) - { - error("Internal error! Unit is not of this quantity.\n"); - } -} - -Unit defaultUnitForQuantity(Quantity q) -{ -#define X(quantity,default_unit) if (q == Quantity::quantity) return Unit::default_unit; -LIST_OF_QUANTITIES -#undef X - - return Unit::Unknown; -} - -const char *toString(Quantity q) -{ -#define X(quantity,default_unit) if (q == Quantity::quantity) return #quantity; -LIST_OF_QUANTITIES -#undef X - return "?"; -} - - -Unit toUnit(std::string s) -{ -#define X(cname,lcname,hrname,quantity,explanation) if (s == #cname || s == #lcname) return Unit::cname; -LIST_OF_UNITS -#undef X - - return Unit::Unknown; -} - -std::string unitToStringHR(Unit u) -{ -#define X(cname,lcname,hrname,quantity,explanation) if (u == Unit::cname) return hrname; -LIST_OF_UNITS -#undef X - - return "?"; -} - -std::string unitToStringLowerCase(Unit u) -{ -#define X(cname,lcname,hrname,quantity,explanation) if (u == Unit::cname) return #lcname; -LIST_OF_UNITS -#undef X - - return "?"; -} - -std::string unitToStringUpperCase(Unit u) -{ -#define X(cname,lcname,hrname,quantity,explanation) if (u == Unit::cname) return #cname; -LIST_OF_UNITS -#undef X - - return "?"; -} - -std::string strWithUnitHR(double v, Unit u) -{ - std::string r = format3fdot3f(v); - r += " "+unitToStringHR(u); - return r; -} - -std::string strWithUnitLowerCase(double v, Unit u) -{ - std::string r = format3fdot3f(v); - r += " "+unitToStringLowerCase(u); - return r; -} - -std::string valueToString(double v, Unit u) -{ - if (::isnan(v)) - { - return "null"; - } - // This rounds the double value to 6 decimal digits. - // TODO this should be changed to track all double digits available. - std::string s = std::to_string(v); - while (s.size() > 0 && s.back() == '0') s.pop_back(); - if (s.back() == '.') { - s.pop_back(); - if (s.length() == 0) return "0"; - return s; - } - if (s.length() == 0) return "0"; - return s; -} - -bool extractUnit(const std::string &s, std::string *vname, Unit *u) -{ - size_t pos; - std::string vn; - const char *c; - - if (s.length() < 3) goto err; - - pos = s.rfind('_'); - - if (pos == std::string::npos) goto err; - if (pos+1 >= s.length()) goto err; - vn = s.substr(0,pos); - pos++; - - c = s.c_str()+pos; - -#define X(cname,lcname,hrname,quantity,explanation) if (!strcmp(c, #lcname)) { *u = Unit::cname; *vname = vn; return true; } -LIST_OF_UNITS -#undef X - -err: -*vname = ""; -*u = Unit::Unknown; -return false; -} - -SIUnit::SIUnit(Unit u) -{ - quantity_ = toQuantity(u); - - switch (u) - { -#define X(cname,si_scale,si_exponents) \ - case Unit::cname: scale_ = si_scale; exponents_ = si_exponents; break; -LIST_OF_SI_CONVERSIONS -#undef X - default: - quantity_ = Quantity::Unknown; - scale_ = 0; - } -} - -SIUnit::SIUnit(std::string s) -{ -} - -Unit SIUnit::asUnit() const -{ -#define X(cname,si_scale,si_exponents) \ - if ((scale_ == si_scale) && (exponents_ == (si_exponents)) && quantity_ == toQuantity(Unit::cname)) return Unit::cname; -LIST_OF_SI_CONVERSIONS -#undef X - - return Unit::Unknown; -} - -Unit SIUnit::asUnit(Quantity q) const -{ -#define X(cname,si_scale,si_exponents) \ - if ((scale_ == si_scale) && (exponents_ == (si_exponents)) && q == toQuantity(Unit::cname)) return Unit::cname; -LIST_OF_SI_CONVERSIONS -#undef X - - return Unit::Unknown; -} - -std::string super(uchar c) -{ - switch (c) - { - case '-': return "⁻"; - case '+': return "⁺"; - case '0': return "⁰"; - case '1': return "¹"; - case '2': return "²"; - case '3': return "³"; - case '4': return "⁴"; - case '5': return "⁵"; - case '6': return "⁶"; - case '7': return "⁷"; - case '8': return "⁸"; - case '9': return "⁹"; - } - assert(false); - return "?"; -} - -std::string to_superscript(int8_t n) -{ - std::string out; - - char buf[5]; - memset(buf, 0, sizeof(buf)); - snprintf(buf, 5, "%d", n); - - for (int i=0; i<5; ++i) - { - if (buf[i] == 0) break; - out += super(buf[i]); - } - - return out; -} - -std::string to_superscript(std::string &s) -{ - std::string out; - - size_t i = 0; - size_t len = s.length(); - - // Skip non-superscript number. - while (i= '0' && s[i] <= '9'))) - { - out += s[i]; - i++; - } - - while (i 127) invalid_ = true; - return sum; -} - -int8_t SIExp::safe_sub(int8_t a, int8_t b) -{ - int diff = a-b; - if (diff < -128) invalid_ = true; - return diff; -} - -int8_t SIExp::safe_div2(int8_t a) -{ - int8_t d = a/2; - if (d*2 != a) invalid_ = true; - return d; -} - -bool SIExp::operator!=(const SIExp &e) const -{ - return ! (*this == e); -} - -bool SIExp::equalIgnoreNonLinear(const SIExp &e) const -{ - return - s_ == e.s_ && - m_ == e.m_ && - kg_ == e.kg_ && - a_ == e.a_ && - mol_ == e.mol_ && - cd_ == e.cd_ && - k_ == e.k_ && - c_ == e.c_ && - f_ == e.f_ && - month_ == e.month_ && - year_ == e.year_ && - unix_timestamp_ == e.unix_timestamp_; -} - -bool SIExp::operator==(const SIExp &e) const -{ - return equalIgnoreNonLinear(e) && - nonlinear_from_ == e.nonlinear_from_ && - nonlinear_to_ == e.nonlinear_to_; -} - -SIExp SIExp::mul(const SIExp &e) const -{ - SIExp ee; - - ee .s(ee.safe_add(s(),e.s())) - .m(ee.safe_add(m(),e.m())) - .kg(ee.safe_add(kg(),e.kg())) - .a(ee.safe_add(a(),e.a())) - .mol(ee.safe_add(mol(),e.mol())) - .cd(ee.safe_add(cd(),e.cd())) - .k(ee.safe_add(k(),e.k())) - .c(ee.safe_add(c(),e.c())) - .f(ee.safe_add(f(),e.f())) - .month(ee.safe_add(month(),e.month())) - .year(ee.safe_add(year(),e.year())) - .unixTimestamp(ee.safe_add(unixTimestamp(),e.unixTimestamp())); - - return ee; -} - -SIExp SIExp::div(const SIExp &e) const -{ - SIExp ee; - ee .s(ee.safe_sub(s(),e.s())) - .m(ee.safe_sub(m(),e.m())) - .kg(ee.safe_sub(kg(),e.kg())) - .a(ee.safe_sub(a(),e.a())) - .mol(ee.safe_sub(mol(),e.mol())) - .cd(ee.safe_sub(cd(),e.cd())) - .k(ee.safe_sub(k(),e.k())) - .c(ee.safe_sub(c(),e.c())) - .f(ee.safe_sub(f(),e.f())) - .month(ee.safe_sub(month(),e.month())) - .year(ee.safe_sub(year(),e.year())) - .unixTimestamp(ee.safe_sub(unixTimestamp(),e.unixTimestamp())); - - return ee; -} - -SIExp SIExp::sqrt() const -{ - SIExp ee; - - ee .s(ee.safe_div2(s())) - .m(ee.safe_div2(m())) - .kg(ee.safe_div2(kg())) - .a(ee.safe_div2(a())) - .mol(ee.safe_div2(mol())) - .cd(ee.safe_div2(cd())) - .k(ee.safe_div2(k())) - .c(ee.safe_div2(c())) - .f(ee.safe_div2(f())) - .month(ee.safe_div2(month())) - .year(ee.safe_div2(year())) - .unixTimestamp(ee.safe_div2(unixTimestamp())); - - return ee; -} - -#define DO_UNIT_SIEXP(var, name) if (var != 0) { if (r.length()>0) { } r += #name; if (var != 1) { r += to_superscript(var); } } - -std::string SIExp::str() const -{ - std::string r; - - DO_UNIT_SIEXP(mol_, mol); - DO_UNIT_SIEXP(cd_, cd); - DO_UNIT_SIEXP(kg_, kg); - DO_UNIT_SIEXP(m_, m); - DO_UNIT_SIEXP(k_, k); - DO_UNIT_SIEXP(c_, c); - DO_UNIT_SIEXP(f_, f); - DO_UNIT_SIEXP(s_, s); - DO_UNIT_SIEXP(a_, a); - DO_UNIT_SIEXP(month_, month); - DO_UNIT_SIEXP(year_, year); - DO_UNIT_SIEXP(unix_timestamp_, ut); - - if (invalid_) r = "!"+r+"-Invalid!"; - - return r; -} - -char available_quantities_[2048]; - -const char *availableQuantities() -{ - if (available_quantities_[0]) return available_quantities_; - -#define X(q,u) if (Quantity::q != Quantity::Unknown) { \ - strcat(available_quantities_, #q); strcat(available_quantities_, "\n"); \ - assert(strlen(available_quantities_) < 1024); } -LIST_OF_QUANTITIES -#undef X - - // Remove last newline - available_quantities_[strlen(available_quantities_)-1] = 0; - return available_quantities_; -} - -char available_units_[2048]; - -const char *availableUnits() -{ - if (available_units_[0]) return available_units_; - -#define X(n,suffix,sn,q,ln) if (Unit::n != Unit::Unknown) { \ - strcat(available_units_, #suffix); strcat(available_units_, " "); \ - assert(strlen(available_units_) < 1024); } -LIST_OF_UNITS -#undef X - - // Remove last newline - available_units_[strlen(available_units_)-1] = 0; - return available_units_; -} diff --git a/components/wmbus_common/units.h b/components/wmbus_common/units.h deleted file mode 100644 index b4dd827e..00000000 --- a/components/wmbus_common/units.h +++ /dev/null @@ -1,339 +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 . -*/ - -#ifndef UNITS_H -#define UNITS_H - -#include -#include -#include -#include - -// Some toolchains (ESP32-C3) define HZ as a macro; drop it so our enum can use HZ safely. -#ifdef HZ -#undef HZ -#endif - -// A named quantity has a preferred unit, -// ie Volume has m3 (cubic meters) Energy has kwh, Power has kw. -// We search for quantities in the mbus telegrams instead of -// hard coding a specific unit. This is useful since some -// meters can send either mj or kwh depending on their configuration. - -#define LIST_OF_QUANTITIES \ - X(Time,Hour) \ - X(Length,M) \ - X(Mass,KG) \ - X(Amperage,Ampere) \ - X(Temperature,C) \ - X(AmountOfSubstance,MOL) \ - X(LuminousIntensity,CD) \ - \ - X(Energy,KWH) \ - X(Reactive_Energy,KVARH) \ - X(Apparent_Energy,KVAH) \ - X(Power,KW) \ - X(Reactive_Power,KVAR) \ - X(Apparent_Power,KVA) \ - \ - X(Volume,M3) \ - X(Flow,M3H) \ - \ - X(Voltage,Volt) \ - X(Frequency,HZ) \ - X(Pressure,BAR) \ - \ - X(PointInTime,DateTimeLT) \ - \ - X(RelativeHumidity,RH) \ - X(HCA,HCA) \ - X(Text,TXT) \ - X(Angle,DEGREE) \ - X(Dimensionless,COUNTER) \ - -enum class Quantity -{ -#define X(quantity,default_unit) quantity, -LIST_OF_QUANTITIES -#undef X - Unknown -}; - -// A named unit (Unit) is a recurring unit that is useful to represent values -// sent in mbus telegrams. A named unit can be translated into an SIUnit -// which is unnamed and can encode any combination of SI units & scale. -// The SIUnits are used inside formulas. Eventually a successful formula -// calculation will map the final SI Unit to a named unit. -#define LIST_OF_UNITS \ - X(Second,s,"s",Time,"second") \ - X(M,m,"m",Length,"meter") \ - X(KG,kg,"kg",Mass,"kilogram") \ - X(Ampere,a,"A",Amperage,"ampere") \ - X(K,k,"K",Temperature,"kelvin") \ - X(MOL,mol,"mol",AmountOfSubstance,"mole") \ - X(CD,cd,"cd",LuminousIntensity,"candela") \ - \ - X(WH,wh,"Wh",Energy,"Watt hour") \ - X(KWH,kwh,"kWh",Energy,"kilo Watt hour") \ - X(MJ,mj,"MJ",Energy,"Mega Joule") \ - X(GJ,gj,"GJ",Energy,"Giga Joule") \ - X(KVARH,kvarh,"kVARh",Reactive_Energy,"kilo volt amperes reactive hour") \ - X(KVAH,kvah,"kVAh",Apparent_Energy,"kilo volt amperes hour") \ - X(M3C,m3c,"m³°C",Energy,"cubic meter celsius") \ - \ - X(W,w,"W",Power,"Watt") \ - X(KW,kw,"kW",Power,"kilo Watt") \ - X(JH,jh,"J/h",Power,"Joule per hour") \ - X(MJH,mjh,"MJ/h",Power,"mega Joule per hour") \ - X(KVAR,kvar,"kVAR",Reactive_Power,"kilo volt amperes reactive") \ - X(KVA,kva,"kVA",Apparent_Power,"kilo volt amperes") \ - X(M3CH,m3ch,"m³°C/h",Power,"cubic meter celsius per hour") \ - X(DBM,dbm,"dBm",Power,"decibel-milliwatt") \ - \ - X(M3,m3,"m³",Volume,"cubic meter") \ - X(L,l,"l",Volume,"litre") \ - X(M3H,m3h,"m³/h",Flow,"cubic meters per hour") \ - X(LH,lh,"l/h",Flow,"liters per hour") \ - \ - X(C,c,"°C",Temperature,"celsius") \ - X(F,f,"°F",Temperature,"fahrenheit") \ - \ - X(Volt,v,"V",Voltage,"volt") \ - X(HZ,hz,"Hz",Frequency,"hz") \ - X(PA,pa,"pa",Pressure,"pascal") \ - X(BAR,bar,"bar",Pressure,"bar") \ - \ - X(Minute,min,"min",Time,"minute") \ - X(Hour,h,"h",Time,"hour") \ - X(Day,d,"d",Time,"day") \ - X(Month,month,"month",Time,"month") \ - X(Year,y,"y",Time,"year") \ - X(UnixTimestamp,ut,"ut",PointInTime,"unix timestamp") \ - X(DateTimeUTC,utc,"utc",PointInTime,"coordinated universal time") \ - X(DateTimeLT,datetime,"datetime",PointInTime,"local time") \ - X(DateLT,date,"date",PointInTime,"local date") \ - X(TimeLT,time,"time",PointInTime,"local time") \ - \ - X(RH,rh,"RH",RelativeHumidity,"relative humidity") \ - X(HCA,hca,"hca",HCA,"heat cost allocation") \ - X(TXT,txt,"txt",Text,"text") \ - X(DEGREE,deg,"°",Angle,"degree") \ - X(RADIAN,rad,"rad",Angle,"radian") \ - X(COUNTER,counter,"counter",Dimensionless,"counter") \ - X(FACTOR,factor,"factor",Dimensionless,"factor") \ - X(NUMBER,nr,"number",Dimensionless,"number") \ - X(PERCENTAGE,pct,"percentage",Dimensionless,"percentage") \ - X(PPM,ppm,"ppm",Dimensionless,"ppm") \ - -enum class Unit -{ -#define X(cname,lcname,hrname,quantity,explanation) cname, -LIST_OF_UNITS -#undef X - Unknown -}; - -// An SIExp can have a non-linear transform in addition to the scale_. -// If such a function is exists, then it is applied to move the value -// into a scaleable unit. Likewise to move from a scaleable unit into -// the non-linear unit. -typedef double (*NonLinearConversionFunc)(double); - -// The SIUnit is used inside formulas and for conversion between units. -// Any numeric named Unit can be expressed using an SIUnit. -// Most SIUnits do not have a corresponding named Unit. -// However the result of a formula calculation or conversion will -// be eventually converted into a named unit. So even if you -// are allowed to write a formula: 9 kwh * 6 kwh which generates -// the unit kwh² which is not explicitly named above, -// you cannot assign this value to any calculated field since kwh^2 -// is not named. However you can do: sqrt(10 kwh * 10 kwh) which -// will generated an SIUnit which is equivalent to kwh. -// -// Other valid formulas are for example: -// energy_kwh = 100 kw * 22 h -// flow_m3h = 100 m3 / 5 h -// -// We can only track units raised to the power of 127 or -128. -// The struct SIExp below is used to track the exponents of the units. -// -struct SIExp -{ - SIExp &s(int8_t i) { s_ = i; return *this; } - SIExp &m(int8_t i) { m_ = i; return *this; } - SIExp &kg(int8_t i) { kg_ = i; return *this; } - SIExp &a(int8_t i) { a_ = i; return *this; } - SIExp &mol(int8_t i) { mol_ = i; return *this; } - SIExp &cd(int8_t i) { cd_ = i; return *this; } - SIExp &k(int8_t i) { k_ = i; if (k_ != 0 && (c_ != 0 || f_ != 0)) { invalid_ = true; } return *this; } - SIExp &c(int8_t i) { c_ = i; if (c_ != 0 && (k_ != 0 || f_ != 0)) { invalid_ = true; } return *this; } - SIExp &f(int8_t i) { f_ = i; if (f_ != 0 && (k_ != 0 || c_ != 0)) { invalid_ = true; } return *this; } - SIExp &month(int8_t i) { month_ = i; return *this; } - SIExp &year(int8_t i) { year_ = i; return *this; } - SIExp &unixTimestamp(int8_t i) { unix_timestamp_ = i; return *this; } - SIExp &nonlinear(double (*from)(double), double (*to)(double)) { - nonlinear_from_ = from; - nonlinear_to_ = to; - assert(nonlinear_from_ != NULL && nonlinear_to_ != NULL); - return *this; - } - - int8_t s() const { return s_; } - int8_t m() const { return m_; } - int8_t kg() const { return kg_; } - int8_t a() const { return a_; } - int8_t mol() const { return mol_; } - int8_t cd() const { return cd_; } - int8_t k() const { return k_; } - int8_t c() const { return c_; } - int8_t f() const { return f_; } - int8_t month() const { return month_; } - int8_t year() const { return year_; } - int8_t unixTimestamp() const { return unix_timestamp_; } - bool isLinear() const { return nonlinear_to_ == NULL; /* Enough to test one, since the other must be set as well. */ } - NonLinearConversionFunc nonlinearTo() const { return nonlinear_to_; } - NonLinearConversionFunc nonlinearFrom() const { return nonlinear_from_; } - - SIExp mul(const SIExp &e) const; - SIExp div(const SIExp &e) const; - SIExp sqrt() const; - int8_t safe_add(int8_t a, int8_t b); - int8_t safe_sub(int8_t a, int8_t b); - int8_t safe_div2(int8_t a); - bool equalIgnoreNonLinear(const SIExp &e) const; - bool operator==(const SIExp &e) const; - bool operator!=(const SIExp &e) const; - - std::string str() const; - - static SIExp build() { return SIExp(); } - -private: - - int8_t s_ {}; - int8_t m_ {}; - int8_t kg_ {}; - int8_t a_ {}; - int8_t mol_ {}; - int8_t cd_ {}; - int8_t k_ {}; - int8_t c_ {}; // Why c and f here? Because they are offset against K. - int8_t f_ {}; // Since SIUnits are also used for conversions of values, not just unit tracking, - // this means that the offset units (c,f) must be treated as distinct units. - // E.g. if you calculated m3*k (and forget the m3 and k value) then you - // cannot convert this value into m3*c since the offset makes the calculation - // depend on the k value, which we no longer know. - // But kw*h can be translated into kw*s since scaling (*3600) can be done on the - // calculated value without knowing the h value. Therefore we have to - // treat k, c and f as distinct units. I.e. you cannot add m3*k+m3*f+m3*c. - int8_t month_ {}; // Why month and year here instead of using s? Because they vary in length and thus cannot - int8_t year_ {}; // be auto-converted to seconds, ie you cannot add/subtract months/years without knowing what you add to. - int8_t unix_timestamp_ {}; // Why not s? Because point in time is referenced agains an offset. UT is 1970-01-01 01:00. - - // If these are non-NULL, then apply from, to move from the non-linear unit (dBM) to a scaleable unit (W). - // Apply to, to move to the non-linear unit (dBM) from a scaleable unit (W). - NonLinearConversionFunc nonlinear_from_ {}; - NonLinearConversionFunc nonlinear_to_ {}; - - // If exponents have over/underflowed or if multiple of (k,c,f) (month,year,unix_timestamp) are set, - // then the SIExp is not valid any more! - bool invalid_ = false; -}; - -enum class MathOp -{ - ADD, SUB -}; - -struct SIUnit -{ - // Transform a double,double,uint64_t into an SIUnit. - // The exp can be created compile time like this: SIUNIT(3.6E6, 0, SI_KG(1)|SI_M(2)|SI_S(-2)) which is kwh. - SIUnit(Quantity q, double scale, SIExp exponents) : - quantity_(q), scale_(scale), exponents_(exponents) {} - // Transform a named unit into an SIUnit. - SIUnit(Unit); - // Parse std::string like: 3.6E106*kg*m^2*s^-3*a^−1 (ie volt) - SIUnit(std::string s); - // Return the known unit that best matches the SIUnit, or Unit::Unknown if none. - Unit asUnit() const ; - // Return the known unit that best matches the SIUnit and the quantity, or Unit::Unknown if none. - Unit asUnit(Quantity q) const; - // Return the quantity that this unit is used for. - Quantity quantity() const { return quantity_; } - // Get the scale. - double scale() const { return scale_; } - // Get the exponents. - const SIExp &exp() const { return exponents_; } - // Return a std::string like 3.6⁶s⁻²m²kg - std::string str() const; - // Return a detailed std::string like: kwh[3.6⁶s⁻²m²kg]Energy - std::string info() const; - // Check if the exponents (ie units) are the same. - bool sameExponents(SIUnit &to_siunit) const { return exponents_ == to_siunit.exponents_; } - // Convert value from this unit to another unit and store it in out. Return false if conversion is impossible! - bool convertTo(double left, const SIUnit &out_siunit, double *out) const; - // Do a math op. Store the resulting unit and value into the destination pointers. - // Return false if the addion cannot be performed. - bool mathOpTo(MathOp op, double left, double right, const SIUnit &right_siunit, SIUnit *out_siunit, double *out) const; - // Multiply this unit with another unit. - SIUnit mul(const SIUnit &m) const ; - // Dividethis unit with another unit. - SIUnit div(const SIUnit &m) const ; - // Square root this unit. - SIUnit sqrt() const ; - -private: - - Quantity quantity_; - double scale_; - SIExp exponents_; -}; - -bool canConvert(Unit from, Unit to); -double convert(double v, Unit from, Unit to); -Unit whenMultiplied(Unit left, Unit right); -double multiply(double l, Unit left, double r, Unit right); - -// Used to convert protocol KWH to KVARH/KVA, strictly speaking -// not a valid conversion, but permitted to work around limitations in the mbus protocol usage. -bool overrideConversion(Unit from, Unit to); - -// Either uppercase KWH or lowercase kwh works here. -Unit toUnit(std::string s); -Quantity toQuantity(std::string s); -const SIUnit &toSIUnit(Unit u); -const char *toString(Quantity q); -bool isQuantity(Unit u, Quantity q); -Quantity toQuantity(Unit u); -void assertQuantity(Unit u, Quantity q); -Unit defaultUnitForQuantity(Quantity q); -std::string unitToStringHR(Unit u); -std::string unitToStringLowerCase(Unit u); -std::string unitToStringUpperCase(Unit u); -std::string valueToString(double v, Unit u); - -bool extractUnit(const std::string &s, std::string *vname, Unit *u); - -#define X(cname,lcname,hrname,quantity,explanation) extern const SIUnit SI_##cname; -LIST_OF_UNITS -#undef X - -const char *availableQuantities(); -const char *availableUnits(); - -#endif diff --git a/components/wmbus_common/util.cpp b/components/wmbus_common/util.cpp deleted file mode 100644 index d5889cdc..00000000 --- a/components/wmbus_common/util.cpp +++ /dev/null @@ -1,1543 +0,0 @@ -/* - Copyright (C) 2017-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"util.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - - -int char2int(char input) -{ - if(input >= '0' && input <= '9') - return input - '0'; - if(input >= 'A' && input <= 'F') - return input - 'A' + 10; - if(input >= 'a' && input <= 'f') - return input - 'a' + 10; - return -1; -} - -bool isHexChar(uchar c) -{ - return char2int(c) != -1; -} - -// The byte 0x13 i converted into the integer value 13. -uchar bcd2bin(uchar c) -{ - return (c&15)+(c>>4)*10; -} - -// The byte 0x13 is converted into the integer value 31. -uchar revbcd2bin(uchar c) -{ - return (c&15)*10+(c>>4); -} - -uchar reverse(uchar c) -{ - return ((c&15)<<4) | (c>>4); -} - - -bool isHexString(const char* txt, bool *invalid, bool strict) -{ - *invalid = false; - // An empty std::string is not an hex std::string. - if (*txt == 0) return false; - - const char *i = txt; - int n = 0; - for (;;) - { - char c = *i++; - if (!strict && c == '#') continue; // Ignore hashes if not strict - if (!strict && c == ' ') continue; // Ignore hashes if not strict - if (!strict && c == '|') continue; // Ignore hashes if not strict - if (!strict && c == '_') continue; // Ignore underlines if not strict - if (c == 0) break; - n++; - if (char2int(c) == -1) return false; - } - // An empty std::string is not an hex std::string. - if (n == 0) return false; - if (n%2 == 1) *invalid = true; - - return true; -} - -bool isHexStringFlex(const char* txt, bool *invalid) -{ - return isHexString(txt, invalid, false); -} - -bool isHexStringFlex(const std::string &txt, bool *invalid) -{ - return isHexString(txt.c_str(), invalid, false); -} - -bool isHexStringStrict(const char* txt, bool *invalid) -{ - return isHexString(txt, invalid, true); -} - -bool isHexStringStrict(const std::string &txt, bool *invalid) -{ - return isHexString(txt.c_str(), invalid, true); -} - -bool hex2bin(const char* src, std::vector *target) -{ - if (!src) return false; - while(*src && src[1]) { - if (*src == ' ' || *src == '#' || *src == '|' || *src == '_') { - // Ignore space and hashes and pipes and underlines. - src++; - } else { - int hi = char2int(*src); - int lo = char2int(src[1]); - if (hi<0 || lo<0) return false; - target->push_back(hi*16 + lo); - src += 2; - } - } - return true; -} - -bool hex2bin(const std::string &src, std::vector *target) -{ - return hex2bin(src.c_str(), target); -} - -bool hex2bin (std::vector &src, std::vector *target) -{ - if (src.size() % 2 == 1) return false; - for (size_t i=0; ipush_back(hi*16 + lo); - } - } - return true; -} - -char const hexChar[16] = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A','B','C','D','E','F'}; - -std::string bin2hex(const std::vector &target) { - std::string str; - for (size_t i = 0; i < target.size(); ++i) { - const char ch = target[i]; - str.append(&hexChar[(ch & 0xF0) >> 4], 1); - str.append(&hexChar[ch & 0xF], 1); - } - return str; -} - -std::string bin2hex (std::vector::iterator data, std::vector::iterator end, int len) { - std::string str; - while (data != end && len-- > 0) { - const char ch = *data; - data++; - str.append(&hexChar[(ch & 0xF0) >> 4], 1); - str.append(&hexChar[ch & 0xF], 1); - } - return str; -} - -std::string bin2hex (std::vector &data, int offset, int len) { - std::string str; - std::vector::iterator i = data.begin(); - i += offset; - while (i != data.end() && len-- > 0) { - const char ch = *i; - i++; - str.append(&hexChar[(ch & 0xF0) >> 4], 1); - str.append(&hexChar[ch & 0xF], 1); - } - return str; -} - -std::string safeString (std::vector &target) { - std::string str; - for (size_t i = 0; i < target.size(); ++i) { - const char ch = target[i]; - if (ch >= 32 && ch < 127 && ch != '<' && ch != '>') { - str += ch; - } else { - str += '<'; - str.append(&hexChar[(ch & 0xF0) >> 4], 1); - str.append(&hexChar[ch & 0xF], 1); - str += '>'; - } - } - return str; -} - -std::string tostrprintf(const char* fmt, ...) -{ - std::string s; - char buf[4096]; - va_list args; - va_start(args, fmt); - size_t n = vsnprintf(buf, 4096, fmt, args); - assert(n < 4096); - va_end(args); - s = buf; - return s; -} - - -// Why a pointer here? To avoid the compiler warning: -// warning: passing an object of reference type to 'va_start' has undefined behavior [-Wvarargs] -std::string tostrprintf(const std::string *fmt, ...) -{ - std::string s; - char buf[4096]; - va_list args; - va_start(args, fmt); // <<<<< here fmt must be a native type. - size_t n = vsnprintf(buf, 4096, fmt->c_str(), args); - assert(n < 4096); - va_end(args); - s = buf; - return s; -} - -void strprintf(std::string *s, const char* fmt, ...) -{ - char buf[4096]; - va_list args; - va_start(args, fmt); - size_t n = vsnprintf(buf, 4096, fmt, args); - assert(n < 4096); - va_end(args); - *s = buf; -} - -void xorit(uchar *srca, uchar *srcb, uchar *dest, int len) -{ - for (int i=0; i= 0; i--) - { - srcb[i] = srca[i] << 1; - srcb[i] |= overflow; - overflow = (srca[i] & 0x80) >> 7; - } - return; -} - -std::string format3fdot3f(double v) -{ - std::string r; - strprintf(&r, "%3.3f", v); - return r; -} - -bool logging_silenced_ = false; -bool verbose_enabled_ = false; -bool debug_enabled_ = false; -bool trace_enabled_ = false; - - -void silentLogging(bool b) { - logging_silenced_ = b; -} - -const char *version_; - -void setVersion(const char *v) -{ - version_ = v; -} - -const char *getVersion() -{ - return version_; -} - -time_t telegrams_start_time_; - - - - - - - - -bool is_ascii_alnum(char c) -{ - if (c >= 'A' && c <= 'Z') return true; - if (c >= 'a' && c <= 'z') return true; - if (c >= '0' && c <= '9') return true; - if (c == '_') return true; - return false; -} - -bool is_ascii(char c) -{ - if (c >= 'A' && c <= 'Z') return true; - if (c >= 'a' && c <= 'z') return true; - return false; -} - -bool isValidAlias(const std::string& alias) -{ - if (alias.length() == 0) return false; - - if (!is_ascii(alias[0])) return false; - - for (char c : alias) - { - if (!is_ascii_alnum(c)) return false; - } - - return true; -} - -bool isFrequency(const std::string& fq) -{ - int len = fq.length(); - if (len == 0) return false; - if (fq[len-1] != 'M') return false; - len--; - for (int i=0; i= iv) { - int pp = *p; - (*p)++; - if (pp+1 <= 255) { - // Nice, no overflow. We are done here! - break; - } - // Move left add add one. - p--; - } -} - -void debugPayload(const std::string& intro, std::vector &payload) -{ - std::string msg = bin2hex(payload); - debug("%s \"%s\"\n", intro.c_str(), msg.c_str()); -} - -void debugPayload(const std::string& intro, std::vector &payload, std::vector::iterator &pos) -{ - std::string msg = bin2hex(pos, payload.end(), 1024); - debug("%s \"%s\"\n", intro.c_str(), msg.c_str()); -} - -void logTelegram (std::vector &original, std::vector &parsed, int header_size, int suffix_size) -{ - std::vector logged = parsed; - if (!original.empty()) - { - logged = std::vector(parsed); - for (unsigned int i = 0; i < original.size(); i++) - { - logged[i] = original[i]; - } - } - time_t diff = time(NULL)-telegrams_start_time_; - std::string parsed_hex = bin2hex(logged); - std::string header = parsed_hex.substr(0, header_size*2); - std::string content = parsed_hex.substr(header_size*2); - if (suffix_size == 0) - { - notice("telegram=|%s_%s|+%ld\n", - header.c_str(), content.c_str(), diff); - } - else - { - assert((suffix_size*2) < (int)content.size()); - std::string content2 = content.substr(0, content.size()-suffix_size*2); - std::string suffix = content.substr(content.size()-suffix_size*2); - notice("telegram=|%s_%s_%s|+%ld\n", - header.c_str(), content2.c_str(), suffix.c_str(), diff); - } -} - -std::string eatTo (std::vector &v, std::vector::iterator &i, int c, size_t max, bool *eof, bool *err) -{ - std::string s; - - *eof = false; - *err = false; - while (max > 0 && i != v.end() && (c == -1 || *i != c)) - { - s += *i; - i++; - max--; - } - if (c != -1 && i != v.end() && *i != c) - { - *err = true; - } - if (i != v.end()) - { - i++; - } - if (i == v.end()) { - *eof = true; - } - return s; -} - -void padWithZeroesTo (std::vector *content, size_t len, std::vector *full_content) -{ - if (content->size() < len) { - warning("Padded with zeroes.", (int)len); - size_t old_size = content->size(); - content->resize(len); - for(size_t i = old_size; i < len; ++i) { - (*content)[i] = 0; - } - full_content->insert(full_content->end(), content->begin()+old_size, content->end()); - } -} - -static std::string space = " "; -std::string padLeft(const std::string& input, int width) -{ - int w = width-input.size(); - if (w < 0) return input; - assert(w < (int)space.length()); - return space.substr(0, w)+input; -} - -int parseTime(const std::string& s) -{ - std::string time = s; - int mul = 1; - if (time.back() == 'h') { - time.pop_back(); - mul = 3600; - } - if (time.back() == 'm') { - time.pop_back(); - mul = 60; - } - if (time.back() == 's') { - time.pop_back(); - mul = 1; - } - int n = atoi(time.c_str()); - return n*mul; -} - -#define CRC16_EN_13757 0x3D65 - -uint16_t crc16_EN13757_per_byte(uint16_t crc, uchar b) -{ - unsigned char i; - - for (i = 0; i < 8; i++) { - - if (((crc & 0x8000) >> 8) ^ (b & 0x80)){ - crc = (crc << 1) ^ CRC16_EN_13757; - }else{ - crc = (crc << 1); - } - - b <<= 1; - } - - return crc; -} - -uint16_t crc16_EN13757(uchar *data, size_t len) -{ - uint16_t crc = 0x0000; - - assert(len == 0 || data != NULL); - - for (size_t i=0; i> 1) ^ CRC16_POLYNOM; - } - else - crc >>= 1; - byte >>= 1; - } - } - return crc; -} - -bool crc16_CCITT_check(uchar *data, uint16_t length) -{ - uint16_t crc = ~crc16_CCITT(data, length); - return crc == CRC16_GOOD_VALUE; -} - -std::string eatToSkipWhitespace (std::vector &v, std::vector::iterator &i, int c, size_t max, bool *eof, bool *err) -{ - eatWhitespace(v, i, eof); - if (*eof) { - if (c != -1) { - *err = true; - } - return ""; - } - std::string s = eatTo(v,i,c,max,eof,err); - trimWhitespace(&s); - return s; -} - -std::string eatTo (std::vector &v, std::vector::iterator &i, int c, size_t max, bool *eof, bool *err) -{ - std::string s; - - *eof = false; - *err = false; - while (max > 0 && i != v.end() && (c == -1 || *i != c)) - { - s += *i; - i++; - max--; - } - if (c != -1 && (i == v.end() || *i != c)) - { - *err = true; - } - if (i != v.end()) - { - i++; - } - if (i == v.end()) { - *eof = true; - } - return s; -} - -void eatWhitespace (std::vector &v, std::vector::iterator &i, bool *eof) -{ - *eof = false; - while (i != v.end() && (*i == ' ' || *i == '\t')) - { - i++; - } - if (i == v.end()) { - *eof = true; - } -} - -void trimWhitespace(std::string *s) -{ - const char *ws = " \t"; - s->erase(0, s->find_first_not_of(ws)); - s->erase(s->find_last_not_of(ws) + 1); -} - -std::string strdate(struct tm *date) -{ - char buf[256]; - strftime(buf, sizeof(buf), "%Y-%m-%d", date); - return std::string(buf); -} - -std::string strdate(double v) -{ - if (::isnan(v)) return "null"; - struct tm date; - time_t t = v; - localtime_r(&t, &date); - return strdate(&date); -} - -std::string strdatetime(struct tm *datetime) -{ - char buf[256]; - strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M", datetime); - return std::string(buf); -} - -std::string strdatetime(double v) -{ - if (::isnan(v)) return "null"; - struct tm datetime; - time_t t = v; - localtime_r(&t, &datetime); - return strdatetime(&datetime); -} - -std::string strdatetimesec(struct tm *datetime) -{ - char buf[256]; - strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", datetime); - return std::string(buf); -} - -std::string strdatetimesec(double v) -{ - struct tm datetime; - time_t t = v; - localtime_r(&t, &datetime); - return strdatetimesec(&datetime); -} - -bool is_leap_year(int year) -{ - year += 1900; - if (year % 4 != 0) return false; - if (year % 400 == 0) return true; - if (year % 100 == 0) return false; - return true; -} - -int days_in_months[] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; - -int get_days_in_month(int year, int month) -{ - if (month < 0 || month >= 12) - { - month = 0; - } - - int days = days_in_months[month]; - - if (month == 1 && is_leap_year(year)) - { - // Handle february in a leap year. - days += 1; - } - - return days; -} - -double addMonths(double t, int months) -{ - time_t ut = (time_t)t; - struct tm time; - localtime_r(&ut, &time); - addMonths(&time, months); - return (double)mktime(&time); -} - -void addMonths(struct tm *date, int months) -{ - bool is_last_day_in_month = date->tm_mday == get_days_in_month(date->tm_year, date->tm_mon); - - int year = date->tm_year + months / 12; - int month = date->tm_mon + months % 12; - - while (month > 11) - { - year += 1; - month -= 12; - } - - while (month < 0) - { - year -= 1; - month += 12; - } - - int day; - - if (is_last_day_in_month) - { - day = get_days_in_month(year, month); // Last day of month maps to last day of result month - } - else - { - day = std::min(date->tm_mday, get_days_in_month(year, month)); - } - - date->tm_year = year; - date->tm_mon = month; - date->tm_mday = day; -} - -int countSetBits(int v) -{ - int n = 0; - while (v) - { - v &= (v-1); - n++; - } - return n; -} - -bool startsWith(const std::string& s, std::string &prefix) -{ - return startsWith(s, prefix.c_str()); -} - -bool startsWith(const std::string& s, const char *prefix) -{ - size_t len = strlen(prefix); - if (s.length() < len) return false; - if (s.length() == len) return s == prefix; - return !strncmp(&s[0], prefix, len); -} - -std::string makeQuotedJson(const std::string& s) -{ - size_t p = s.find('='); - std::string key, value; - if (p != std::string::npos) - { - key = s.substr(0,p); - value = s.substr(p+1); - } - else - { - key = s; - value = ""; - } - - return std::string("\"")+key+"\":\""+value+"\""; -} - -std::string currentYear() -{ - char datetime[40]; - memset(datetime, 0, sizeof(datetime)); - - struct timeval tv; - gettimeofday(&tv, NULL); - - strftime(datetime, 20, "%Y", localtime(&tv.tv_sec)); - return std::string(datetime); -} - -std::string currentYearMonth() -{ - char datetime[40]; - memset(datetime, 0, sizeof(datetime)); - - struct timeval tv; - gettimeofday(&tv, NULL); - - strftime(datetime, 20, "%Y-%m", localtime(&tv.tv_sec)); - return std::string(datetime); -} - -std::string currentYearMonthDay() -{ - char datetime[40]; - memset(datetime, 0, sizeof(datetime)); - - struct timeval tv; - gettimeofday(&tv, NULL); - - strftime(datetime, 20, "%Y-%m-%d", localtime(&tv.tv_sec)); - return std::string(datetime); -} - -std::string currentHour() -{ - char datetime[40]; - memset(datetime, 0, sizeof(datetime)); - - struct timeval tv; - gettimeofday(&tv, NULL); - - strftime(datetime, 20, "%Y-%m-%d_%H", localtime(&tv.tv_sec)); - return std::string(datetime); -} - -std::string currentMinute() -{ - char datetime[40]; - memset(datetime, 0, sizeof(datetime)); - - struct timeval tv; - gettimeofday(&tv, NULL); - - strftime(datetime, 20, "%Y-%m-%d_%H:%M", localtime(&tv.tv_sec)); - return std::string(datetime); -} - -std::string currentSeconds() -{ - char datetime[40]; - memset(datetime, 0, sizeof(datetime)); - - struct timeval tv; - gettimeofday(&tv, NULL); - - strftime(datetime, 20, "%Y-%m-%d_%H:%M:%S", localtime(&tv.tv_sec)); - return std::string(datetime); -} - -std::string currentMicros() -{ - char datetime[40]; - memset(datetime, 0, sizeof(datetime)); - - struct timeval tv; - gettimeofday(&tv, NULL); - - strftime(datetime, 20, "%Y-%m-%d_%H:%M:%S", localtime(&tv.tv_sec)); - return std::string(datetime)+"."+std::to_string(tv.tv_usec); -} - -bool hasBytes(int n, std::vector::iterator &pos, std::vector &frame) -{ - int remaining = distance(pos, frame.end()); - if (remaining < n) return false; - return true; -} - -bool startsWith(const std::string& s, std::vector &data) -{ - if (s.length() > data.size()) return false; - - for (size_t i=0; itm_wday-1; // tm_wday 0=sun - if (day == -1) day = 6; // adjust so 0=mon and 6=sun - int hour = nowt->tm_hour; // hours since midnight 0-23 - - // Test is inclusive. mon-sun(00-23) will cover whole week all hours. - // mon-tue(00-00) will cover mon and tue one hour after midnight. - if (day >= tp->day_in_week_from && day <= tp->day_in_week_to && hour >= tp->hour_from && hour <= tp->hour_to) - { - return true; - } - return false; -} - -bool extract_times(const char *p, TimePeriod *tp) -{ - if (strlen(p) != 7) return false; // Expect (00-23) - if (p[3] != '-') return false; // Must have - in middle. - int fa = p[1]-48; - if (fa < 0 || fa > 9) return false; - int fb = p[2]-48; - if (fb < 0 || fb > 9) return false; - int ta = p[4]-48; - if (ta < 0 || ta > 9) return false; - int tb = p[5]-48; - if (tb < 0 || tb > 9) return false; - tp->hour_from = fa*10+fb; - tp->hour_to = ta*10+tb; - if (tp->hour_from > 23) return false; // Hours are 00-23 - if (tp->hour_to > 23) return false; // Ditto. - if (tp->hour_to < tp->hour_from) return false; // To must be strictly larger than from, hence the need for 23. - - return true; -} - -int day_name_to_nr(const std::string& name) -{ - if (name == "mon") return 0; - if (name == "tue") return 1; - if (name == "wed") return 2; - if (name == "thu") return 3; - if (name == "fri") return 4; - if (name == "say") return 5; - if (name == "sun") return 6; - return -1; -} - -bool extract_days(char *p, TimePeriod *tp) -{ - if (strlen(p) == 3) - { - std::string s = p; - int d = day_name_to_nr(s); - if (d == -1) return false; - tp->day_in_week_from = d; - tp->day_in_week_to = d; - return true; - } - - if (strlen(p) != 7) return false; // Expect mon-fri - if (p[3] != '-') return false; // Must have - in middle. - std::string from = std::string(p, p+3); - std::string to = std::string(p+4, p+7); - - int f = day_name_to_nr(from); - int t = day_name_to_nr(to); - if (f == -1 || t == -1) return false; - if (f >= t) return false; - tp->day_in_week_from = f; - tp->day_in_week_to = t; - return true; -} - -bool extract_single_period(char *tok, TimePeriod *tp) -{ - // Minimum length is 8 chars, eg "1(00-23)" - size_t len = strlen(tok); - if (len < 8) return false; - // tok is for example: mon-fri(00-23) or tue(18-19) or 1(00-23) - char *p = strchr(tok, '('); - if (p == NULL) return false; // There must be a ( - if (tok[len-1] != ')') return false; // Must end in ) - bool ok = extract_times(p, tp); - if (!ok) return false; - *p = 0; // Terminate in the middle of tok. - ok = extract_days(tok, tp); - if (!ok) return false; - - return true; -} - -bool extract_periods(const std::string& periods, std::vector *period_structs) -{ - if (periods.length() == 0) return false; - - char buf[periods.length()+1]; - strcpy(buf, periods.c_str()); - - char *saveptr {}; - char *tok = strtok_r(buf, ",", &saveptr); - if (tok == NULL) - { - // No comma found. - TimePeriod tp {}; - bool ok = extract_single_period(tok, &tp); - if (!ok) return false; - period_structs->push_back(tp); - return true; - } - - while (tok != NULL) - { - TimePeriod tp {}; - bool ok = extract_single_period(tok, &tp); - if (!ok) return false; - period_structs->push_back(tp); - tok = strtok_r(NULL, ",", &saveptr); - } - - return true; -} - -bool isValidTimePeriod(const std::string& periods) -{ - std::vector period_structs; - bool ok = extract_periods(periods, &period_structs); - return ok; -} - -bool isInsideTimePeriod(time_t now, std::string periods) -{ - struct tm nowt {}; - localtime_r(&now, &nowt); - - std::vector period_structs; - - bool ok = extract_periods(periods, &period_structs); - if (!ok) return false; - - for (auto &tp : period_structs) - { - //debug("period %d %d %d %d\n", tp.day_in_week_from, tp.day_in_week_to, tp.hour_from, tp.hour_to); - if (is_inside(&nowt, &tp)) return true; - } - return false; -} - -size_t memoryUsage() -{ - return 0; -} - -std::vector alarm_shells_; - -const char* toString(Alarm type) -{ - switch (type) - { - case Alarm::DeviceFailure: return "DeviceFailure"; - case Alarm::RegularResetFailure: return "RegularResetFailure"; - case Alarm::DeviceInactivity: return "DeviceInactivity"; - case Alarm::SpecifiedDeviceNotFound: return "SpecifiedDeviceNotFound"; - } - return "?"; -} - - - -void setAlarmShells (std::vector &alarm_shells) -{ - alarm_shells_ = alarm_shells; -} - -bool stringFoundCaseIgnored(const std::string& h, const std::string& n) -{ - std::string haystack = h; - std::string needle = n; - // Modify haystack and needle, in place, to become lowercase. - for_each(haystack.begin(), haystack.end(), [](char & c) { - c = ::tolower(c); - }); - for_each(needle.begin(), needle.end(), [](char & c) { - c = ::tolower(c); - }); - - // Now use default c++ find, return true if needle was found in haystack. - return haystack.find(needle) != std::string::npos; -} - -std::vector splitString(const std::string &s, char c) -{ - auto end = s.cend(); - auto start = end; - - std::vector v; - for (auto i = s.cbegin(); i != end; ++i) - { - if (*i != c) - { - if (start == end) - { - start = i; - } - continue; - } - if (start != end) - { - v.emplace_back(start, i); - start = end; - } - } - if (start != end) - { - v.emplace_back(start, end); - } - return v; -} - -std::set splitStringIntoSet(const std::string &s, char c) -{ - std::vector v = splitString(s, c); - std::set words(v.begin(), v.end()); - return words; -} - -std::vector splitDeviceString(const std::string& ds) -{ - std::string s = ds; - std::string cmd; - - // The CMD(...) might have colons inside. - // Check this first. - size_t p = s.rfind(":CMD("); - if (s.back() == ')' && p != std::string::npos) - { - cmd = s.substr(p+1); - s = s.substr(0,p); - } - - // Now we can split. - std::vector r = splitString(s, ':'); - - if (cmd != "") - { - // Re-append the comand last. - r.push_back(cmd); - } - return r; -} - -uint32_t indexFromRtlSdrName(const std::string& s) -{ - size_t p = s.find('_'); - if (p == std::string::npos) return -1; - std::string n = s.substr(0, p); - return (uint32_t)atoi(n.c_str()); -} - -#define KB 1024ull - -std::string helper(size_t scale, size_t s, std::string suffix) -{ - size_t o = s; - s /= scale; - size_t diff = o-(s*scale); - if (diff == 0) { - return std::to_string(s) + ".00"+suffix; - } - size_t dec = (int)(100*(diff+1) / scale); - return std::to_string(s) + ((dec<10)?".0":".") + std::to_string(dec) + suffix; -} - -std::string humanReadableTwoDecimals(size_t s) -{ - if (s < KB) - { - return std::to_string(s) + " B"; - } - if (s < KB * KB) - { - return helper(KB, s, " KiB"); - } - if (s < KB * KB * KB) - { - return helper(KB*KB, s, " MiB"); - } -#if SIZEOF_SIZE_T == 8 - if (s < KB * KB * KB * KB) - { - return helper(KB*KB*KB, s, " GiB"); - } - if (s < KB * KB * KB * KB * KB) - { - return helper(KB*KB*KB*KB, s, " TiB"); - } - return helper(KB*KB*KB*KB*KB, s, " PiB"); -#else - return helper(KB*KB*KB, s, " GiB"); -#endif -} - -std::string dirname(const std::string& p) -{ - size_t s = p.rfind('/'); - if (s == std::string::npos) return ""; - return p.substr(0, s); -} - -bool parseExtras(const std::string& s, std::map *extras) -{ - std::vector parts = splitString(s, ' '); - - for (auto &p : parts) - { - std::vector kv = splitString(p, '='); - if (kv.size() != 2) return false; - (*extras)[kv[0]] = kv[1]; - } - return true; -} - -bool isValidBps(const std::string& b) -{ - if (b == "300") return true; - if (b == "600") return true; - if (b == "1200") return true; - if (b == "2400") return true; - if (b == "4800") return true; - if (b == "9600") return true; - if (b == "14400") return true; - if (b == "19200") return true; - if (b == "38400") return true; - if (b == "57600") return true; - if (b == "115200") return true; - if (b == "230400") return true; - return false; -} - -bool findBytes (std::vector &v, uchar a, uchar b, uchar c, size_t *out) -{ - size_t p = 0; - - while (p+2 < v.size()) - { - if (v[p+0] == a && - v[p+1] == b && - v[p+2] == c) - { - *out = p; - return true; - } - p++; - } - *out = 999999; - return false; -} - -std::string reverseBCD(const std::string& v) -{ - int vlen = v.length(); - if (vlen % 2 != 0) - { - return "BADHEX:"+v; - } - - std::string n = ""; - for (int i=0; i bytes; - bool ok = hex2bin(v, &bytes); - if (!ok) return "BADHEX:"+v; - reverse(bytes.begin(), bytes.end()); - return safeString(bytes); -} - -#define SLIP_END 0xc0 /* indicates end of packet */ -#define SLIP_ESC 0xdb /* indicates byte stuffing */ -#define SLIP_ESC_END 0xdc /* ESC ESC_END means END data byte */ -#define SLIP_ESC_ESC 0xdd /* ESC ESC_ESC means ESC data byte */ - -void addSlipFraming (std::vector& from, std::vector &to) -{ - to.push_back(SLIP_END); - for (uchar c : from) - { - if (c == SLIP_END) - { - to.push_back(SLIP_ESC); - to.push_back(SLIP_ESC_END); - } - else if (c == SLIP_ESC) - { - to.push_back(SLIP_ESC); - to.push_back(SLIP_ESC_ESC); - } - else - { - to.push_back(c); - } - } - to.push_back(SLIP_END); -} - -void removeSlipFraming (std::vector& from, size_t *frame_length, std::vector &to) -{ - *frame_length = 0; - to.clear(); - to.reserve(from.size()); - bool esc = false; - size_t i; - bool found_end = false; - - for (i = 0; i < from.size(); ++i) - { - uchar c = from[i]; - if (c == SLIP_END) - { - if (to.size() > 0) - { - found_end = true; - i++; - break; - } - } - else if (c == SLIP_ESC) - { - esc = true; - } - else if (esc) - { - esc = false; - if (c == SLIP_ESC_END) to.push_back(SLIP_END); - else if (c == SLIP_ESC_ESC) to.push_back(SLIP_ESC); - else to.push_back(c); // This is an error...... - } - else - { - to.push_back(c); - } - } - - if (found_end) - { - *frame_length = i; - } - else - { - *frame_length = 0; - to.clear(); - } - -} - -// Check if hex std::string is likely to be ascii -bool isLikelyAscii(const std::string& v) -{ - std::vector val; - bool ok = hex2bin(v, &val); - - // For example 64 bits: - // 0000 0000 4142 4344 - // is probably the std::string DCBA - - if (!ok) return false; - - size_t i = 0; - for (; i < val.size(); ++i) - { - if (val[i] != 0) break; - } - - if (i == val.size()) - { - // Value is all zeroes, this is probably a number. - return false; - } - - for (; i < val.size(); ++i) - { - if (val[i] < 20 || val[i] > 126) return false; - } - - return true; -} - -std::string joinStatusOKStrings(const std::string &aa, const std::string &bb) -{ - std::string a = aa; - while (a.length() > 0 && a.back() == ' ') a.pop_back(); - std::string b = bb; - while (b.length() > 0 && b.back() == ' ') b.pop_back(); - - if (a == "" || a == "OK" || a == "null") - { - if (b == "" || b == "null") return "OK"; - return b; - } - if (b == "" || b == "OK" || b == "null") - { - if (a == "" || a == "null") return "OK"; - return a; - } - - return a+" "+b; -} - -std::string joinStatusEmptyStrings(const std::string &aa, const std::string &bb) -{ - std::string a = aa; - while (a.length() > 0 && a.back() == ' ') a.pop_back(); - std::string b = bb; - while (b.length() > 0 && b.back() == ' ') b.pop_back(); - - if (a == "" || a == "null") - { - if (b == "" || b == "null") return ""; - return b; - } - if (b == "" || b == "null") - { - if (a == "" || a == "null") return ""; - return a; - } - - if (a != "OK" && b == "OK") return a; - if (a == "OK" && b != "OK") return b; - if (a == "OK" && b == "OK") return a; - - return a+" "+b; -} - - -std::string sortStatusString(const std::string &a) -{ - std::set flags; - std::string curr; - - for (size_t i=0; i 0) - { - flags.insert(curr); - curr = ""; - } - } - else - { - curr += c; - } - } - - if (curr.length() > 0) - { - flags.insert(curr); - curr = ""; - } - - std::string result; - for (auto &s : flags) - { - result += s+" "; - } - - while (result.size() > 0 && result.back() == ' ') result.pop_back(); - - // This feature is only used for the em24 deprecated backwards compatible error field. - // This should go away in the future. - replace(result.begin(), result.end(), '~', ' '); - - return result; -} - -uchar *safeButUnsafeVectorPtr (std::vector &v) -{ - if (v.size() == 0) return NULL; - return &v[0]; -} - -int strlen_utf8(const char *s) -{ - int len = 0; - for (; *s; ++s) if ((*s & 0xC0) != 0x80) ++len; - return len; -} - -std::string strTimestampUTC(double v) -{ - char datetime[40]; - memset(datetime, 0, sizeof(datetime)); - time_t d = v; - struct tm ts; - gmtime_r(&d, &ts); - strftime(datetime, sizeof(datetime), "%FT%TZ", &ts); - return std::string(datetime); -} - -int toMfctCode(char a, char b, char c) -{ - return ((a-64)*1024+(b-64)*32+(c-64)); -} - -bool is_lowercase_alnum_text(const char *text) -{ - const char *i = text; - while (*i) - { - char c = *i; - if (!((c >= '0' && c <= '9') || - (c >= 'a' && c <= 'z'))) - { - return false; - } - i++; - } - return true; -} - -bool endsWith(const std::string& str, const std::string& suffix) -{ - return str.size() >= suffix.size() && 0 == str.compare(str.size()-suffix.size(), suffix.size(), suffix); -} - -std::string lang_; - -const std::string &language() -{ - if (lang_.length() > 0) return lang_; - - const char *la = getenv("LANG"); - if (!la || strlen(la) < 2) - { - lang_ = "en"; - } - else - { - if (la[2] == '_' || la[2] == 0) - { - lang_ = std::string(la, la+2); - } - else - { - lang_ = "en"; - } - } - - return lang_; -} - -TestBit toTestBit(const char *s) -{ - if (!strcmp(s, "Set")) return TestBit::Set; - if (!strcmp(s, "NotSet")) return TestBit::NotSet; - return TestBit::Unknown; -} diff --git a/components/wmbus_common/util.h b/components/wmbus_common/util.h deleted file mode 100644 index ad454ffa..00000000 --- a/components/wmbus_common/util.h +++ /dev/null @@ -1,262 +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 UTIL_H -#define UTIL_H - -#include -#include -#include -#include -#include -#include -#include - -#include "esphome/core/log.h" - -void setVersion(const char *v); -const char *getVersion(); - -typedef unsigned char uchar; - -enum class TestBit -{ - Unknown, - Set, - NotSet -}; - -uchar bcd2bin(uchar c); -uchar revbcd2bin(uchar c); -uchar reverse(uchar c); -// A BCD std::string 102030405060 is reversed to 605040302010 -std::string reverseBCD(const std::string &v); -// A hex std::string encoding ascii chars is reversed and safely translated into a readble std::string. -std::string reverseBinaryAsciiSafeToString(const std::string &v); -// Check if hex std::string is likely to be ascii -bool isLikelyAscii(const std::string &v); - -bool isHexChar(uchar c); - -// Flex std::strings contain hexadecimal digits and permit # | and whitespace. -bool isHexStringFlex(const char *txt, bool *invalid); -bool isHexStringFlex(const std::string &txt, bool *invalid); -// Strict std::strings contain only hexadecimal digits. -bool isHexStringStrict(const char *txt, bool *invalid); -bool isHexStringStrict(const std::string &txt, bool *invalid); -int char2int(char input); -bool hex2bin(const char *src, std::vector *target); -bool hex2bin(const std::string &src, std::vector *target); -bool hex2bin(std::vector &src, std::vector *target); -std::string bin2hex(const std::vector &target); -std::string bin2hex(std::vector::iterator data, std::vector::iterator end, int len); -std::string bin2hex(std::vector &data, int offset, int len); -std::string safeString(std::vector &target); -void strprintf(std::string *s, const char *fmt, ...); -std::string tostrprintf(const char *fmt, ...); -std::string tostrprintf(const std::string *fmt, ...); -bool endsWith(const std::string &str, const std::string &suffix); - -// Return for example: 2010-03-21 -std::string strdate(struct tm *date); -std::string strdate(double v); -// Return for example: 2010-03-21 15:22 -std::string strdatetime(struct tm *date); -std::string strdatetime(double v); -// Return for example: 2010-03-21 15:22:03 -std::string strdatetimesec(struct tm *date); -std::string strdatetimesec(double v); -// Return UTC timestamp: -std::string strTimestampUTC(double v); -void addMonths(struct tm *date, int m); -double addMonths(double t, int m); - -bool stringFoundCaseIgnored(const std::string &haystack, const std::string &needle); - -void xorit(uchar *srca, uchar *srcb, uchar *dest, int len); -void shiftLeft(uchar *srca, uchar *srcb, int len); -std::string format3fdot3f(double v); - -#define trace(...) //esph_log_vv("wmbusmeters", __VA_ARGS__) -#define verbose(...) //esph_log_v("wmbusmeters", __VA_ARGS__) -#define debug(...) //esph_log_d("wmbusmeters", __VA_ARGS__) -#define notice(...) //esph_log_i("wmbusmeters", __VA_ARGS__) -#define warning(...) //esph_log_w("wmbusmeters", __VA_ARGS__) -#define error(...) //esph_log_e("wmbusmeters", __VA_ARGS__) - -void debugPayload(const std::string &intro, std::vector &payload); -void debugPayload(const std::string &intro, std::vector &payload, std::vector::iterator &pos); -void logTelegram(std::vector &original, std::vector &parsed, int header_size, int suffix_size); - -enum class Alarm -{ - DeviceFailure, - RegularResetFailure, - DeviceInactivity, - SpecifiedDeviceNotFound -}; - -const char *toString(Alarm type); -void setAlarmShells(std::vector &alarm_shells); - -bool isValidAlias(const std::string &alias); - -bool isNumber(const std::string &fq); - -// Split s into std::strings separated by c. -std::vector splitString(const std::string &s, char c); -// Split s into std::strings separated by c and store inte set. -std::set splitStringIntoSet(const std::string &s, char c); -// Split device std::string cul:c1:CMD(bar 1:2) into cul c1 CMD(bar 1:2) -// I.e. the : colon inside CMD is not used for splitting. -std::vector splitDeviceString(const std::string &s); - -void incrementIV(uchar *iv, size_t len); - -std::string eatTo(std::vector &v, std::vector::iterator &i, int c, size_t max, bool *eof, bool *err); - -void padWithZeroesTo(std::vector *content, size_t len, std::vector *full_content); -std::string padLeft(const std::string &input, int width); - -// Parse text std::string into seconds, 5h = (3600*5) 2m = (60*2) 1s = 1 -int parseTime(const std::string &time); - -// Test if current time is inside any of the specified periods. -// For example: mon-sun(00-24) is always true! -// mon-fri(08-20) is true monday to friday from 08.00 to 19.59 -// tue(09-10),sat(00-24) is true tuesday 09.00 to 09.59 and whole of saturday. -bool isInsideTimePeriod(time_t now, std::string periods); -bool isValidTimePeriod(const std::string &periods); - -uint16_t crc16_EN13757(uchar *data, size_t len); - -// This crc is used by im871a for its serial communication. -uint16_t crc16_CCITT(uchar *data, uint16_t length); -bool crc16_CCITT_check(uchar *data, uint16_t length); - -void addSlipFraming(std::vector &from, std::vector &to); -// Frame length is set to zero if no frame was found. -void removeSlipFraming(std::vector &from, size_t *frame_length, std::vector &to); - -// Eat characters from the std::vector v, iterating using i, until the end char c is found. -// If end char == -1, then do not expect any end char, get all until eof. -// If the end char is not found, return error. -// If the maximum length is reached without finding the end char, return error. -std::string eatTo(std::vector &v, std::vector::iterator &i, int c, size_t max, bool *eof, bool *err); -// Eat whitespace (space and tab, not end of lines). -void eatWhitespace(std::vector &v, std::vector::iterator &i, bool *eof); -// First eat whitespace, then start eating until c is found or eof. The found std::string is trimmed from beginning and ending whitespace. -std::string eatToSkipWhitespace(std::vector &v, std::vector::iterator &i, int c, size_t max, bool *eof, bool *err); -// Remove leading and trailing white space -void trimWhitespace(std::string *s); -// Count the number of 1:s in the binary number v. -int countSetBits(int v); - -bool startsWith(const std::string &s, const char *prefix); -bool startsWith(const std::string &s, std::string &prefix); - -// Given alfa=beta it returns "alfa":"beta" -std::string makeQuotedJson(const std::string &s); - -std::string currentYear(); -std::string currentYearMonth(); -std::string currentYearMonthDay(); -std::string currentHour(); -std::string currentMinute(); -std::string currentSeconds(); -std::string currentMicros(); - -#define CHECK(n) \ - if (!hasBytes(n, pos, frame)) \ - return expectedMore(__LINE__); - -bool hasBytes(int n, std::vector::iterator &pos, std::vector &frame); - -bool startsWith(const std::string &s, std::vector &data); - -// Sum the memory used by the heap and stack. -size_t memoryUsage(); - -std::string humanReadableTwoDecimals(size_t s); - -uint32_t indexFromRtlSdrName(const std::string &s); - -bool check_if_rtlwmbus_exists_in_path(); -bool check_if_rtlsdr_exists_in_path(); - -// Return the actual executable binary that is running. -std::string currentProcessExe(); - -std::string dirname(const std::string &p); - -std::string lookForExecutable(const std::string &prog, std::string bin_dir, std::string default_dir); - -// Extract from "ppm=5 radix=7" and put key values into map. -bool parseExtras(const std::string &s, std::map *extras); -void checkIfMultipleWmbusMetersRunning(); - -bool findBytes(std::vector &v, uchar a, uchar b, uchar c, size_t *out); - -enum class OutputFormat -{ - NONE, - PLAIN, - TERMINAL, - JSON, - HTML -}; - -// Joing two status std::strings with a space, but merge OKs. -// I.e. "OK" + "OK" --> "OK" -// "ERROR" + "OK" --> "ERROR" -// "OK" + "ERROR FLOW" --> "ERROR FLOW" -// "ERROR" + "FLOW" --> "ERROR FLOW" -// It also translates empty std::strings into OK. -// "" + "OK" --> "OK" -// "" + "" --> "OK" -std::string joinStatusOKStrings(const std::string &a, const std::string &b); - -// Same but do not introduce OK, keep empty std::strings empty. -std::string joinStatusEmptyStrings(const std::string &a, const std::string &b); - -// Sort the words in a status std::string: "GAMMA BETA ALFA" --> "ALFA BETA GAMMA" -// Also identical flags are merged: "BETA ALFA ALFA" --> "ALFA BETA" -// Finally ~ is replaced with a space, this is only used for backwards compatibilty for deprecated fields. -std::string sortStatusString(const std::string &a); - -// If a std::vector is empty, then there will be an assert (with some compiler flags) if we use &v[0] -// even if we do not intend to actually use the pointer to uchars! -// So provide safeVectorPtr which will return NULL instead of assert-crashing. -uchar *safeButUnsafeVectorPtr(std::vector &v); - -// Count utf8 unicode code points. -int strlen_utf8(const char *s); - -int toMfctCode(char a, char b, char c); - -bool is_lowercase_alnum_text(const char *text); - -// The language that the user expects driver and other messages in. -const std::string &language(); - -TestBit toTestBit(const char *s); - -#ifndef FUZZING -#define FUZZING false -#endif - -#endif diff --git a/components/wmbus_common/walk.cpp b/components/wmbus_common/walk.cpp new file mode 100644 index 00000000..b97d23e9 --- /dev/null +++ b/components/wmbus_common/walk.cpp @@ -0,0 +1,652 @@ +#include "walk.h" + +#include +#include +#include +#include + +namespace wmbus { + +std::vector parse_records(std::span frame, std::span data, + std::optional> format) { + // A full frame carries the dif/vif bytes in its data, so taking from one takes from the other. + const bool shared = !format.has_value(); + std::span layout = shared ? data : *format; + const auto take_layout = [&](size_t n) { + layout = layout.subspan(n); + if (shared) + data = layout; + }; + const auto take_data = [&](size_t n) { + data = data.subspan(n); + if (shared) + layout = data; + }; + const auto offset = [&]() { return (uint16_t) (data.data() - frame.data()); }; + + std::vector records; + while (!layout.empty()) { + const uint8_t dif = layout[0]; + int datalen = dif_len_bytes(dif); + + if (datalen == -2) { + if (dif != 0x0f) + break; + // Manufacturer data to the end of the frame, as a "0F" record. + Record mfct{}; + mfct.id.push_back(0x0F); + mfct.dif = 0x0f; + mfct.vif = 0x7f; + mfct.measurement_type = MeasurementType::Instantaneous; + mfct.offset = offset(); + mfct.slice = data.empty() ? std::span{} : data.subspan(1); + records.push_back(std::move(mfct)); + break; + } + + if (dif == 0x2f) { + take_layout(1); + continue; + } + + const std::optional header = parse_record_header(layout); + if (!header) + break; + Record record{}; + record.set_header(layout, *header); + take_layout(header->length); + + if (data.empty()) + break; + if (datalen == -1) { + datalen = data[0]; + take_data(1); + } + // Upstream leaves the last byte out of a record the frame cut short. + if ((int) data.size() < datalen) + datalen = std::max(0, (int) data.size() - 1); + + record.slice = data.first(datalen); + record.offset = offset(); + take_data(datalen); + records.push_back(std::move(record)); + if (data.empty()) + break; + } + return records; +} + +enum class ProfileMode : uint8_t { Absolute, Increments, Decrements, SignedDifference }; + +enum class ProfileDistance : uint8_t { + NotSpacedInTime, + Seconds, + Minutes, + Hours, + Days, + HalfMonth, + OneMonth, + ThreeMonths, + SixMonths +}; + +struct ProfileHeader { + uint8_t slot_dif_nibble; + ProfileMode mode; + ProfileDistance distance; + bool binary_unsigned; + int spacing_step; + int array_column; +}; + +// The sibling date record a profile counts its slots from. +struct ProfileBaseDate { + uint8_t dif_nibble; + uint8_t vif; + MeasurementType measurement_type; + struct tm date; + int len; +}; + +// The value a profile's increments count from, in its own coding: an 8 digit BCD +// base carries 4 digit BCD increments (OMS Vol.2 Annex G, Table G.4). +struct ProfileBaseValue { + uint8_t dif_nibble; + int len; + int64_t value; +}; + +// TAF7 sends no base date: the base value's actuality duration, one spacing further +// per point, places the points in time instead (OMS Vol.2 Annex R, R.3.2). +struct ProfileAge { + uint32_t base_seconds; + uint32_t spacing_seconds; +}; + +// The VIFEs that mark a compact profile, and the pair (FF 77) that marks an expanded slot. +constexpr uint16_t INVERSE_COMPACT_PROFILE = 0x13; +constexpr uint16_t COMPACT_PROFILE_WITH_REGISTER = 0x1e; +constexpr uint16_t COMPACT_PROFILE = 0x1f; +constexpr uint16_t SYNTHETIC_COMBINABLE = 0x7f77; +constexpr uint16_t DATE_VIF = 0x6c; +// Actuality duration VIFs; a generated one is seconds, 32 bit binary. +constexpr VifSpan ACTUALITY_DURATION{0x74, 0x77}; +constexpr uint8_t ACTUALITY_DURATION_VIF = 0x74; +constexpr uint8_t ACTUALITY_DURATION_DIF_NIBBLE = 0x04; + +static std::optional decode_profile_header(uint8_t spacing_control, uint8_t spacing_value) { + static const ProfileMode MODES[4] = {ProfileMode::Absolute, ProfileMode::Increments, ProfileMode::Decrements, + ProfileMode::SignedDifference}; + static const ProfileDistance UNITS[4] = {ProfileDistance::Seconds, ProfileDistance::Minutes, ProfileDistance::Hours, + ProfileDistance::Days}; + const uint8_t slot_dif_nibble = spacing_control & 0x0f; + const uint8_t increment_bits = (spacing_control >> 6) & 0x03; + const uint8_t spacing_unit = (spacing_control >> 4) & 0x03; + + ProfileHeader h{slot_dif_nibble, + MODES[increment_bits], + ProfileDistance::NotSpacedInTime, + dif_is_binary(slot_dif_nibble) && (increment_bits == 1 || increment_bits == 2), + 0, + 0}; + + if (spacing_value == 0) { + h.array_column = spacing_unit + 1; + return h; + } + if (spacing_value <= 250) { + h.distance = UNITS[spacing_unit]; + h.spacing_step = spacing_value; + return h; + } + if (spacing_value == 253 && spacing_unit == 3) { + h.distance = ProfileDistance::HalfMonth; + h.spacing_step = 1; + return h; + } + if (spacing_value == 254 && spacing_unit != 0) { + h.distance = spacing_unit == 1 ? ProfileDistance::SixMonths + : spacing_unit == 2 ? ProfileDistance::ThreeMonths + : ProfileDistance::OneMonth; + h.spacing_step = spacing_unit == 1 ? 6 : spacing_unit == 2 ? 3 : 1; + return h; + } + return {}; +} + +static std::optional decode_slot_unsigned(uint8_t nibble, std::span bytes) { + if (dif_is_binary(nibble)) { + uint64_t v = 0; + for (size_t i = 0; i < bytes.size(); i++) + v |= ((uint64_t) bytes[i]) << (8 * i); + return v; + } + if (dif_is_bcd(nibble)) { + uint64_t value = 0; + uint64_t mul = 1; + for (size_t i = 0; i < bytes.size(); i++) { + int lo = bytes[i] & 0x0f; + int hi = (bytes[i] >> 4) & 0x0f; + if (lo > 9 || hi > 9) + return {}; + value += (uint64_t) lo * mul; + mul *= 10; + value += (uint64_t) hi * mul; + mul *= 10; + } + return value; + } + return {}; +} + +static bool encode_slot_unsigned(uint8_t nibble, uint64_t value, std::span out) { + if (dif_is_binary(nibble)) { + for (size_t i = 0; i < out.size(); i++) + out[i] = (uint8_t) ((value >> (8 * i)) & 0xff); + return true; + } + if (dif_is_bcd(nibble)) { + for (size_t i = 0; i < out.size(); i++) { + uint8_t lo = value % 10; + value /= 10; + uint8_t hi = value % 10; + value /= 10; + out[i] = (uint8_t) ((hi << 4) | lo); + } + return value == 0; + } + return false; +} + +static std::optional decode_slot_signed(uint8_t nibble, std::span bytes, bool binary_unsigned) { + if (dif_is_bcd(nibble)) { + const std::optional u = decode_slot_unsigned(nibble, bytes); + if (!u || *u > (uint64_t) std::numeric_limits::max()) + return {}; + return (int64_t) *u; + } + if (!dif_is_binary(nibble)) + return {}; + + uint64_t raw = 0; + for (size_t i = 0; i < bytes.size(); i++) + raw |= ((uint64_t) bytes[i]) << (8 * i); + const int bits = (int) bytes.size() * 8; + if (binary_unsigned) { + if (raw > (uint64_t) std::numeric_limits::max()) + return {}; + return (int64_t) raw; + } + if (bits < 64) { + uint64_t sign_mask = (uint64_t) 1 << (bits - 1); + uint64_t value_mask = ((uint64_t) 1 << bits) - 1; + raw &= value_mask; + if (raw & sign_mask) + raw |= ~value_mask; + } + return (int64_t) raw; +} + +static bool encode_slot_signed(uint8_t nibble, int64_t value, bool binary_unsigned, std::span out) { + if (dif_is_bcd(nibble)) { + if (value < 0) + return false; + return encode_slot_unsigned(nibble, (uint64_t) value, out); + } + if (!dif_is_binary(nibble)) + return false; + + const int bits = (int) out.size() * 8; + uint64_t raw = 0; + if (binary_unsigned) { + if (value < 0) + return false; + if (bits < 64 && (uint64_t) value > (((uint64_t) 1 << bits) - 1)) + return false; + raw = (uint64_t) value; + } else { + int64_t min_v, max_v; + if (bits == 64) { + min_v = std::numeric_limits::min(); + max_v = std::numeric_limits::max(); + } else { + min_v = -((int64_t) 1 << (bits - 1)); + max_v = ((int64_t) 1 << (bits - 1)) - 1; + } + if (value < min_v || value > max_v) + return false; + raw = (uint64_t) value; + } + for (size_t i = 0; i < out.size(); i++) + out[i] = (uint8_t) ((raw >> (8 * i)) & 0xff); + return true; +} + +static bool is_signed_binary_illegal_value(std::span bytes) { + if (bytes.empty()) + return false; + return std::all_of(bytes.begin(), bytes.end() - 1, [](uint8_t b) { return b == 0x00; }) && bytes.back() == 0x80; +} + +void Record::set_synthetic_id(uint8_t dif_nibble, int storage_nr, uint8_t vif) { + id.clear(); + int lsb = storage_nr & 1; + int remaining = storage_nr >> 1; + uint8_t first = (dif_nibble & 0x0f) | (lsb ? 0x40 : 0x00); + if (remaining > 0) + first |= 0x80; + id.push_back(first); + while (remaining > 0) { + uint8_t dife = remaining & 0x0f; + remaining >>= 4; + if (remaining > 0) + dife |= 0x80; + id.push_back(dife); + } + id.push_back(vif); + id.push_back(SYNTHETIC_COMBINABLE >> 8); + id.push_back(SYNTHETIC_COMBINABLE & 0xff); + dif = first; +} + +static bool is_profile_marker(uint16_t raw) { + return raw == COMPACT_PROFILE || raw == COMPACT_PROFILE_WITH_REGISTER || raw == INVERSE_COMPACT_PROFILE; +} + +// Profile markers aside; an import and an export profile differ only by BackwardFlow. +static bool same_profile_combinables(const Record &a, const Record &b) { + const auto covered = [](const Record &from, const Record &to) { + return std::all_of(from.combinables.begin(), from.combinables.end(), + [&](uint16_t c) { return is_profile_marker(c) || to.has_combinable(c); }); + }; + return covered(a, b) && covered(b, a); +} + +// So that points of two profiles stay apart for a field matcher. +static void carry_combinables(const Record &entry, Record &out) { + for (uint16_t raw : entry.combinables) { + if (!is_profile_marker(raw)) + out.push_combinable(raw); + } + out.push_combinable(SYNTHETIC_COMBINABLE); +} + +static std::optional find_base_value(const Record &entry, std::span before) { + // Prefer the profile's own combinables, but any base value beats none. + for (const bool require_same_combinables : {true, false}) { + for (const Record &c : before) { + // A variable length record is a profile itself, never a base value. + if ((c.dif & 0x0f) == 0x0d) + continue; + if (require_same_combinables && !same_profile_combinables(c, entry)) + continue; + if (c.storage_nr != entry.storage_nr) + continue; + if ((c.vif & 0xff) != (entry.vif & 0xff)) + continue; + if (c.tariff_nr != entry.tariff_nr) + continue; + if (c.subunit_nr != entry.subunit_nr) + continue; + const int len = dif_len_bytes(c.dif & 0x0f); + if (len <= 0) + continue; + if (const std::optional v = c.extract_long()) + return ProfileBaseValue{(uint8_t) (c.dif & 0x0f), len, (int64_t) *v}; + } + } + return {}; +} + +static std::optional find_base_actuality(const Record &entry, std::span before) { + for (const Record &c : before) { + if (c.storage_nr != entry.storage_nr) + continue; + if (c.tariff_nr != entry.tariff_nr) + continue; + if (c.subunit_nr != entry.subunit_nr) + continue; + if (!ACTUALITY_DURATION.contains(c.vif)) + continue; + if (c.has_combinable(SYNTHETIC_COMBINABLE)) + continue; + // Auto scaling brings seconds, minutes, hours and days alike to hours. + if (const std::optional hours = c.extract_double(true, true)) + return *hours * 3600.0; + } + return {}; +} + +static std::optional find_base_date(const Record &entry, std::span before) { + for (const Record &c : before) { + if (c.storage_nr != entry.storage_nr) + continue; + if (c.tariff_nr != entry.tariff_nr) + continue; + if (c.subunit_nr != entry.subunit_nr) + continue; + if (c.vif != DATE_VIF) + continue; + const DecodedDate d = c.extract_date(); + if (!d.valid) + continue; + const size_t len = c.bytes().size(); + if (len != 2 && len != 4) + continue; + return ProfileBaseDate{(uint8_t) (c.dif & 0x0f), (uint8_t) (c.vif & 0xff), c.measurement_type, d.value, (int) len}; + } + return {}; +} + +// Calendar spacings have no fixed length. +static std::optional spacing_seconds(ProfileDistance distance, int spacing_step) { + if (spacing_step <= 0) + return {}; + switch (distance) { + case ProfileDistance::Seconds: + return (uint32_t) spacing_step; + case ProfileDistance::Minutes: + return (uint32_t) spacing_step * 60; + case ProfileDistance::Hours: + return (uint32_t) spacing_step * 60 * 60; + case ProfileDistance::Days: + return (uint32_t) spacing_step * 24 * 60 * 60; + default: + return {}; + } +} + +static bool step_profile_date(struct tm &date, ProfileDistance distance, int spacing_step, bool inverse) { + const int direction = inverse ? -1 : 1; + if (distance == ProfileDistance::OneMonth || distance == ProfileDistance::ThreeMonths || + distance == ProfileDistance::SixMonths) { + add_months(date, direction * std::max(spacing_step, 1)); + return true; + } + if (distance == ProfileDistance::HalfMonth) { + // The boundaries are the 1st and the 16th. + if (inverse) { + if (date.tm_mday >= 16) { + date.tm_mday = 1; + } else { + add_months(date, -1); + date.tm_mday = 16; + } + } else if (date.tm_mday <= 15) { + date.tm_mday = 16; + } else { + add_months(date, 1); + date.tm_mday = 1; + } + return true; + } + + int64_t step_seconds = 0; + if (distance == ProfileDistance::Seconds) + step_seconds = spacing_step; + else if (distance == ProfileDistance::Minutes) + step_seconds = (int64_t) spacing_step * 60; + else if (distance == ProfileDistance::Hours) + step_seconds = (int64_t) spacing_step * 60 * 60; + else if (distance == ProfileDistance::Days) + step_seconds = (int64_t) spacing_step * 24 * 60 * 60; + if (step_seconds <= 0) + return false; + + const time_t ts = mktime(&date); + if (ts == (time_t) -1) + return false; + const time_t shifted = (time_t) ((int64_t) ts + direction * step_seconds); + return localtime_r(&shifted, &date) != nullptr; +} + +static bool encode_date_bytes(const struct tm &date, std::span out) { + int year = date.tm_year + 1900; + int month = date.tm_mon + 1; + int day = date.tm_mday; + if (year < 2000 || year > 2127) + return false; + if (month < 1 || month > 12) + return false; + if (day < 1 || day > 31) + return false; + + int y = year - 2000; + uint8_t lo_date = (uint8_t) ((day & 0x1f) | ((y & 0x07) << 5)); + uint8_t hi_date = (uint8_t) ((month & 0x0f) | ((y & 0x78) << 1)); + + if (out.size() == 2) { + out[0] = lo_date; + out[1] = hi_date; + return true; + } + if (out.size() == 4) { + if (date.tm_hour < 0 || date.tm_hour > 23) + return false; + if (date.tm_min < 0 || date.tm_min > 59) + return false; + out[0] = (uint8_t) (date.tm_min & 0x3f); + out[1] = (uint8_t) (date.tm_hour & 0x1f); + out[2] = lo_date; + out[3] = hi_date; + return true; + } + return false; +} + +// Each point is followed by a trailer placing it in time: its date, or its age. +static void expand_profile(const Record &entry, std::span before, + const std::function &visit) { + const bool with_register = entry.has_combinable(COMPACT_PROFILE_WITH_REGISTER); + const bool inverse = entry.has_combinable(INVERSE_COMPACT_PROFILE); + if (!with_register && !inverse && !entry.has_combinable(COMPACT_PROFILE)) + return; + + const std::span bytes = entry.bytes(); + if (bytes.size() < 2) + return; + const std::optional header = decode_profile_header(bytes[0], bytes[1]); + if (!header) + return; + const ProfileHeader &h = *header; + const int slot_bytes = dif_len_bytes(h.slot_dif_nibble); + if (slot_bytes <= 0) + return; + const size_t step = (size_t) slot_bytes; + if ((bytes.size() - 2) % step != 0) + return; + + std::optional base_value = find_base_value(entry, before); + std::optional base = find_base_date(entry, before); + std::optional age; + if (!base) { + const std::optional spacing = spacing_seconds(h.distance, h.spacing_step); + const std::optional base_seconds = spacing ? find_base_actuality(entry, before) : std::nullopt; + if (base_seconds) + age = ProfileAge{(uint32_t) std::llround(*base_seconds), *spacing}; + } + + const bool incremental = h.mode != ProfileMode::Absolute; + // A reading is written in the base value's coding, not the increment's: 1 byte + // increments on a 1995 Wh base produce readings that no longer fit in a byte. + const bool reconstruct = inverse && incremental && base_value.has_value(); + const uint8_t value_dif_nibble = reconstruct ? base_value->dif_nibble : h.slot_dif_nibble; + int subunit_nr = entry.subunit_nr; + if (h.distance == ProfileDistance::NotSpacedInTime && h.array_column > 0) + subunit_nr += h.array_column - 1; + + // Inverse and with-register profiles read their slots forwards, the others backwards. + const bool forwards = inverse || with_register; + const size_t slots = (bytes.size() - 2) / step; + int index = 0; + for (size_t i = 0; i < slots; ++i) { + const std::span slot = bytes.subspan(2 + (forwards ? i : slots - 1 - i) * step, step); + if (all_ff(slot)) { + // In the incremental modes an empty slot ends the profile. + if (incremental) + break; + continue; + } + if (incremental && dif_is_binary(h.slot_dif_nibble) && !h.binary_unsigned && is_signed_binary_illegal_value(slot)) + break; + + const int storage_nr = entry.storage_nr + 1 + index; + Record point{}; + point.set_synthetic_id(value_dif_nibble, storage_nr, (uint8_t) (entry.vif & 0xff)); + point.measurement_type = entry.measurement_type; + point.vif = entry.vif; + point.storage_nr = storage_nr; + point.tariff_nr = entry.tariff_nr; + point.subunit_nr = subunit_nr; + point.offset = entry.offset; + carry_combinables(entry, point); + point.slice = slot; + + if (reconstruct) { + const std::optional delta = decode_slot_signed(h.slot_dif_nibble, slot, h.binary_unsigned); + if (!delta) + break; + const int64_t absolute = + h.mode == ProfileMode::Decrements ? base_value->value + *delta : base_value->value - *delta; + + // No dif data field is longer than this, so the value always fits. + std::array encoded; + const size_t len = (size_t) base_value->len; + if (!encode_slot_signed(value_dif_nibble, absolute, false, std::span(encoded).first(len))) + break; + point.computed.assign(encoded.begin(), encoded.begin() + len); + base_value->value = absolute; + } + visit(point); + + const int trailer_index = index++; + Record trailer{}; + trailer.storage_nr = storage_nr; + trailer.tariff_nr = entry.tariff_nr; + trailer.subunit_nr = subunit_nr; + trailer.offset = entry.offset; + if (base) { + // Tried once per point, whether the next date encodes or not. + struct tm next_date = base->date; + std::array date_bytes; + if (!step_profile_date(next_date, h.distance, h.spacing_step, inverse) || + !encode_date_bytes(next_date, std::span(date_bytes).first((size_t) base->len))) + continue; + base->date = next_date; + trailer.set_synthetic_id(base->dif_nibble, storage_nr, base->vif); + trailer.measurement_type = base->measurement_type; + trailer.vif = base->vif; + trailer.push_combinable(SYNTHETIC_COMBINABLE); + trailer.computed.assign(date_bytes.begin(), date_bytes.begin() + base->len); + visit(trailer); + } else if (age) { + const uint32_t seconds = age->base_seconds + age->spacing_seconds * (uint32_t) (trailer_index + 1); + trailer.set_synthetic_id(ACTUALITY_DURATION_DIF_NIBBLE, storage_nr, ACTUALITY_DURATION_VIF); + trailer.measurement_type = entry.measurement_type; + trailer.vif = ACTUALITY_DURATION_VIF; + carry_combinables(entry, trailer); + trailer.computed = {(uint8_t) (seconds & 0xff), (uint8_t) ((seconds >> 8) & 0xff), + (uint8_t) ((seconds >> 16) & 0xff), (uint8_t) ((seconds >> 24) & 0xff)}; + visit(trailer); + } + } +} + +static Record record_of(const IxmlEntry &entry) { + Record r{}; + if (const std::optional header = parse_record_header(entry.id)) + r.set_header(entry.id, *header); + r.slice = entry.data; + r.offset = entry.offset; + return r; +} + +void for_each_record(const Telegram &t, const std::function &visit) { + // Entries already visited in place of a frame record, so the tail skips them. + std::vector taken; + const auto untaken = [&](size_t i) { return i >= taken.size() || !taken[i]; }; + + for (size_t k = 0; k < t.records.size(); ++k) { + const Record &frame_record = t.records[k]; + taken.resize(t.ixml_entries.size(), false); + // An ixml entry under the same key takes the frame record's place. + size_t replacement = 0; + while (replacement < t.ixml_entries.size() && + !(untaken(replacement) && frame_record.same_id(t.ixml_entries[replacement].id))) + ++replacement; + if (replacement < t.ixml_entries.size()) { + taken[replacement] = true; + visit(record_of(t.ixml_entries[replacement])); + } else { + visit(frame_record); + } + expand_profile(frame_record, std::span(t.records).first(k + 1), visit); + } + + for (size_t i = 0; i < t.ixml_entries.size(); ++i) { + if (untaken(i)) + visit(record_of(t.ixml_entries[i])); + } +} + +} // namespace wmbus diff --git a/components/wmbus_common/walk.h b/components/wmbus_common/walk.h new file mode 100644 index 00000000..9aad837d --- /dev/null +++ b/components/wmbus_common/walk.h @@ -0,0 +1,23 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include "records.h" +#include "telegram.h" + +namespace wmbus { + +// A compact frame takes the dif/vif bytes from `format`, a full frame from `data`. +std::vector parse_records(std::span frame, std::span data, + std::optional> format); + +// In upstream's order: each frame record (or the ixml entry replacing it) and its +// profile's points, then the remaining ixml entries, including ones added during +// the walk. A profile's point lives for its visit only. +void for_each_record(const Telegram &t, const std::function &visit); + +} // namespace wmbus diff --git a/components/wmbus_common/wmbus.cpp b/components/wmbus_common/wmbus.cpp deleted file mode 100644 index 49dc995f..00000000 --- a/components/wmbus_common/wmbus.cpp +++ /dev/null @@ -1,4736 +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"aescmac.h" -#include"wmbus.h" -#include"wmbus_utils.h" -#include"dvparser.h" -#include"manufacturer_specificities.h" -#include -#include -#include -#include -#include -#include -#include - -#include -#include - -struct LinkModeInfo -{ - LinkMode mode; - const char *name; - const char *lcname; - const char *option; - uint64_t val; -}; - -LinkModeInfo link_modes_[] = { -#define X(name,lcname,option,val) { LinkMode::name, #name , #lcname, #option, val }, -LIST_OF_LINK_MODES -#undef X -}; - -const char *toString(LinkMode lm) -{ -#define X(name,lcname,option,val) if (lm == LinkMode::name) return #lcname; -LIST_OF_LINK_MODES -#undef X - - return "unknown"; -} - -LinkModeInfo *getLinkModeInfo(LinkMode lm); -LinkModeInfo *getLinkModeInfoFromBit(int bit); - -LinkModeInfo *getLinkModeInfo(LinkMode lm) -{ - for (auto& s : link_modes_) - { - if (s.mode == lm) - { - return &s; - } - } - assert(0); - return NULL; -} - -LinkModeInfo *getLinkModeInfoFromBit(uint64_t bit) -{ - for (auto& s : link_modes_) - { - if (s.val == bit) - { - return &s; - } - } - assert(0); - return NULL; -} - -LinkMode isLinkModeOption(const char *arg) -{ - for (auto& s : link_modes_) { - if (!strcmp(arg, s.option)) { - return s.mode; - } - } - return LinkMode::UNKNOWN; -} - -LinkMode toLinkMode(const char *arg) -{ - for (auto& s : link_modes_) { - if (!strcmp(arg, s.lcname)) { - return s.mode; - } - } - return LinkMode::UNKNOWN; -} - -LinkModeSet parseLinkModes(std::string m) -{ - LinkModeSet lms; - char buf[m.length()+1]; - strcpy(buf, m.c_str()); - char *saveptr {}; - const char *tok = strtok_r(buf, ",", &saveptr); - while (tok != NULL) - { - LinkMode lm = toLinkMode(tok); - if (lm == LinkMode::UNKNOWN) - { - error("(wmbus) not a valid link mode: %s\n", tok); - } - lms.addLinkMode(lm); - tok = strtok_r(NULL, ",", &saveptr); - } - return lms; -} - -bool isValidLinkModes(std::string m) -{ - LinkModeSet lms; - char buf[m.length()+1]; - strcpy(buf, m.c_str()); - char *saveptr {}; - const char *tok = strtok_r(buf, ",", &saveptr); - while (tok != NULL) - { - LinkMode lm = toLinkMode(tok); - if (lm == LinkMode::UNKNOWN) - { - return false; - } - lms.addLinkMode(lm); - tok = strtok_r(NULL, ",", &saveptr); - } - return true; -} - -LinkModeSet &LinkModeSet::addLinkMode(LinkMode lm) -{ - for (auto& s : link_modes_) { - if (s.mode == lm) { - set_ |= s.val; - } - } - return *this; -} - -void LinkModeSet::unionLinkModeSet(LinkModeSet lms) -{ - set_ |= lms.set_; -} - -void LinkModeSet::disjunctionLinkModeSet(LinkModeSet lms) -{ - set_ &= lms.set_; -} - -bool LinkModeSet::supports(LinkModeSet lms) -{ - // Will return false, if lms is UKNOWN (=0). - return (set_ & lms.set_) != 0; -} - -bool LinkModeSet::has(LinkMode lm) -{ - LinkModeInfo *lmi = getLinkModeInfo(lm); - return (set_ & lmi->val) != 0; -} - -bool LinkModeSet::hasAll(LinkModeSet lms) -{ - return (set_ & lms.set_) == lms.set_; -} - -std::string LinkModeSet::hr() -{ - std::string r; - if (set_ == Any_bit) return "any"; - if (set_ == 0) return "none"; - for (auto& s : link_modes_) - { - if (s.mode == LinkMode::Any) continue; - if (set_ & s.val) - { - r += s.lcname; - r += ","; - } - } - r.pop_back(); - return r; -} - -struct Manufacturer { - const char *code; - int m_field; - const char *name; - - Manufacturer(const char *c, int m, const char *n) { - code = c; - m_field = m; - name = n; - } -}; - -std::vector manufacturers_; - -struct Initializer { Initializer(); }; - -static Initializer initializser_; - -Initializer::Initializer() { - -#define X(key,code,name) manufacturers_.push_back(Manufacturer(#key,code,name)); -LIST_OF_MANUFACTURERS -#undef X - -} - -void Telegram::addAddressMfctFirst(const std::vector::iterator &pos) -{ - Address a; - a.decodeMfctFirst(pos); - addresses.push_back(a); -} - -void Telegram::addAddressIdFirst(const std::vector::iterator &pos) -{ - Address a; - a.decodeIdFirst(pos); - addresses.push_back(a); -} - -void Telegram::print() -{ - uchar a=0, b=0, c=0, d=0; - if (dll_id.size() >= 4) - { - a = dll_id[0]; - b = dll_id[1]; - c = dll_id[2]; - d = dll_id[3]; - } - const char *enc = ""; - - if (ell_sec_mode != ELLSecurityMode::NoSecurity || - tpl_sec_mode != TPLSecurityMode::NoSecurity) - { - enc = " encrypted"; - } - - notice("Received telegram from: %02x%02x%02x%02x\n", a,b,c,d); - notice(" manufacturer: (%s) %s (0x%02x)\n", - manufacturerFlag(dll_mfct).c_str(), - manufacturer(dll_mfct).c_str(), - dll_mfct); - notice(" type: %s (0x%02x)%s\n", mediaType(dll_type, dll_mfct).c_str(), dll_type, enc); - - notice(" ver: 0x%02x\n", dll_version); - - if (tpl_id_found) - { - notice(" Concerning meter: %02x%02x%02x%02x\n", tpl_id_b[3],tpl_id_b[2],tpl_id_b[1],tpl_id_b[0]); - notice(" manufacturer: (%s) %s (0x%02x)\n", - manufacturerFlag(tpl_mfct).c_str(), - manufacturer(tpl_mfct).c_str(), - tpl_mfct); - notice(" type: %s (0x%02x)%s\n", mediaType(tpl_type, dll_mfct).c_str(), tpl_type, enc); - - notice(" ver: 0x%02x\n", tpl_version); - } - if (about.device != "") - { - notice(" device: %s\n", about.device.c_str()); - notice(" rssi: %d dBm\n", about.rssi_dbm); - if (about.link_mode != LinkMode::UNKNOWN) - { - notice(" link mode: %s\n", toString(about.link_mode)); - } - } - std::string possible_drivers = autoDetectPossibleDrivers(); - notice(" driver: %s\n", possible_drivers.c_str()); -} - -void Telegram::printDLL() -{ - if (about.type == FrameType::WMBUS) - { - std::string possible_drivers = autoDetectPossibleDrivers(); - - std::string man = manufacturerFlag(dll_mfct); - verbose("(telegram) DLL L=%02x C=%02x (%s) M=%04x (%s) A=%02x%02x%02x%02x VER=%02x TYPE=%02x (%s) (driver %s) DEV=%s RSSI=%d\n", - dll_len, - dll_c, cType(dll_c).c_str(), - dll_mfct, - man.c_str(), - dll_id[0], dll_id[1], dll_id[2], dll_id[3], - dll_version, - dll_type, - mediaType(dll_type, dll_mfct).c_str(), - possible_drivers.c_str(), - about.device.c_str(), - about.rssi_dbm); - } - - if (about.type == FrameType::MBUS) - { - verbose("(telegram) DLL L=%02x C=%02x (%s) A=%02x\n", - dll_len, - dll_c, cType(dll_c).c_str(), - mbus_primary_address); - } - -} - -void Telegram::printELL() -{ - if (ell_ci == 0) return; - - std::string ell_cc_info = ccType(ell_cc); - verbose("(telegram) ELL CI=%02x CC=%02x (%s) ACC=%02x", - ell_ci, ell_cc, ell_cc_info.c_str(), ell_acc); - - if (ell_ci == 0x8d || ell_ci == 0x8f) - { - std::string ell_sn_info = toStringFromELLSN(ell_sn); - - verbose(" SN=%02x%02x%02x%02x (%s) CRC=%02x%02x", - ell_sn_b[0], ell_sn_b[1], ell_sn_b[2], ell_sn_b[3], ell_sn_info.c_str(), - ell_pl_crc_b[0], ell_pl_crc_b[1]); - } - if (ell_ci == 0x8e || ell_ci == 0x8f) - { - std::string man = manufacturerFlag(ell_mfct); - verbose(" M=%02x%02x (%s) ID=%02x%02x%02x%02x", - ell_mfct_b[0], ell_mfct_b[1], man.c_str(), - ell_id_b[0], ell_id_b[1], ell_id_b[2], ell_id_b[3]); - } - verbose("\n"); -} - -void Telegram::printNWL() -{ - if (nwl_ci == 0) return; - - verbose("(telegram) NWL CI=%02x\n", - nwl_ci); -} - -void Telegram::printAFL() -{ - if (afl_ci == 0) return; - - verbose("(telegram) AFL CI=%02x\n", - afl_ci); - -} - -void Telegram::printTPL() -{ - if (tpl_ci == 0) return; - - verbose("(telegram) TPL CI=%02x", tpl_ci); - - if (tpl_ci == 0x7a || tpl_ci == 0x72) - { - std::string tpl_cfg_info = toStringFromTPLConfig(tpl_cfg); - verbose(" ACC=%02x STS=%02x CFG=%04x (%s)", - tpl_acc, tpl_sts, tpl_cfg, tpl_cfg_info.c_str()); - } - - if (tpl_ci == 0x72) - { - std::string info = mediaType(tpl_type, tpl_mfct); - verbose(" ID=%02x%02x%02x%02x MFT=%02x%02x VER=%02x TYPE=%02x (%s)", - tpl_id_b[0], tpl_id_b[1], tpl_id_b[2], tpl_id_b[3], - tpl_mfct_b[0], tpl_mfct_b[1], - tpl_version, tpl_type, info.c_str()); - } - - verbose("\n"); -} - - -std::string manufacturer(int m_field) { - for (auto &m : manufacturers_) { - if (m.m_field == m_field) return m.name; - } - // Some weird meters send the first char in lower case aPT iTW. Fix and try again. - m_field &= 0x7fff; - for (auto &m : manufacturers_) { - if (m.m_field == m_field) return m.name; - } - return "Unknown"; -} - -std::string mediaType(int a_field_device_type, int m_field) { - switch (a_field_device_type) { - case 0: return "Other"; - case 1: return "Oil meter"; - case 2: return "Electricity meter"; - case 3: return "Gas meter"; - case 4: return "Heat meter"; - case 5: return "Steam meter"; - case 6: return "Warm Water (30°C-90°C) meter"; - case 7: return "Water meter"; - case 8: return "Heat Cost Allocator"; - case 9: return "Compressed air meter"; - case 0x0a: return "Cooling load volume at outlet meter"; - case 0x0b: return "Cooling load volume at inlet meter"; - case 0x0c: return "Heat volume at inlet meter"; - case 0x0d: return "Heat/Cooling load meter"; - case 0x0e: return "Bus/System component"; - case 0x0f: return "Unknown"; - case 0x15: return "Hot water (>=90°C) meter"; - case 0x16: return "Cold water meter"; - case 0x17: return "Hot/Cold water meter"; - case 0x18: return "Pressure meter"; - case 0x19: return "A/D converter"; - case 0x1A: return "Smoke detector"; - case 0x1B: return "Room sensor (eg temperature or humidity)"; - case 0x1C: return "Gas detector"; - case 0x1D: return "Reserved for sensors"; - case 0x1F: return "Reserved for sensors"; - case 0x20: return "Breaker (electricity)"; - case 0x21: return "Valve (gas or water)"; - case 0x22: return "Reserved for switching devices"; - case 0x23: return "Reserved for switching devices"; - case 0x24: return "Reserved for switching devices"; - case 0x25: return "Customer unit (display device)"; - case 0x26: return "Reserved for customer units"; - case 0x27: return "Reserved for customer units"; - case 0x28: return "Waste water"; - case 0x29: return "Garbage"; - case 0x2A: return "Reserved for Carbon dioxide"; - case 0x2B: return "Reserved for environmental meter"; - case 0x2C: return "Reserved for environmental meter"; - case 0x2D: return "Reserved for environmental meter"; - case 0x2E: return "Reserved for environmental meter"; - case 0x2F: return "Reserved for environmental meter"; - case 0x30: return "Reserved for system devices"; - case 0x31: return "Reserved for communication controller"; - case 0x32: return "Reserved for unidirectional repeater"; - case 0x33: return "Reserved for bidirectional repeater"; - case 0x34: return "Reserved for system devices"; - case 0x35: return "Reserved for system devices"; - case 0x36: return "Radio converter (system side)"; - case 0x37: return "Radio converter (meter side)"; - case 0x38: return "Reserved for system devices"; - case 0x39: return "Reserved for system devices"; - case 0x3A: return "Reserved for system devices"; - case 0x3B: return "Reserved for system devices"; - case 0x3C: return "Reserved for system devices"; - case 0x3D: return "Reserved for system devices"; - case 0x3E: return "Reserved for system devices"; - case 0x3F: return "Reserved for system devices"; - } - - if (m_field == MANUFACTURER_TCH) - { - switch (a_field_device_type) { - // Techem MK Radio 3/4 manufacturer specific. - case 0x62: return "Warm water"; // MKRadio3/MKRadio4 - case 0x72: return "Cold water"; // MKRadio3/MKRadio4 - // Techem FHKV. - case 0x80: return "Heat Cost Allocator"; // FHKV data ii/iii - // Techem Vario 4 Typ 4.5.1 manufacturer specific. - case 0xC3: return "Heat meter"; - // Techem V manufacturer specific. - case 0x43: return "Heat meter"; - case 0xf0: return "Smoke detector"; - } - } - return "Unknown"; -} - -std::string mediaTypeJSON(int a_field_device_type, int m_field) -{ - switch (a_field_device_type) { - case 0: return "other"; - case 1: return "oil"; - case 2: return "electricity"; - case 3: return "gas"; - case 4: return "heat"; - case 5: return "steam"; - case 6: return "warm water"; - case 7: return "water"; - case 8: return "heat cost allocation"; - case 9: return "compressed air"; - case 0x0a: return "cooling load volume at outlet"; - case 0x0b: return "cooling load volume at inlet"; - case 0x0c: return "heat volume at inlet"; - case 0x0d: return "heat/cooling load"; - case 0x0e: return "bus/system component"; - case 0x0f: return "unknown"; - case 0x15: return "hot water"; - case 0x16: return "cold water"; - case 0x17: return "hot/cold water"; - case 0x18: return "pressure"; - case 0x19: return "a/d converter"; - case 0x1A: return "smoke detector"; - case 0x1B: return "room sensor"; - case 0x1C: return "gas detector"; - case 0x1D: return "reserved"; - case 0x1F: return "reserved"; - case 0x20: return "breaker"; - case 0x21: return "valve"; - case 0x22: return "reserved"; - case 0x23: return "reserved"; - case 0x24: return "reserved"; - case 0x25: return "customer unit (display device)"; - case 0x26: return "reserved"; - case 0x27: return "reserved"; - case 0x28: return "waste water"; - case 0x29: return "garbage"; - case 0x2A: return "reserved"; - case 0x2B: return "reserved"; - case 0x2C: return "reserved"; - case 0x2D: return "reserved"; - case 0x2E: return "reserved"; - case 0x2F: return "reserved"; - case 0x30: return "reserved"; - case 0x31: return "reserved"; - case 0x32: return "reserved"; - case 0x33: return "reserved"; - case 0x34: return "reserved"; - case 0x35: return "reserved"; - case 0x36: return "radio converter (system side)"; - case 0x37: return "radio converter (meter side)"; - case 0x38: return "reserved"; - case 0x39: return "reserved"; - case 0x3A: return "reserved"; - case 0x3B: return "reserved"; - case 0x3C: return "reserved"; - case 0x3D: return "reserved"; - case 0x3E: return "reserved"; - case 0x3F: return "reserved"; - } - - if (m_field == MANUFACTURER_TCH) - { - switch (a_field_device_type) { - // Techem MK Radio 3/4 manufacturer specific. - case 0x62: return "warm water"; // MKRadio3/MKRadio4 - case 0x72: return "cold water"; // MKRadio3/MKRadio4 - // Techem FHKV. - case 0x80: return "heat cost allocator"; // FHKV data ii/iii - // Techem Vario 4 Typ 4.5.1 manufacturer specific. - case 0xC3: return "heat"; - // Techem V manufacturer specific. - case 0x43: return "heat"; - case 0xf0: return "smoke detector"; - } - } - return "Unknown"; -} - -/* - X(0x73, TPL_73, "TPL: long header compact APL follows", 0, CI_TYPE::TPL, "") \ -*/ - -#define LIST_OF_CI_FIELDS \ - X(0x51, TPL_51, "TPL: APL follows", 0, CI_TYPE::TPL, "") \ - X(0x72, TPL_72, "TPL: long header APL follows", 0, CI_TYPE::TPL, "") \ - X(0x78, TPL_78, "TPL: no header APL follows", 0, CI_TYPE::TPL, "") \ - X(0x79, TPL_79, "TPL: compact APL follows", 0, CI_TYPE::TPL, "") \ - X(0x7A, TPL_7A, "TPL: short header APL follows", 0, CI_TYPE::TPL, "") \ - X(0x81, NWL_81, "NWL: TPL or APL follows?", 0, CI_TYPE::NWL, "") \ - X(0x8C, ELL_I, "ELL: I", 2, CI_TYPE::ELL, "CC, ACC") \ - X(0x8D, ELL_II, "ELL: II", 8, CI_TYPE::ELL, "CC, ACC, SN, Payload CRC") \ - X(0x8E, ELL_III, "ELL: III", 10, CI_TYPE::ELL, "CC, ACC, M2, A2") \ - X(0x8F, ELL_IV, "ELL: IV", 16, CI_TYPE::ELL, "CC, ACC, M2, A2, SN, Payload CRC") \ - X(0x86, ELL_V, "ELL: V", -1, CI_TYPE::ELL, "Variable length") \ - X(0x90, AFL, "AFL", 10, CI_TYPE::AFL, "") - -enum CI_Field_Values { -#define X(val,name,cname,len,citype,explain) name = val, -LIST_OF_CI_FIELDS -#undef X -}; - -bool isCiFieldOfType(int ci_field, CI_TYPE type) -{ -#define X(val,name,cname,len,citype,explain) if (ci_field == val && type == citype) return true; -LIST_OF_CI_FIELDS -#undef X - return false; -} - -int ciFieldLength(int ci_field) -{ -#define X(val,name,cname,len,citype,explain) if (ci_field == val) return len; -LIST_OF_CI_FIELDS -#undef X - return -2; -} - -bool isCiFieldManufacturerSpecific(int ci_field) -{ - return ci_field >= 0xA0 && ci_field <= 0xB7; -} - -std::string ciType(int ci_field) -{ - if (ci_field >= 0xA0 && ci_field <= 0xB7) { - return "Mfct specific"; - } - if (ci_field >= 0x00 && ci_field <= 0x1f) { - return "Reserved for DLMS"; - } - - if (ci_field >= 0x20 && ci_field <= 0x4f) { - return "Reserved"; - } - - switch (ci_field) { - case 0x50: return "Application reset or select to device (no tplh)"; - case 0x51: return "Command to device (no tplh)"; // Only for mbus, not wmbus. - case 0x52: return "Selection of device (no tplh)"; - case 0x53: return "Application reset or select to device (long tplh)"; - case 0x54: return "Request of selected application to device (no tplh)"; - case 0x55: return "Request of selected application to device (long tplh)"; - case 0x56: return "Reserved"; - case 0x57: return "Reserved"; - case 0x58: return "Reserved"; - case 0x59: return "Reserved"; - case 0x5a: return "Command to device (short tplh)"; - case 0x5b: return "Command to device (long tplh)"; - case 0x5c: return "Sync action (no tplh)"; - case 0x5d: return "Reserved"; - case 0x5e: return "Reserved"; - case 0x5f: return "Specific usage"; - case 0x60: return "COSEM Data sent by the Readout device to the meter (long tplh)"; - case 0x61: return "COSEM Data sent by the Readout device to the meter (short tplh)"; - case 0x62: return "?"; - case 0x63: return "?"; - case 0x64: return "Reserved for OBIS-based Data sent by the Readout device to the meter (long tplh)"; - case 0x65: return "Reserved for OBIS-based Data sent by the Readout device to the meter (short tplh)"; - case 0x66: return "Response of selected application from device (no tplh)"; - case 0x67: return "Response of selected application from device (short tplh)"; - case 0x68: return "Response of selected application from device (long tplh)"; - case 0x69: return "EN 13757-3 Application Layer with Format frame (no tplh)"; - case 0x6A: return "EN 13757-3 Application Layer with Format frame (short tplh)"; - case 0x6B: return "EN 13757-3 Application Layer with Format frame (long tplh)"; - case 0x6C: return "Clock synchronisation (absolute) (long tplh)"; - case 0x6D: return "Clock synchronisation (relative) (long tplh)"; - case 0x6E: return "Application error from device (short tplh)"; - case 0x6F: return "Application error from device (long tplh)"; - case 0x70: return "Application error from device without Transport Layer"; - case 0x71: return "Reserved for Alarm Report"; - case 0x72: return "EN 13757-3 Application Layer (long tplh)"; - case 0x73: return "EN 13757-3 Application Layer with Compact frame and long Transport Layer"; - case 0x74: return "Alarm from device (short tplh)"; - case 0x75: return "Alarm from device (long tplh)"; - case 0x76: return "?"; - case 0x77: return "?"; - case 0x78: return "EN 13757-3 Application Layer (no tplh)"; - case 0x79: return "EN 13757-3 Application Layer with Compact frame (no tplh)"; - case 0x7A: return "EN 13757-3 Application Layer (short tplh)"; - case 0x7B: return "EN 13757-3 Application Layer with Compact frame (short tplh)"; - case 0x7C: return "COSEM Application Layer (long tplh)"; - case 0x7D: return "COSEM Application Layer (short tplh)"; - case 0x7E: return "Reserved for OBIS-based Application Layer (long tplh)"; - case 0x7F: return "Reserved for OBIS-based Application Layer (short tplh)"; - case 0x80: return "EN 13757-3 Transport Layer (long tplh) from other device to the meter"; - - case 0x81: return "Network Layer data"; - case 0x82: return "Network management data to device (short tplh)"; - case 0x83: return "Network Management data to device (no tplh)"; - case 0x84: return "Transport layer to device (compact frame) (long tplh)"; - case 0x85: return "Transport layer to device (format frame) (long tplh)"; - case 0x86: return "Extended Link Layer V (variable length)"; - case 0x87: return "Network management data from device (long tplh)"; - case 0x88: return "Network management data from device (short tplh)"; - case 0x89: return "Network management data from device (no tplh)"; - case 0x8A: return "EN 13757-3 Transport Layer (short tplh) from the meter to the other device"; // No application layer, e.g. ACK - case 0x8B: return "EN 13757-3 Transport Layer (long tplh) from the meter to the other device"; // No application layer, e.g. ACK - - case 0x8C: return "ELL: Extended Link Layer I (2 Byte)"; // CC, ACC - case 0x8D: return "ELL: Extended Link Layer II (8 Byte)"; // CC, ACC, SN, Payload CRC - case 0x8E: return "ELL: Extended Link Layer III (10 Byte)"; // CC, ACC, M2, A2 - case 0x8F: return "ELL: Extended Link Layer IV (16 Byte)"; // CC, ACC, M2, A2, SN, Payload CRC - - case 0x90: return "AFL: Authentication and Fragmentation Sublayer"; - case 0x91: return "Reserved"; - case 0x92: return "Reserved"; - case 0x93: return "Reserved"; - case 0x94: return "Reserved"; - case 0x95: return "Reserved"; - case 0x96: return "Reserved"; - case 0x97: return "Reserved"; - case 0x98: return "?"; - case 0x99: return "?"; - - case 0xB8: return "Set baud rate to 300"; - case 0xB9: return "Set baud rate to 600"; - case 0xBA: return "Set baud rate to 1200"; - case 0xBB: return "Set baud rate to 2400"; - case 0xBC: return "Set baud rate to 4800"; - case 0xBD: return "Set baud rate to 9600"; - case 0xBE: return "Set baud rate to 19200"; - case 0xBF: return "Set baud rate to 38400"; - case 0xC0: return "Image transfer to device (long tplh)"; - case 0xC1: return "Image transfer from device (short tplh)"; - case 0xC2: return "Image transfer from device (long tplh)"; - case 0xC3: return "Security info transfer to device (long tplh)"; - case 0xC4: return "Security info transfer from device (short tplh)"; - case 0xC5: return "Security info transfer from device (long tplh)"; - } - return "?"; -} - -void Telegram::addExplanationAndIncrementPos (std::vector::iterator &pos, int len, KindOfData k, Understanding u, const char* fmt, ...) -{ - // char buf[1024]; - // buf[1023] = 0; - - // va_list args; - // va_start(args, fmt); - // vsnprintf(buf, 1023, fmt, args); - // va_end(args); - - // Explanation e(parsed.size(), len, buf, k, u); - // explanations.push_back(e); - // parsed.insert(parsed.end(), pos, pos+len); - pos += len; -} - -void Telegram::setExplanation (std::vector::iterator &pos, int len, KindOfData k, Understanding u, const char* fmt, ...) -{ - // char buf[1024]; - // buf[1023] = 0; - - // va_list args; - // va_start(args, fmt); - // vsnprintf(buf, 1023, fmt, args); - // va_end(args); - - // Explanation e(distance(frame.begin(),pos), len, buf, k, u); - // explanations.push_back(e); -} - -void Telegram::addMoreExplanation(int pos, std::string json) -{ - addMoreExplanation(pos, " (%s)", json.c_str()); -} - -void Telegram::addMoreExplanation(int pos, const char* fmt, ...) -{ - // char buf[1024]; - - // buf[1023] = 0; - - // va_list args; - // va_start(args, fmt); - // vsnprintf(buf, 1023, fmt, args); - // va_end(args); - - // bool found = false; - // for (auto& p : explanations) { - // if (p.pos == pos) - // { - // // Append more information. - // p.info = p.info+buf; - // // Since we are adding more information, we assume that we have a full understanding. - // p.understanding = Understanding::FULL; - // found = true; - // } - // } - - // if (!found) { - // debug("(wmbus) warning: cannot find offset %d to add more explanation \"%s\"\n", pos, buf); - // } -} - -void Telegram::addSpecialExplanation(int offset, int len, KindOfData k, Understanding u, const char* fmt, ...) -{ - // char buf[1024]; - // buf[1023] = 0; - - // va_list args; - // va_start(args, fmt); - // vsnprintf(buf, 1023, fmt, args); - // va_end(args); - - // explanations.push_back({offset, len, buf, k, u}); -} - -bool expectedMore(int line) -{ - verbose("(wmbus) parser expected more data! (%d)\n", line); - return false; -} - -bool Telegram::parseMBusDLLandTPL (std::vector::iterator &pos) -{ - int remaining = distance(pos, frame.end()); - - if (remaining == 1 && *pos == 0xE5) - { - addExplanationAndIncrementPos(pos, 1, KindOfData::PROTOCOL, Understanding::FULL, "E5"); - return true; - } - - if (remaining < 5) { - return expectedMore(__LINE__); - } - - debug("(mbus) parse MBUS DLL @%d %d\n", distance(frame.begin(), pos), remaining); - debugPayload("(mbus) ", frame); - - if (*pos != 0x68) return false; - addExplanationAndIncrementPos(pos, 1, KindOfData::PROTOCOL, Understanding::FULL, "68 start"); - - dll_len = *pos; - addExplanationAndIncrementPos(pos, 1, KindOfData::PROTOCOL, Understanding::FULL, "%02x length (%d bytes)", dll_len, dll_len); - - // Two identical length bytes are expected! - if (*pos != dll_len) return false; - addExplanationAndIncrementPos(pos, 1, KindOfData::PROTOCOL, Understanding::FULL, "%02x length again (%d bytes)", dll_len, dll_len); - - if (*pos != 0x68) return false; - addExplanationAndIncrementPos(pos, 1, KindOfData::PROTOCOL, Understanding::FULL, "68 start"); - - if (remaining < dll_len) return expectedMore(__LINE__); - - // Last byte should be 0x16 - auto end = frame.end(); - end--; - if (*end != 0x16) return false; - setExplanation(end, 1, KindOfData::PROTOCOL, Understanding::FULL, "16 end"); - - // Second last byte should be crc. Should have been checked before! No need to check again here? - end--; - setExplanation(end, 1, KindOfData::PROTOCOL, Understanding::FULL, "%02X crc", *end); - - // Mark crc and end as suffix, ie should not be parsed by dvparser. - suffix_size = 2; - - dll_c = *pos; - addExplanationAndIncrementPos(pos, 1, KindOfData::PROTOCOL, Understanding::FULL, "%02x dll-c (%s)", dll_c, mbusCField(dll_c)); - - mbus_primary_address = *pos; - addExplanationAndIncrementPos(pos, 1, KindOfData::PROTOCOL, Understanding::FULL, "%02x dll-a primary (%d)", mbus_primary_address, mbus_primary_address); - - // Add dll_id to ids. - std::string id = tostrprintf("p%d", mbus_primary_address); - Address a; - a.id = id; - addresses.push_back(a); - - mbus_ci = *pos; - addExplanationAndIncrementPos(pos, 1, KindOfData::PROTOCOL, Understanding::FULL, "%02x tpl-ci (%s)", mbus_ci, mbusCiField(mbus_ci)); - - if (mbus_ci == 0x72) - { - return parse_TPL_72(pos); - } - - return false; -} - -bool Telegram::parseDLL (std::vector::iterator &pos) -{ - int remaining = distance(pos, frame.end()); - if (remaining == 0) return expectedMore(__LINE__); - - debug("(wmbus) parseDLL @%d %d\n", distance(frame.begin(), pos), remaining); - dll_len = *pos; - if (remaining < dll_len) return expectedMore(__LINE__); - addExplanationAndIncrementPos(pos, 1, KindOfData::PROTOCOL, Understanding::FULL, "%02x length (%d bytes)", dll_len, dll_len); - - dll_c = *pos; - addExplanationAndIncrementPos(pos, 1, KindOfData::PROTOCOL, Understanding::FULL, "%02x dll-c (%s)", dll_c, cType(dll_c).c_str()); - - CHECK(8) - addAddressMfctFirst(pos); - - dll_mfct_b[0] = *(pos+0); - dll_mfct_b[1] = *(pos+1); - dll_mfct = dll_mfct_b[1] <<8 | dll_mfct_b[0]; - std::string man = manufacturerFlag(dll_mfct); - addExplanationAndIncrementPos(pos, 2, KindOfData::PROTOCOL, Understanding::FULL, "%02x%02x dll-mfct (%s)", - dll_mfct_b[0], dll_mfct_b[1], man.c_str()); - - dll_a.resize(6); - dll_id.resize(4); - for (int i=0; i<6; ++i) - { - dll_a[i] = *(pos+i); - if (i<4) - { - dll_id_b[i] = *(pos+i); - dll_id[i] = *(pos+3-i); - } - } - // Add dll_id to ids. - addExplanationAndIncrementPos(pos, 4, KindOfData::PROTOCOL, Understanding::FULL, "%02x%02x%02x%02x dll-id (%s)", - *(pos+0), *(pos+1), *(pos+2), *(pos+3), addresses.back().id.c_str()); - - dll_version = *(pos+0); - dll_type = *(pos+1); - addExplanationAndIncrementPos(pos, 1, KindOfData::PROTOCOL, Understanding::FULL, "%02x dll-version", dll_version); - addExplanationAndIncrementPos(pos, 1, KindOfData::PROTOCOL, Understanding::FULL, "%02x dll-type (%s)", dll_type, - mediaType(dll_type, dll_mfct).c_str()); - - return true; -} - -std::string Telegram::toStringFromELLSN(int sn) -{ - int session = (sn >> 0) & 0x0f; // lowest 4 bits - int time = (sn >> 4) & 0x1ffffff; // next 25 bits - int sec = (sn >> 29) & 0x7; // next 3 bits. - std::string info; - ELLSecurityMode esm = fromIntToELLSecurityMode(sec); - info += toString(esm); - info += " session="; - info += std::to_string(session); - info += " time="; - info += std::to_string(time); - return info; -} - -bool Telegram::parseELL (std::vector::iterator &pos) -{ - int remaining = distance(pos, frame.end()); - if (remaining == 0) return false; - - debug("(wmbus) parseELL @%d %d\n", distance(frame.begin(), pos), remaining); - int ci_field = *pos; - if (!isCiFieldOfType(ci_field, CI_TYPE::ELL)) return true; - addExplanationAndIncrementPos(pos, 1, KindOfData::PROTOCOL, Understanding::FULL, "%02x ell-ci-field (%s)", - ci_field, ciType(ci_field).c_str()); - ell_ci = ci_field; - int len = ciFieldLength(ell_ci); - - if (remaining < len+1) return expectedMore(__LINE__); - - // All ELL:s (including ELL I) start with cc,acc. - - ell_cc = *pos; - addExplanationAndIncrementPos(pos, 1, KindOfData::PROTOCOL, Understanding::FULL, "%02x ell-cc (%s)", ell_cc, ccType(ell_cc).c_str()); - - ell_acc = *pos; - addExplanationAndIncrementPos(pos, 1, KindOfData::PROTOCOL, Understanding::FULL, "%02x ell-acc", ell_acc); - - bool has_target_mft_address = false; - bool has_session_number_pl_crc = false; - - switch (ell_ci) - { - case CI_Field_Values::ELL_I: - // Already handled above. - break; - case CI_Field_Values::ELL_II: - has_session_number_pl_crc = true; - break; - case CI_Field_Values::ELL_III: - has_target_mft_address = true; - break; - case CI_Field_Values::ELL_IV: - has_session_number_pl_crc = true; - has_target_mft_address = true; - break; - case CI_Field_Values::ELL_V: - verbose("ELL V not yet handled\n"); - return false; - } - - if (has_target_mft_address) - { - CHECK(8); - addAddressMfctFirst(pos); - - ell_mfct_b[0] = *(pos+0); - ell_mfct_b[1] = *(pos+1); - ell_mfct = ell_mfct_b[1] << 8 | ell_mfct_b[0]; - std::string man = manufacturerFlag(ell_mfct); - addExplanationAndIncrementPos(pos, 2, KindOfData::PROTOCOL, Understanding::FULL, "%02x%02x ell-mfct (%s)", - ell_mfct_b[0], ell_mfct_b[1], man.c_str()); - - ell_id_found = true; - ell_id_b[0] = *(pos+0); - ell_id_b[1] = *(pos+1); - ell_id_b[2] = *(pos+2); - ell_id_b[3] = *(pos+3); - - // Add ell_id to ids. - addExplanationAndIncrementPos(pos, 4, KindOfData::PROTOCOL, Understanding::FULL, "%02x%02x%02x%02x ell-id", - ell_id_b[0], ell_id_b[1], ell_id_b[2], ell_id_b[3]); - - ell_version = *pos; - addExplanationAndIncrementPos(pos, 1, KindOfData::PROTOCOL, Understanding::FULL, "%02x ell-version", ell_version); - - ell_type = *pos; - addExplanationAndIncrementPos(pos, 1, KindOfData::PROTOCOL, Understanding::FULL, "%02x ell-type", ell_type); - } - - if (has_session_number_pl_crc) - { - std::string sn_info; - ell_sn_b[0] = *(pos+0); - ell_sn_b[1] = *(pos+1); - ell_sn_b[2] = *(pos+2); - ell_sn_b[3] = *(pos+3); - ell_sn = ell_sn_b[3]<<24 | ell_sn_b[2]<<16 | ell_sn_b[1] << 8 | ell_sn_b[0]; - - ell_sn_session = (ell_sn >> 0) & 0x0f; // lowest 4 bits - ell_sn_time = (ell_sn >> 4) & 0x1ffffff; // next 25 bits - ell_sn_sec = (ell_sn >> 29) & 0x7; // next 3 bits. - ell_sec_mode = fromIntToELLSecurityMode(ell_sn_sec); - std::string info = toString(ell_sec_mode); - addExplanationAndIncrementPos(pos, 4, KindOfData::PROTOCOL, Understanding::FULL, "%02x%02x%02x%02x sn (%s)", - ell_sn_b[0], ell_sn_b[1], ell_sn_b[2], ell_sn_b[3], info.c_str()); - - if (ell_sec_mode == ELLSecurityMode::AES_CTR) - { - if (meter_keys) - { - decrypt_ELL_AES_CTR(this, frame, pos, meter_keys->confidentiality_key); - // Actually this ctr decryption always succeeds, if wrong key, it will decrypt to garbage. - } - // Now the frame from pos and onwards has been decrypted, perhaps. - } - - ell_pl_crc_b[0] = *(pos+0); - ell_pl_crc_b[1] = *(pos+1); - ell_pl_crc = (ell_pl_crc_b[1] << 8) | ell_pl_crc_b[0]; - - int dist = distance(frame.begin(), pos+2); - int len = distance(pos+2, frame.end()); - uint16_t check = crc16_EN13757(&(frame[dist]), len); - - addExplanationAndIncrementPos(pos, 2, KindOfData::PROTOCOL, Understanding::FULL, - "%02x%02x payload crc (calculated %02x%02x %s)", - ell_pl_crc_b[0], ell_pl_crc_b[1], - check & 0xff, check >> 8, (ell_pl_crc==check?"OK":"ERROR")); - - - if (ell_pl_crc == check || FUZZING) - { - } - else - { - // Ouch, checksum of the payload does not match. - // A wrong key, or no key was probably used for decryption. - decryption_failed = true; - - // Log the content as failed decryption. - int num_encrypted_bytes = frame.end()-pos; - std::string info = bin2hex(pos, frame.end(), num_encrypted_bytes); - info += " failed decryption. Wrong key?"; - addExplanationAndIncrementPos(pos, num_encrypted_bytes, KindOfData::CONTENT, Understanding::ENCRYPTED, info.c_str()); - - if (parser_warns_) - { - if (!beingAnalyzed()) - { - // Print this warning only once! Unless you are using verbose or debug. - verbose("(wmbus) WARNING! decrypted payload crc failed check, did you use the correct decryption key? " - "%02x%02x payload crc (calculated %02x%02x) " - "Permanently ignoring telegrams from id: %02x%02x%02x%02x mfct: (%s) %s (0x%02x) type: %s (0x%02x) ver: 0x%02x\n", - ell_pl_crc_b[0], ell_pl_crc_b[1], - check & 0xff, check >> 8, - dll_id_b[3], dll_id_b[2], dll_id_b[1], dll_id_b[0], - manufacturerFlag(dll_mfct).c_str(), - manufacturer(dll_mfct).c_str(), - dll_mfct, - mediaType(dll_type, dll_mfct).c_str(), dll_type, - dll_version); - } - } - } - } - - return true; -} - -bool Telegram::parseNWL (std::vector::iterator &pos) -{ - int remaining = distance(pos, frame.end()); - if (remaining == 0) return false; - - debug("(wmbus) parseNWL @%d %d\n", distance(frame.begin(), pos), remaining); - int ci_field = *pos; - if (!isCiFieldOfType(ci_field, CI_TYPE::NWL)) return true; - addExplanationAndIncrementPos(pos, 1, KindOfData::PROTOCOL, Understanding::FULL, "%02x nwl-ci-field (%s)", - ci_field, ciType(ci_field).c_str()); - nwl_ci = ci_field; - // We have only seen 0x81 0x1d so far. - int len = 1; // ciFieldLength(nwl_ci); - - if (remaining < len+1) return expectedMore(__LINE__); - - uchar nwl = *pos; - addExplanationAndIncrementPos(pos, 1, KindOfData::PROTOCOL, Understanding::FULL, "%02x nwl?", nwl); - - return true; -} - -bool Telegram::parseAFL (std::vector::iterator &pos) -{ - // 90 0F (len) 002C (fc) 25 (mc) 49EE 0A00 77C1 9D3D 1A08 ABCD --- 729067296179161102F - // 90 0F (len) 002C (fc) 25 (mc) 0C39 0000 ED17 6BBB B159 1ADB --- 7A1D003007103EA - - int remaining = distance(pos, frame.end()); - if (remaining == 0) return false; - - debug("(wmbus) parseAFL @%d %d\n", distance(frame.begin(), pos), remaining); - - int ci_field = *pos; - if (!isCiFieldOfType(ci_field, CI_TYPE::AFL)) return true; - addExplanationAndIncrementPos(pos, 1, KindOfData::PROTOCOL, Understanding::FULL, "%02x afl-ci-field (%s)", - ci_field, ciType(ci_field).c_str()); - afl_ci = ci_field; - - afl_len = *pos; - addExplanationAndIncrementPos(pos, 1, KindOfData::PROTOCOL, Understanding::FULL, "%02x afl-len (%d)", - afl_len, afl_len); - - int len = ciFieldLength(afl_ci); - if (remaining < len) return expectedMore(__LINE__); - - afl_fc_b[0] = *(pos+0); - afl_fc_b[1] = *(pos+1); - afl_fc = afl_fc_b[1] << 8 | afl_fc_b[0]; - std::string afl_fc_info = toStringFromAFLFC(afl_fc); - addExplanationAndIncrementPos(pos, 2, KindOfData::PROTOCOL, Understanding::FULL, "%02x%02x afl-fc (%s)", - afl_fc_b[0], afl_fc_b[1], afl_fc_info.c_str()); - - bool has_key_info = afl_fc & 0x0200; - bool has_mac = afl_fc & 0x0400; - bool has_counter = afl_fc & 0x0800; - //bool has_len = afl_fc & 0x1000; - bool has_control = afl_fc & 0x2000; - //bool has_more_fragments = afl_fc & 0x4000; - - if (has_control) - { - afl_mcl = *pos; - std::string afl_mcl_info = toStringFromAFLMC(afl_mcl); - addExplanationAndIncrementPos(pos, 1, KindOfData::PROTOCOL, Understanding::FULL, "%02x afl-mcl (%s)", - afl_mcl, afl_mcl_info.c_str()); - } - - if (has_key_info) - { - afl_ki_b[0] = *(pos+0); - afl_ki_b[1] = *(pos+1); - afl_ki = afl_ki_b[1] << 8 | afl_ki_b[0]; - std::string afl_ki_info = ""; - addExplanationAndIncrementPos(pos, 2, KindOfData::PROTOCOL, Understanding::FULL, "%02x%02x afl-ki (%s)", - afl_ki_b[0], afl_ki_b[1], afl_ki_info.c_str()); - } - - if (has_counter) - { - afl_counter_b[0] = *(pos+0); - afl_counter_b[1] = *(pos+1); - afl_counter_b[2] = *(pos+2); - afl_counter_b[3] = *(pos+3); - afl_counter = afl_counter_b[3] << 24 | - afl_counter_b[2] << 16 | - afl_counter_b[1] << 8 | - afl_counter_b[0]; - - addExplanationAndIncrementPos(pos, 4, KindOfData::PROTOCOL, Understanding::FULL, "%02x%02x%02x%02x afl-counter (%u)", - afl_counter_b[0],afl_counter_b[1], - afl_counter_b[2],afl_counter_b[3], - afl_counter); - } - - if (has_mac) - { - int at = afl_mcl & 0x0f; - AFLAuthenticationType aat = fromIntToAFLAuthenticationType(at); - int len = toLen(aat); - if (len != 2 && - len != 4 && - len != 8 && - len != 12 && - len != 16) - { - if (parser_warns_) - { - warning("(wmbus) WARNING! bad length of mac\n"); - } - return false; - } - afl_mac_b.clear(); - for (int i=0; i 0) info.pop_back(); - return info; -} - -std::string Telegram::toStringFromAFLMC(int mc) -{ - std::string info = ""; - int at = mc & 0x0f; - AFLAuthenticationType aat = fromIntToAFLAuthenticationType(at); - info += toString(aat); - info += " "; - if (mc & 0x10) info += "KeyInfo "; - if (mc & 0x20) info += "MessCounter "; - if (mc & 0x40) info += "MessLen "; - if (info.length() > 0) info.pop_back(); - return info; -} - -std::string Telegram::toStringFromTPLConfig(int cfg) -{ - std::string info = ""; - if (cfg & 0x8000) info += "bidirectional "; - if (cfg & 0x4000) info += "accessibility "; - if (cfg & 0x2000) info += "synchronous "; - if (cfg & 0x1f00) - { - int m = (cfg >> 8) & 0x1f; - TPLSecurityMode tsm = fromIntToTPLSecurityMode(m); - info += toString(tsm); - info += " "; - if (tsm == TPLSecurityMode::AES_CBC_IV) - { - int num_blocks = (cfg & 0x00f0) >> 4; - int cntn = (cfg & 0x000c) >> 2; - int ra = (cfg & 0x0002) >> 1; - int hc = cfg & 0x0001; - info += "nb="+std::to_string(num_blocks); - info += " cntn="+std::to_string(cntn); - info += " ra="+std::to_string(ra); - info += " hc="+std::to_string(hc); - info += " "; - } - } - if (info.length() > 0) info.pop_back(); - return info; -} - -bool Telegram::parseTPLConfig(std::vector::iterator &pos) -{ - CHECK(2); - uchar cfg1 = *(pos+0); - uchar cfg2 = *(pos+1); - tpl_cfg = cfg2 << 8 | cfg1; - - if (tpl_cfg & 0x1f00) - { - int m = (tpl_cfg >> 8) & 0x1f; - tpl_sec_mode = fromIntToTPLSecurityMode(m); - } - bool has_cfg_ext = false; - std::string info = toStringFromTPLConfig(tpl_cfg); - info += " "; - if (tpl_sec_mode == TPLSecurityMode::AES_CBC_IV) // Security mode 5 - { - tpl_num_encr_blocks = (tpl_cfg >> 4) & 0x0f; - } - if (tpl_sec_mode == TPLSecurityMode::AES_CBC_NO_IV) // Security mode 7 - { - tpl_num_encr_blocks = (tpl_cfg >> 4) & 0x0f; - has_cfg_ext = true; - } - addExplanationAndIncrementPos(pos, 2, KindOfData::PROTOCOL, Understanding::FULL, - "%02x%02x tpl-cfg %04x (%s)", cfg1, cfg2, tpl_cfg, info.c_str()); - - if (has_cfg_ext) - { - CHECK(1); - tpl_cfg_ext = *(pos+0); - tpl_kdf_selection = (tpl_cfg_ext >> 4) & 3; - - addExplanationAndIncrementPos(pos, 1, KindOfData::PROTOCOL, Understanding::FULL, - "%02x tpl-cfg-ext (KDFS=%d)", tpl_cfg_ext, tpl_kdf_selection); - - if (tpl_kdf_selection == 1) - { - std::vector input; - std::vector mac; - mac.resize(16); - - // DC C ID 0x07 0x07 0x07 0x07 0x07 0x07 0x07 - // Derivation Constant DC = 0x00 = encryption from meter. - // 0x01 = mac from meter. - // 0x10 = encryption from communication partner. - // 0x11 = mac from communication partner. - input.insert(input.end(), 0x00); // DC 00 = generate ephemereal encryption key from meter. - // If there is a tpl_counter, then use it, else use afl_counter. - input.insert(input.end(), afl_counter_b, afl_counter_b+4); - // If there is a tpl_id, then use it, else use ddl_id. - if (tpl_id_found) - { - input.insert(input.end(), tpl_id_b, tpl_id_b+4); - } - else - { - input.insert(input.end(), dll_id_b, dll_id_b+4); - } - - // Pad. - for (int i=0; i<7; ++i) input.insert(input.end(), 0x07); - - debugPayload("(wmbus) input to kdf for enc", input); - - if (meter_keys == NULL || meter_keys->confidentiality_key.size() != 16) - { - if (isSimulated()) - { - debug("(wmbus) simulation without keys, not generating Kmac and Kenc.\n"); - return true; - } - debug("(wmbus) no key, thus cannot execute kdf.\n"); - return false; - } - AES_CMAC(safeButUnsafeVectorPtr(meter_keys->confidentiality_key), - safeButUnsafeVectorPtr(input), 16, - safeButUnsafeVectorPtr(mac)); - std::string s = bin2hex(mac); - debug("(wmbus) ephemereal Kenc %s\n", s.c_str()); - tpl_generated_key.clear(); - tpl_generated_key.insert(tpl_generated_key.end(), mac.begin(), mac.end()); - - input[0] = 0x01; // DC 01 = generate ephemereal mac key from meter. - mac.clear(); - mac.resize(16); - debugPayload("(wmbus) input to kdf for mac", input); - AES_CMAC(safeButUnsafeVectorPtr(meter_keys->confidentiality_key), - safeButUnsafeVectorPtr(input), 16, - safeButUnsafeVectorPtr(mac)); - s = bin2hex(mac); - debug("(wmbus) ephemereal Kmac %s\n", s.c_str()); - tpl_generated_mac_key.clear(); - tpl_generated_mac_key.insert(tpl_generated_mac_key.end(), mac.begin(), mac.end()); - } - } - - return true; -} - -bool Telegram::parseShortTPL(std::vector::iterator &pos) -{ - CHECK(1); - - tpl_acc = *pos; - addExplanationAndIncrementPos(pos, 1, KindOfData::PROTOCOL, Understanding::FULL, - "%02x tpl-acc-field", tpl_acc); - - CHECK(1); - tpl_sts = *pos; - tpl_sts_offset = distance(frame.begin(), pos); - addExplanationAndIncrementPos(pos, 1, KindOfData::PROTOCOL, Understanding::FULL, - "%02x tpl-sts-field (%s)", tpl_sts, decodeTPLStatusByteOnlyStandardBits(tpl_sts).c_str()); - bool ok = parseTPLConfig(pos); - if (!ok) return false; - - return true; -} - -bool Telegram::parseLongTPL(std::vector::iterator &pos) -{ - CHECK(8); - addAddressIdFirst(pos); - - tpl_id_found = true; - tpl_id_b[0] = *(pos+0); - tpl_id_b[1] = *(pos+1); - tpl_id_b[2] = *(pos+2); - tpl_id_b[3] = *(pos+3); - - tpl_a.resize(6); - for (int i=0; i<4; ++i) - { - tpl_a[i] = *(pos+i); - } - - addExplanationAndIncrementPos(pos, 4, KindOfData::PROTOCOL, Understanding::FULL, - "%02x%02x%02x%02x tpl-id (%02x%02x%02x%02x)", - tpl_id_b[0], tpl_id_b[1], tpl_id_b[2], tpl_id_b[3], - tpl_id_b[3], tpl_id_b[2], tpl_id_b[1], tpl_id_b[0]); - - CHECK(2); - tpl_mfct_b[0] = *(pos+0); - tpl_mfct_b[1] = *(pos+1); - tpl_mfct = tpl_mfct_b[1] << 8 | tpl_mfct_b[0]; - std::string man = manufacturerFlag(tpl_mfct); - addExplanationAndIncrementPos(pos, 2, KindOfData::PROTOCOL, Understanding::FULL, "%02x%02x tpl-mfct (%s)", tpl_mfct_b[0], tpl_mfct_b[1], man.c_str()); - - CHECK(1); - tpl_version = *(pos+0); - tpl_a[4] = *(pos+0); - addExplanationAndIncrementPos(pos, 1, KindOfData::PROTOCOL, Understanding::FULL, "%02x tpl-version", tpl_version); - - CHECK(1); - tpl_type = *(pos+0); - tpl_a[5] = *(pos+0); - std::string info = mediaType(tpl_type, tpl_mfct); - addExplanationAndIncrementPos(pos, 1, KindOfData::PROTOCOL, Understanding::FULL, "%02x tpl-type (%s)", tpl_type, info.c_str()); - - bool ok = parseShortTPL(pos); - - return ok; -} - -bool Telegram::checkMAC(std::vector &frame, - std::vector::iterator from, - std::vector::iterator to, - std::vector &inmac, - std::vector &mackey) -{ - std::vector input; - std::vector mac; - mac.resize(16); - - if (mackey.size() != 16) return false; - if (inmac.size() == 0) return false; - - // AFL.MAC = CMAC (Kmac/Lmac, - // AFL.MCL || AFL.MCR || {AFL.ML || } NextCI || ... || Last Byte of message) - - input.insert(input.end(), afl_mcl); - input.insert(input.end(), afl_counter_b, afl_counter_b+4); - input.insert(input.end(), from, to); - std::string s = bin2hex(input); - debug("(wmbus) input to mac %s\n", s.c_str()); - AES_CMAC(safeButUnsafeVectorPtr(mackey), - safeButUnsafeVectorPtr(input), input.size(), - safeButUnsafeVectorPtr(mac)); - std::string calculated = bin2hex(mac); - debug("(wmbus) calculated mac %s\n", calculated.c_str()); - std::string received = bin2hex(inmac); - debug("(wmbus) received mac %s\n", received.c_str()); - std::string truncated = calculated.substr(0, received.length()); - bool ok = truncated == received; - if (ok) - { - debug("(wmbus) mac ok!\n"); - } - else - { - debug("(wmbus) mac NOT ok!\n"); - explainParse("BADMAC", 0); - } - return ok; -} - -bool loadFormatBytesFromSignature(uint16_t format_signature, std::vector *format_bytes); - -bool Telegram::alreadyDecryptedCBC (std::vector::iterator &pos) -{ - CHECK(2); - if (*(pos+0) != 0x2f || *(pos+1) != 0x2f) return false; - addExplanationAndIncrementPos(pos, 2, KindOfData::PROTOCOL, Understanding::FULL, "%02x%02x already decrypted check bytes", *(pos+0), *(pos+1)); - return true; -} - -bool Telegram::potentiallyDecrypt (std::vector::iterator &pos) -{ - if (tpl_sec_mode == TPLSecurityMode::AES_CBC_IV) - { - if (alreadyDecryptedCBC(pos)) - { - if (meter_keys && meter_keys->hasConfidentialityKey()) - { - // Oups! This telegram is already decrypted (but the header still says it should be encrypted) - // this is probably a replay telegram from --logtelegrams. - // But since we have specified a key! Do not accept this telegram! - warning("(wmbus) WARNING!! telegram should have been fully encrypted, but was not! " - "id: %02x%02x%02x%02x mfct: (%s) %s (0x%02x) type: %s (0x%02x) ver: 0x%02x\n", - dll_id_b[3], dll_id_b[2], dll_id_b[1], dll_id_b[0], - manufacturerFlag(dll_mfct).c_str(), - manufacturer(dll_mfct).c_str(), - dll_mfct, - mediaType(dll_type, dll_mfct).c_str(), dll_type, - dll_version); - return false; - } - return true; - } - if (!meter_keys) return false; - if (!meter_keys->hasConfidentialityKey()) - { - addDefaultManufacturerKeyIfAny(frame, tpl_sec_mode, meter_keys); - } - int num_encrypted_bytes = 0; - int num_not_encrypted_at_end = 0; - - bool ok = decrypt_TPL_AES_CBC_IV(this, frame, pos, meter_keys->confidentiality_key, - &num_encrypted_bytes, &num_not_encrypted_at_end); - if (!ok) - { - // No key supplied. - std::string info = bin2hex(pos, frame.end(), num_encrypted_bytes); - info += " encrypted"; - addExplanationAndIncrementPos(pos, num_encrypted_bytes, KindOfData::CONTENT, Understanding::ENCRYPTED, info.c_str()); - if (parser_warns_) - { - if (!beingAnalyzed()) - { - // Print this warning only once! Unless you are using verbose or debug. - warning("(wmbus) WARNING! no key to decrypt payload! " - "Permanently ignoring telegrams from id: %02x%02x%02x%02x mfct: (%s) %s (0x%02x) type: %s (0x%02x) ver: 0x%02x\n", - dll_id_b[3], dll_id_b[2], dll_id_b[1], dll_id_b[0], - manufacturerFlag(dll_mfct).c_str(), - manufacturer(dll_mfct).c_str(), - dll_mfct, - mediaType(dll_type, dll_mfct).c_str(), dll_type, - dll_version); - } - } - return false; - } - // Now the frame from pos and onwards has been decrypted. - - CHECK(2); - uchar a = *(pos+0); - uchar b = *(pos+1); - - addExplanationAndIncrementPos(pos, 2, KindOfData::PROTOCOL, Understanding::FULL, - "%02x%02x decrypt check bytes (%s)", *(pos+0), *(pos+1), - (*(pos+0) == 0x2f && *(pos+1) == 0x2f) ? "OK":"ERROR should be 2f2f"); - - if ((a != 0x2f || b != 0x2f) && !FUZZING) - { - // Wrong key supplied. - int num_bytes = distance(pos, frame.end()); - std::string info = bin2hex(pos, frame.end(), num_bytes); - info += " failed decryption. Wrong key?"; - addExplanationAndIncrementPos(pos, num_bytes, KindOfData::CONTENT, Understanding::ENCRYPTED, info.c_str()); - - if (parser_warns_) - { - if (!beingAnalyzed()) - { - // Print this warning only once! Unless you are using verbose or debug. - warning("(wmbus) WARNING!! decrypted content failed check, did you use the correct decryption key? " - "Permanently ignoring telegrams from id: %02x%02x%02x%02x mfct: (%s) %s (0x%02x) type: %s (0x%02x) ver: 0x%02x\n", - dll_id_b[3], dll_id_b[2], dll_id_b[1], dll_id_b[0], - manufacturerFlag(dll_mfct).c_str(), - manufacturer(dll_mfct).c_str(), - dll_mfct, - mediaType(dll_type, dll_mfct).c_str(), dll_type, - dll_version); - } - } - return false; - } - } - else if (tpl_sec_mode == TPLSecurityMode::AES_CBC_NO_IV) - { - if (alreadyDecryptedCBC(pos)) - { - if (meter_keys && meter_keys->hasConfidentialityKey()) - { - // Oups! This telegram is already decrypted (but the header still says it should be encrypted) - // this is probably a replay telegram from --logtelegrams. - // But since we have specified a key! Do not accept this telegram! - warning("(wmbus) WARNING! telegram should have been fully encrypted, but was not! " - "id: %02x%02x%02x%02x mfct: (%s) %s (0x%02x) type: %s (0x%02x) ver: 0x%02x\n", - dll_id_b[3], dll_id_b[2], dll_id_b[1], dll_id_b[0], - manufacturerFlag(dll_mfct).c_str(), - manufacturer(dll_mfct).c_str(), - dll_mfct, - mediaType(dll_type, dll_mfct).c_str(), dll_type, - dll_version); - return false; - } - return true; - } - - bool mac_ok = checkMAC(frame, tpl_start, frame.end(), afl_mac_b, tpl_generated_mac_key); - - // Do not attempt to decrypt if the mac has failed! - if (!mac_ok) - { - if (parser_warns_) - { - if (!beingAnalyzed()) - { - // Print this warning only once! Unless you are using verbose or debug. - warning("(wmbus) WARNING! telegram mac check failed, did you use the correct decryption key? " - "Permanently ignoring telegrams from id: %02x%02x%02x%02x mfct: (%s) %s (0x%02x) type: %s (0x%02x) ver: 0x%02x\n", - dll_id_b[3], dll_id_b[2], dll_id_b[1], dll_id_b[0], - manufacturerFlag(dll_mfct).c_str(), - manufacturer(dll_mfct).c_str(), - dll_mfct, - mediaType(dll_type, dll_mfct).c_str(), dll_type, - dll_version); - return false; - } - - std::string info = bin2hex(pos, frame.end(), frame.end()-pos); - info += " encrypted mac failed"; - addExplanationAndIncrementPos(pos, frame.end()-pos, KindOfData::CONTENT, Understanding::ENCRYPTED, info.c_str()); - if (meter_keys->confidentiality_key.size() > 0) - { - // Only fail if we gave an explicit key. - return false; - } - return true; - } - return false; - } - - int num_encrypted_bytes = 0; - int num_not_encrypted_at_end = 0; - bool ok = decrypt_TPL_AES_CBC_NO_IV(this, frame, pos, tpl_generated_key, - &num_encrypted_bytes, - &num_not_encrypted_at_end); - if (!ok) - { - addExplanationAndIncrementPos(pos, num_encrypted_bytes, KindOfData::CONTENT, Understanding::FULL, - "encrypted data"); - return false; - } - - // Now the frame from pos and onwards has been decrypted. - CHECK(2); - uchar a = *(pos+0); - uchar b = *(pos+1); - addExplanationAndIncrementPos(pos, 2, KindOfData::PROTOCOL, Understanding::FULL, - "%02x%02x decrypt check bytes (%s)", a, b, - (a == 0x2f && b == 0x2f) ? "OK":"ERROR should be 2f2f"); - - if ((a != 0x2f || b != 0x2f) && !FUZZING) - { - // Wrong key supplied. - std::string info = bin2hex(pos, frame.end(), num_encrypted_bytes); - info += " failed decryption. Wrong key?"; - addExplanationAndIncrementPos(pos, num_encrypted_bytes, KindOfData::CONTENT, Understanding::ENCRYPTED, info.c_str()); - - if (parser_warns_) - { - if (!beingAnalyzed()) - { - // Print this warning only once! Unless you are using verbose or debug. - warning("(wmbus) WARNING!!! decrypted content failed check, did you use the correct decryption key? " - "Permanently ignoring telegrams from id: %02x%02x%02x%02x mfct: (%s) %s (0x%02x) type: %s (0x%02x) ver: 0x%02x\n", - dll_id_b[3], dll_id_b[2], dll_id_b[1], dll_id_b[0], - manufacturerFlag(dll_mfct).c_str(), - manufacturer(dll_mfct).c_str(), - dll_mfct, - mediaType(dll_type, dll_mfct).c_str(), dll_type, - dll_version); - } - } - return false; - } - } - else if (tpl_sec_mode == TPLSecurityMode::SPECIFIC_16_31) - { - debug("(wmbus) non-standard security mode 16_31\n"); - if (mustDecryptDiehlRealData(frame)) - { - debug("(diehl) must decode frame\n"); - if (!meter_keys) return false; - bool ok = decryptDielhRealData(this, frame, pos, meter_keys->confidentiality_key); - // If this telegram is simulated, the content might already be decrypted and the - // decruption will fail. But we can assume all is well anyway! - if (!ok && isSimulated()) return true; - if (!ok) return false; - // Now the frame from pos and onwards has been decrypted. - debug("(diehl) decryption successful\n"); - } - } - else - if (meter_keys && meter_keys->hasConfidentialityKey()) - { - // Oups! This telegram is NOT encrypted, but we have specified a key! - // Do not accept this telegram! - warning("(wmbus) WARNING!!! telegram should have been encrypted, but was not! " - "id: %02x%02x%02x%02x mfct: (%s) %s (0x%02x) type: %s (0x%02x) ver: 0x%02x\n", - dll_id_b[3], dll_id_b[2], dll_id_b[1], dll_id_b[0], - manufacturerFlag(dll_mfct).c_str(), - manufacturer(dll_mfct).c_str(), - dll_mfct, - mediaType(dll_type, dll_mfct).c_str(), dll_type, - dll_version); - return false; - } - - return true; -} - -bool Telegram::parse_TPL_72 (std::vector::iterator &pos) -{ - bool ok = parseLongTPL(pos); - if (!ok) return false; - - bool decrypt_ok = potentiallyDecrypt(pos); - - header_size = distance(frame.begin(), pos); - int remaining = distance(pos, frame.end())-suffix_size; - - if (decrypt_ok) - { - parseDV(this, frame, pos, remaining, &dv_entries); - } - else - { - decryption_failed = true; - } - - return true; -} - -bool Telegram::parse_TPL_78 (std::vector::iterator &pos) -{ - header_size = distance(frame.begin(), pos); - int remaining = distance(pos, frame.end())-suffix_size; - parseDV(this, frame, pos, remaining, &dv_entries); - - return true; -} - -bool Telegram::parse_TPL_79 (std::vector::iterator &pos) -{ - // Compact frame - CHECK(2); - uchar ecrc0 = *(pos+0); - uchar ecrc1 = *(pos+1); - size_t offset = distance(frame.begin(), pos); - addExplanationAndIncrementPos(pos, 2, KindOfData::PROTOCOL, Understanding::FULL, - "%02x%02x format signature", ecrc0, ecrc1); - format_signature = ecrc1<<8 | ecrc0; - - std::vector format_bytes; - bool ok = loadFormatBytesFromSignature(format_signature, &format_bytes); - if (!ok) { - // We have not yet seen a long frame, but we know the formats for some - // meter specific hashes. - ok = findFormatBytesFromKnownMeterSignatures(&format_bytes); - if (!ok) - { - addMoreExplanation(offset, " (unknown)"); - int num_compressed_bytes = distance(pos, frame.end()); - std::string info = bin2hex(pos, frame.end(), num_compressed_bytes); - info += " compressed and signature unknown"; - addExplanationAndIncrementPos(pos, distance(pos, frame.end()), KindOfData::CONTENT, Understanding::COMPRESSED, info.c_str()); - - verbose("(wmbus) ignoring compressed telegram since format signature hash 0x%02x is yet unknown.\n" - " this is not a problem, since you only need wait for at most 8 telegrams\n" - " (8*16 seconds) until an full length telegram arrives and then we know\n" - " the format giving this hash and start decoding the telegrams properly.\n", - format_signature); - return false; - } - } - std::vector::iterator format = format_bytes.begin(); - - // 2,3 = crc for payload = hash over both DRH and data bytes. Or is it only over the data bytes? - CHECK(2); - int ecrc2 = *(pos+0); - int ecrc3 = *(pos+1); - addExplanationAndIncrementPos(pos, 2, KindOfData::PROTOCOL, Understanding::FULL, - "%02x%02x data crc", ecrc2, ecrc3); - - header_size = distance(frame.begin(), pos); - int remaining = distance(pos, frame.end())-suffix_size; - - parseDV(this, frame, pos, remaining, &dv_entries, &format, format_bytes.size()); - - return true; -} - -bool Telegram::parse_TPL_7A (std::vector::iterator &pos) -{ - bool ok = parseShortTPL(pos); - if (!ok) return false; - - bool decrypt_ok = potentiallyDecrypt(pos); - - header_size = distance(frame.begin(), pos); - int remaining = distance(pos, frame.end())-suffix_size; - - if (decrypt_ok) - { - parseDV(this, frame, pos, remaining, &dv_entries); - } - else - { - decryption_failed = true; - } - return true; -} - -bool Telegram::parseTPL (std::vector::iterator &pos) -{ - int remaining = distance(pos, frame.end()); - if (remaining == 0) return false; - - debug("(wmbus) parseTPL @%d %d\n", distance(frame.begin(), pos), remaining); - CHECK(1); - int ci_field = *pos; - int mfct_specific = isCiFieldManufacturerSpecific(ci_field); - - if (!isCiFieldOfType(ci_field, CI_TYPE::TPL) && !mfct_specific) - { - addExplanationAndIncrementPos(pos, 1, KindOfData::PROTOCOL, Understanding::NONE, - "%02x unknown ci-field", - ci_field); - if (parser_warns_) - { - warning("(wmbus) Unknown tpl-ci-field %02x\n", ci_field); - } - return false; - } - tpl_ci = ci_field; - tpl_start = pos; - - addExplanationAndIncrementPos(pos, 1, KindOfData::PROTOCOL, Understanding::FULL, - "%02x tpl-ci-field (%s)", - tpl_ci, ciType(tpl_ci).c_str()); - int len = ciFieldLength(tpl_ci); - - if (remaining < len+1 && ! mfct_specific) return expectedMore(__LINE__); - - switch (tpl_ci) - { - case CI_Field_Values::TPL_72: return parse_TPL_72(pos); - case CI_Field_Values::TPL_78: return parse_TPL_78(pos); - case CI_Field_Values::TPL_79: return parse_TPL_79(pos); - case CI_Field_Values::TPL_7A: return parse_TPL_7A(pos); - default: - { - // A0 to B7 are manufacturer specific. - header_size = distance(frame.begin(), pos); - int num_mfct_bytes = frame.end()-pos-suffix_size; - std::string info = bin2hex(pos, frame.end(), num_mfct_bytes); - info += " mfct specific"; - addExplanationAndIncrementPos(pos, num_mfct_bytes, KindOfData::CONTENT, Understanding::NONE, info.c_str()); - - return true; // Manufacturer specific telegram payload. Oh well.... - } - } - - header_size = distance(frame.begin(), pos); - if (parser_warns_) - { - warning("(wmbus) Not implemented tpl-ci %02x\n", tpl_ci); - } - return false; -} - -void Telegram::preProcess() -{ - DiehlAddressTransformMethod diehl_method = mustTransformDiehlAddress(frame); - if (diehl_method != DiehlAddressTransformMethod::NONE) - { - debug("(diehl) preprocess necessary %s\n", toString(diehl_method)); - original = std::vector(frame.begin(), frame.begin() + 10); - transformDiehlAddress(frame, diehl_method); - } -} - -bool Telegram::parse (std::vector &input_frame, MeterKeys *mk, bool warn) -{ - switch (about.type) - { - case FrameType::WMBUS: return parseWMBUS(input_frame, mk, warn); - case FrameType::MBUS: return parseMBUS(input_frame, mk, warn); - case FrameType::HAN: return parseHAN(input_frame, mk, warn); - } - assert(0); - return false; -} - -bool Telegram::parseHeader (std::vector &input_frame) -{ - switch (about.type) - { - case FrameType::WMBUS: return parseWMBUSHeader(input_frame); - case FrameType::MBUS: return parseMBUSHeader(input_frame); - case FrameType::HAN: return parseHANHeader(input_frame); - } - assert(0); - return false; -} - -bool Telegram::parseWMBUSHeader (std::vector &input_frame) -{ - assert(about.type == FrameType::WMBUS); - - bool ok; - // Parsing the header is used to extract the ids, so that we can - // match the telegram towards any known ids and thus keys. - // No need to warn. - parser_warns_ = false; - decryption_failed = false; - // explanations.clear(); - suffix_size = 0; - frame = input_frame; - std::vector::iterator pos = frame.begin(); - // Parsed accumulates parsed bytes. - parsed.clear(); - // Fixes quirks from non-compliant meters to make telegram compatible with the standard - preProcess(); - - ok = parseDLL(pos); - if (!ok) return false; - - // At the worst, only the DLL is parsed. That is fine. - ok = parseELL(pos); - if (!ok) return true; - // Could not decrypt stop here. - if (decryption_failed) return true; - - ok = parseNWL(pos); - if (!ok) return true; - - ok = parseAFL(pos); - if (!ok) return true; - - ok = parseTPL(pos); - if (!ok) return true; - - return true; -} - -bool Telegram::parseWMBUS (std::vector &input_frame, MeterKeys *mk, bool warn) -{ - assert(about.type == FrameType::WMBUS); - - parser_warns_ = warn; - decryption_failed = false; - // explanations.clear(); - suffix_size = 0; - meter_keys = mk; - assert(meter_keys != NULL); - bool ok; - frame = input_frame; - std::vector::iterator pos = frame.begin(); - // Parsed accumulates parsed bytes. - parsed.clear(); - // Fixes quirks from non-compliant meters to make telegram compatible with the standard - preProcess(); - // ┌──────────────────────────────────────────────┐ - // │ │ - // │ Parse DLL Data Link Layer for Wireless MBUS. │ - // │ │ - // └──────────────────────────────────────────────┘ - - ok = parseDLL(pos); - if (!ok) return false; - - printDLL(); - - // ┌──────────────────────────────────────────────┐ - // │ │ - // │ Is this an ELL block? │ - // │ │ - // └──────────────────────────────────────────────┘ - - ok = parseELL(pos); - if (!ok) return false; - - printELL(); - if (decryption_failed) return false; - - // ┌──────────────────────────────────────────────┐ - // │ │ - // │ Is this an NWL block? │ - // │ │ - // └──────────────────────────────────────────────┘ - - ok = parseNWL(pos); - if (!ok) return false; - - printNWL(); - - // ┌──────────────────────────────────────────────┐ - // │ │ - // │ Is this an AFL block? │ - // │ │ - // └──────────────────────────────────────────────┘ - - ok = parseAFL(pos); - if (!ok) return false; - - printAFL(); - - // ┌──────────────────────────────────────────────┐ - // │ │ - // │ Is this a TPL block? It ought to be! │ - // │ │ - // └──────────────────────────────────────────────┘ - - ok = parseTPL(pos); - if (!ok) return false; - - printTPL(); - if (decryption_failed) return false; - - return true; -} - -bool Telegram::parseMBUSHeader (std::vector &input_frame) -{ - assert(about.type == FrameType::MBUS); - - bool ok; - // Parsing the header is used to extract the ids, so that we can - // match the telegram towards any known ids and thus keys. - // No need to warn. - parser_warns_ = false; - decryption_failed = false; - // explanations.clear(); - suffix_size = 0; - frame = input_frame; - std::vector::iterator pos = frame.begin(); - // Parsed accumulates parsed bytes. - parsed.clear(); - - ok = parseMBusDLLandTPL(pos); - if (!ok) return false; - - return true; -} - -bool Telegram::parseMBUS (std::vector &input_frame, MeterKeys *mk, bool warn) -{ - assert(about.type == FrameType::MBUS); - - parser_warns_ = warn; - decryption_failed = false; - // explanations.clear(); - suffix_size = 0; - meter_keys = mk; - assert(meter_keys != NULL); - bool ok; - frame = input_frame; - std::vector::iterator pos = frame.begin(); - // Parsed accumulates parsed bytes. - parsed.clear(); - - // ┌──────────────────────────────────────────────┐ - // │ │ - // │ Parse DLL Data Link Layer for Wireless MBUS. │ - // │ │ - // └──────────────────────────────────────────────┘ - - ok = parseMBusDLLandTPL(pos); - if (!ok) return false; - - return true; -} - -bool Telegram::parseHANHeader (std::vector &input_frame) -{ - assert(about.type == FrameType::HAN); - - return false; -} - -bool Telegram::parseHAN (std::vector &input_frame, MeterKeys *mk, bool warn) -{ - assert(about.type == FrameType::HAN); - - return false; -} - -void Telegram::explainParse(std::string intro, int from) -{ - // for (auto& p : explanations) - // { - // // Protocol or content? - // const char *c = p.kind == KindOfData::PROTOCOL ? " " : "C"; - // const char *u = "?"; - // if (p.understanding == Understanding::FULL) u = "!"; - // if (p.understanding == Understanding::PARTIAL) u = "p"; - // if (p.understanding == Understanding::ENCRYPTED) u = "E"; - // if (p.understanding == Understanding::COMPRESSED) u = "C"; - - // // Do not print ok for understood protocol, it is implicit. - // // However if a protocol is not full understood then print p or ?. - // if (p.kind == KindOfData::PROTOCOL && p.understanding == Understanding::FULL) u = " "; - - // debug("%s %03d %s%s: %s\n", intro.c_str(), p.pos, c, u, p.info.c_str()); - // } -} - -// std::string renderAnalysisAsText (std::vector &explanations, OutputFormat of) -// { -// std::string s; - -// const char *green; -// const char *yellow; -// const char *red; -// const char *reset; - -// if (of == OutputFormat::TERMINAL) -// { -// green = "\033[0;97m\033[1;42m"; -// yellow = "\033[0;97m\033[0;43m"; -// red = "\033[0;97m\033[0;41m\033[1;37m"; -// reset = "\033[0m"; -// } -// else if (of == OutputFormat::HTML) -// { -// green = ""; -// yellow = ""; -// red = ""; -// reset = ""; -// } -// else -// { -// green = ""; -// yellow = ""; -// red = ""; -// reset = ""; -// } - -// for (auto& p : explanations) -// { -// // Protocol or content? -// const char *c = p.kind == KindOfData::PROTOCOL ? " " : "C"; -// const char *u = "?"; -// if (p.understanding == Understanding::FULL) u = "!"; -// if (p.understanding == Understanding::PARTIAL) u = "p"; -// if (p.understanding == Understanding::ENCRYPTED) u = "E"; -// if (p.understanding == Understanding::COMPRESSED) u = "C"; - -// // Do not print ok for understood protocol, it is implicit. -// // However if a protocol is not full understood then print p or ?. -// if (p.kind == KindOfData::PROTOCOL && p.understanding == Understanding::FULL) u = " "; - -// const char *pre; -// const char *post = reset; - -// if (*u == '!') -// { -// pre = green; -// } -// else if (*u == 'p') -// { -// pre = yellow; -// } -// else if (*u == ' ') -// { -// pre = ""; -// post = ""; -// } -// else -// { -// pre = red; -// } - -// s += tostrprintf("%03d %s%s: %s%s%s\n", p.pos, c, u, pre, p.info.c_str(), post); -// } -// return s; -// } - -// std::string renderAnalysisAsJson (std::vector &explanations) -// { -// return "{ \"TODO\": true }\n"; -// } - -std::string Telegram::analyzeParse(OutputFormat format, int *content_length, int *understood_content_length) -{ - // int u = 0; - // int l = 0; - - // sort(explanations.begin(), explanations.end(), - // [](const Explanation & a, const Explanation & b) -> bool { return a.pos < b.pos; }); - - // // Calculate how much is understood. - // for (auto& e : explanations) - // { - // if (e.kind == KindOfData::CONTENT) - // { - // l += e.len; - // if (e.understanding == Understanding::PARTIAL || - // e.understanding == Understanding::FULL) - // { - // // Its content and we have at least some understanding. - // u += e.len; - // } - // } - // } - // *content_length = l; - // *understood_content_length = u; - - // switch(format) - // { - // case OutputFormat::PLAIN : - // case OutputFormat::HTML : - // case OutputFormat::TERMINAL: - // { - // return renderAnalysisAsText(explanations, format); - // break; - // } - // case OutputFormat::JSON: - // return renderAnalysisAsJson(explanations); - // break; - // case OutputFormat::NONE: - // // Do nothing - // return ""; - // break; - // } - return "ERROR"; -} - -void detectMeterDrivers(int manufacturer, int version, int type, std::vector *drivers); - -std::string Telegram::autoDetectPossibleDrivers() -{ - std::vector drivers; - detectMeterDrivers(dll_mfct, dll_version, dll_type, &drivers); - if (tpl_id_found) - { - detectMeterDrivers(tpl_mfct, tpl_version, tpl_type, &drivers); - } - std::string possibles; - for (std::string d : drivers) possibles = possibles+d+" "; - if (possibles != "") possibles.pop_back(); - else possibles = "unknown!"; - - return possibles; -} - -const char *mbusCField(uchar c_field) -{ - std::string s; - switch (c_field) - { - case 0x08: return "RSP_UD2"; - } - return "?"; -} - -const char *mbusCiField(uchar c_field) -{ - std::string s; - switch (c_field) - { - case 0x78: return "no header"; - case 0x7a: return "short header"; - case 0x72: return "long header"; - case 0x79: return "no header compact frame"; - case 0x7b: return "short header compact frame"; - case 0x73: return "long header compact frame"; - case 0x69: return "no header format frame"; - case 0x6a: return "short header format frame"; - case 0x6b: return "long header format frame"; - } - return "?"; -} - -std::string cType(int c_field) -{ - std::string s; - if (c_field & 0x80) - { - s += "relayed "; - } - - if (c_field & 0x40) - { - s += "from meter "; - } - else - { - s += "to meter "; - } - - int code = c_field & 0x0f; - - switch (code) { - case 0x0: s += "SND_NKE"; break; // to meter, link reset - case 0x3: s += "SND_UD2"; break; // to meter, command = user data - case 0x4: s += "SND_NR"; break; // from meter, unsolicited data, no response expected - case 0x5: s += "SND_UD3"; break; // to multiple meters, command = user data, no response expected - case 0x6: s += "SND_IR"; break; // from meter, installation request/data - case 0x7: s += "ACC_NR"; break; // from meter, unsolicited offers to access the meter - case 0x8: s += "ACC_DMD"; break; // from meter, unsolicited demand to access the meter - case 0xa: s += "REQ_UD1"; break; // to meter, alarm request - case 0xb: s += "REQ_UD2"; break; // to meter, data request - } - - return s; -} - -bool isValidWMBusCField(int c_field) -{ - // These are the currently seen valid C fields for wmbus telegrams. - // 0x46 is only from an ei6500 meter.... all else is ox44 - // However in the future we might see relayed telegrams which will perhaps have - // some other c field. - return - c_field == 0x44 || - c_field == 0x46; -} - -bool isValidMBusCField(int c_field) -{ - return false; -} - -std::string ccType(int cc_field) -{ - std::string s = ""; - if (cc_field & CC_B_BIDIRECTIONAL_BIT) s += "bidir "; - if (cc_field & CC_RD_RESPONSE_DELAY_BIT) s += "fast_resp "; - else s += "slow_resp "; - if (cc_field & CC_S_SYNCH_FRAME_BIT) s += "sync "; - if (cc_field & CC_R_RELAYED_BIT) s+= "relayed "; // Relayed by a repeater - if (cc_field & CC_P_HIGH_PRIO_BIT) s+= "prio "; - - if (s.size() > 0 && s.back() == ' ') s.pop_back(); - return s; -} - - -int difLenBytes(int dif) -{ - int t = dif & 0x0f; - switch (t) { - case 0x0: return 0; // No data - case 0x1: return 1; // 8 Bit Integer/Binary - case 0x2: return 2; // 16 Bit Integer/Binary - case 0x3: return 3; // 24 Bit Integer/Binary - case 0x4: return 4; // 32 Bit Integer/Binary - case 0x5: return 4; // 32 Bit Real - case 0x6: return 6; // 48 Bit Integer/Binary - case 0x7: return 8; // 64 Bit Integer/Binary - case 0x8: return 0; // Selection for Readout - case 0x9: return 1; // 2 digit BCD - case 0xA: return 2; // 4 digit BCD - case 0xB: return 3; // 6 digit BCD - case 0xC: return 4; // 8 digit BCD - case 0xD: return -1; // variable length - case 0xE: return 6; // 12 digit BCD - case 0xF: // Special Functions - if (dif == 0x2f) return 1; // The skip code 0x2f, used for padding. - return -2; - } - // Bad! - return -2; -} - -std::string difType(int dif) -{ - std::string s; - int t = dif & 0x0f; - switch (t) { - case 0x0: s+= "No data"; break; - case 0x1: s+= "8 Bit Integer/Binary"; break; - case 0x2: s+= "16 Bit Integer/Binary"; break; - case 0x3: s+= "24 Bit Integer/Binary"; break; - case 0x4: s+= "32 Bit Integer/Binary"; break; - case 0x5: s+= "32 Bit Real"; break; - case 0x6: s+= "48 Bit Integer/Binary"; break; - case 0x7: s+= "64 Bit Integer/Binary"; break; - case 0x8: s+= "Selection for Readout"; break; - case 0x9: s+= "2 digit BCD"; break; - case 0xA: s+= "4 digit BCD"; break; - case 0xB: s+= "6 digit BCD"; break; - case 0xC: s+= "8 digit BCD"; break; - case 0xD: s+= "variable length"; break; - case 0xE: s+= "12 digit BCD"; break; - case 0xF: s+= "Special Functions"; break; - default: s+= "?"; break; - } - - if (t != 0xf) - { - // Only print these suffixes when we have actual values. - t = dif & 0x30; - - switch (t) { - case 0x00: s += " Instantaneous value"; break; - case 0x10: s += " Maximum value"; break; - case 0x20: s += " Minimum value"; break; - case 0x30: s+= " Value during error state"; break; - default: s += "?"; break; - } - } - if (dif & 0x40) { - // This is the lsb of the storage nr. - s += " storagenr=1"; - } - return s; -} - -MeasurementType difMeasurementType(int dif) -{ - int t = dif & 0x30; - switch (t) { - case 0x00: return MeasurementType::Instantaneous; - case 0x10: return MeasurementType::Maximum; - case 0x20: return MeasurementType::Minimum; - case 0x30: return MeasurementType::AtError; - } - assert(0); -} - -std::string vifType(int vif) -{ - // Remove any remaining 0x80 top bits. - vif &= 0x7f7f; - - switch (vif) - { - case 0x00: return "Energy mWh"; - case 0x01: return "Energy 10⁻² Wh"; - case 0x02: return "Energy 10⁻¹ Wh"; - case 0x03: return "Energy Wh"; - case 0x04: return "Energy 10¹ Wh"; - case 0x05: return "Energy 10² Wh"; - case 0x06: return "Energy kWh"; - case 0x07: return "Energy 10⁴ Wh"; - - case 0x08: return "Energy J"; - case 0x09: return "Energy 10¹ J"; - case 0x0A: return "Energy 10² J"; - case 0x0B: return "Energy kJ"; - case 0x0C: return "Energy 10⁴ J"; - case 0x0D: return "Energy 10⁵ J"; - case 0x0E: return "Energy MJ"; - case 0x0F: return "Energy 10⁷ J"; - - case 0x10: return "Volume cm³"; - case 0x11: return "Volume 10⁻⁵ m³"; - case 0x12: return "Volume 10⁻⁴ m³"; - case 0x13: return "Volume l"; - case 0x14: return "Volume 10⁻² m³"; - case 0x15: return "Volume 10⁻¹ m³"; - case 0x16: return "Volume m³"; - case 0x17: return "Volume 10¹ m³"; - - case 0x18: return "Mass g"; - case 0x19: return "Mass 10⁻² kg"; - case 0x1A: return "Mass 10⁻¹ kg"; - case 0x1B: return "Mass kg"; - case 0x1C: return "Mass 10¹ kg"; - case 0x1D: return "Mass 10² kg"; - case 0x1E: return "Mass t"; - case 0x1F: return "Mass 10⁴ kg"; - - case 0x20: return "On time seconds"; - case 0x21: return "On time minutes"; - case 0x22: return "On time hours"; - case 0x23: return "On time days"; - - case 0x24: return "Operating time seconds"; - case 0x25: return "Operating time minutes"; - case 0x26: return "Operating time hours"; - case 0x27: return "Operating time days"; - - case 0x28: return "Power mW"; - case 0x29: return "Power 10⁻² W"; - case 0x2A: return "Power 10⁻¹ W"; - case 0x2B: return "Power W"; - case 0x2C: return "Power 10¹ W"; - case 0x2D: return "Power 10² W"; - case 0x2E: return "Power kW"; - case 0x2F: return "Power 10⁴ W"; - - case 0x30: return "Power J/h"; - case 0x31: return "Power 10¹ J/h"; - case 0x32: return "Power 10² J/h"; - case 0x33: return "Power kJ/h"; - case 0x34: return "Power 10⁴ J/h"; - case 0x35: return "Power 10⁵ J/h"; - case 0x36: return "Power MJ/h"; - case 0x37: return "Power 10⁷ J/h"; - - case 0x38: return "Volume flow cm³/h"; - case 0x39: return "Volume flow 10⁻⁵ m³/h"; - case 0x3A: return "Volume flow 10⁻⁴ m³/h"; - case 0x3B: return "Volume flow l/h"; - case 0x3C: return "Volume flow 10⁻² m³/h"; - case 0x3D: return "Volume flow 10⁻¹ m³/h"; - case 0x3E: return "Volume flow m³/h"; - case 0x3F: return "Volume flow 10¹ m³/h"; - - case 0x40: return "Volume flow ext. 10⁻⁷ m³/min"; - case 0x41: return "Volume flow ext. cm³/min"; - case 0x42: return "Volume flow ext. 10⁻⁵ m³/min"; - case 0x43: return "Volume flow ext. 10⁻⁴ m³/min"; - case 0x44: return "Volume flow ext. l/min"; - case 0x45: return "Volume flow ext. 10⁻² m³/min"; - case 0x46: return "Volume flow ext. 10⁻¹ m³/min"; - case 0x47: return "Volume flow ext. m³/min"; - - case 0x48: return "Volume flow ext. mm³/s"; - case 0x49: return "Volume flow ext. 10⁻⁸ m³/s"; - case 0x4A: return "Volume flow ext. 10⁻⁷ m³/s"; - case 0x4B: return "Volume flow ext. cm³/s"; - case 0x4C: return "Volume flow ext. 10⁻⁵ m³/s"; - case 0x4D: return "Volume flow ext. 10⁻⁴ m³/s"; - case 0x4E: return "Volume flow ext. l/s"; - case 0x4F: return "Volume flow ext. 10⁻² m³/s"; - - case 0x50: return "Mass g/h"; - case 0x51: return "Mass 10⁻² kg/h"; - case 0x52: return "Mass 10⁻¹ kg/h"; - case 0x53: return "Mass kg/h"; - case 0x54: return "Mass 10¹ kg/h"; - case 0x55: return "Mass 10² kg/h"; - case 0x56: return "Mass t/h"; - case 0x57: return "Mass 10⁴ kg/h"; - - case 0x58: return "Flow temperature 10⁻³ °C"; - case 0x59: return "Flow temperature 10⁻² °C"; - case 0x5A: return "Flow temperature 10⁻¹ °C"; - case 0x5B: return "Flow temperature °C"; - - case 0x5C: return "Return temperature 10⁻³ °C"; - case 0x5D: return "Return temperature 10⁻² °C"; - case 0x5E: return "Return temperature 10⁻¹ °C"; - case 0x5F: return "Return temperature °C"; - - case 0x60: return "Temperature difference 10⁻³ K/°C"; - case 0x61: return "Temperature difference 10⁻² K/°C"; - case 0x62: return "Temperature difference 10⁻¹ K/°C"; - case 0x63: return "Temperature difference K/°C"; - - case 0x64: return "External temperature 10⁻³ °C"; - case 0x65: return "External temperature 10⁻² °C"; - case 0x66: return "External temperature 10⁻¹ °C"; - case 0x67: return "External temperature °C"; - - case 0x68: return "Pressure mbar"; - case 0x69: return "Pressure 10⁻² bar"; - case 0x6A: return "Pressure 10⁻¹ bar"; - case 0x6B: return "Pressure bar"; - - case 0x6C: return "Date type G"; - case 0x6D: return "Date and time type"; - - case 0x6E: return "Units for H.C.A."; - case 0x6F: return "Third extension 6F of VIF-codes"; - - case 0x70: return "Averaging duration seconds"; - case 0x71: return "Averaging duration minutes"; - case 0x72: return "Averaging duration hours"; - case 0x73: return "Averaging duration days"; - - case 0x74: return "Actuality duration seconds"; - case 0x75: return "Actuality duration minutes"; - case 0x76: return "Actuality duration hours"; - case 0x77: return "Actuality duration days"; - - case 0x78: return "Fabrication no"; - case 0x79: return "Enhanced identification"; - - case 0x7B: return "First extension FB of VIF-codes"; - case 0x7C: return "VIF in following std::string (length in first byte)"; - case 0x7D: return "Second extension FD of VIF-codes"; - - case 0x7E: return "Any VIF"; - case 0x7F: return "Manufacturer specific"; - - case 0x7B00: return "Active Energy 0.1 MWh"; - case 0x7B01: return "Active Energy 1 MWh"; - - case 0x7B1A: return "Relative humidity 0.1%"; - case 0x7B1B: return "Relative humidity 1%"; - - default: return "?"; - } -} - -double vifScale(int vif) -{ - // Remove any remaining 0x80 top bits. - vif &= 0x7f7f; - - switch (vif) { - // wmbusmeters always returns enery as kwh - case 0x00: return 1000000.0; // Energy mWh - case 0x01: return 100000.0; // Energy 10⁻² Wh - case 0x02: return 10000.0; // Energy 10⁻¹ Wh - case 0x03: return 1000.0; // Energy Wh - case 0x04: return 100.0; // Energy 10¹ Wh - case 0x05: return 10.0; // Energy 10² Wh - case 0x06: return 1.0; // Energy kWh - case 0x07: return 0.1; // Energy 10⁴ Wh - - // or wmbusmeters always returns energy as MJ - case 0x08: return 1000000.0; // Energy J - case 0x09: return 100000.0; // Energy 10¹ J - case 0x0A: return 10000.0; // Energy 10² J - case 0x0B: return 1000.0; // Energy kJ - case 0x0C: return 100.0; // Energy 10⁴ J - case 0x0D: return 10.0; // Energy 10⁵ J - case 0x0E: return 1.0; // Energy MJ - case 0x0F: return 0.1; // Energy 10⁷ J - - // wmbusmeters always returns volume as m3 - case 0x10: return 1000000.0; // Volume cm³ - case 0x11: return 100000.0; // Volume 10⁻⁵ m³ - case 0x12: return 10000.0; // Volume 10⁻⁴ m³ - case 0x13: return 1000.0; // Volume l - case 0x14: return 100.0; // Volume 10⁻² m³ - case 0x15: return 10.0; // Volume 10⁻¹ m³ - case 0x16: return 1.0; // Volume m³ - case 0x17: return 0.1; // Volume 10¹ m³ - - // wmbusmeters always returns weight in kg - case 0x18: return 1000.0; // Mass g - case 0x19: return 100.0; // Mass 10⁻² kg - case 0x1A: return 10.0; // Mass 10⁻¹ kg - case 0x1B: return 1.0; // Mass kg - case 0x1C: return 0.1; // Mass 10¹ kg - case 0x1D: return 0.01; // Mass 10² kg - case 0x1E: return 0.001; // Mass t - case 0x1F: return 0.0001; // Mass 10⁴ kg - - // wmbusmeters always returns time in hours - case 0x20: return 3600.0; // On time seconds - case 0x21: return 60.0; // On time minutes - case 0x22: return 1.0; // On time hours - case 0x23: return (1.0/24.0); // On time days - - case 0x24: return 3600.0; // Operating time seconds - case 0x25: return 60.0; // Operating time minutes - case 0x26: return 1.0; // Operating time hours - case 0x27: return (1.0/24.0); // Operating time days - - // wmbusmeters always returns power in kw - case 0x28: return 1000000.0; // Power mW - case 0x29: return 100000.0; // Power 10⁻² W - case 0x2A: return 10000.0; // Power 10⁻¹ W - case 0x2B: return 1000.0; // Power W - case 0x2C: return 100.0; // Power 10¹ W - case 0x2D: return 10.0; // Power 10² W - case 0x2E: return 1.0; // Power kW - case 0x2F: return 0.1; // Power 10⁴ W - - // or wmbusmeters always returns power in MJh - case 0x30: return 1000000.0; // Power J/h - case 0x31: return 100000.0; // Power 10¹ J/h - case 0x32: return 10000.0; // Power 10² J/h - case 0x33: return 1000.0; // Power kJ/h - case 0x34: return 100.0; // Power 10⁴ J/h - case 0x35: return 10.0; // Power 10⁵ J/h - case 0x36: return 1.0; // Power MJ/h - case 0x37: return 0.1; // Power 10⁷ J/h - - // wmbusmeters always returns volume flow in m3h - case 0x38: return 1000000.0; // Volume flow cm³/h - case 0x39: return 100000.0; // Volume flow 10⁻⁵ m³/h - case 0x3A: return 10000.0; // Volume flow 10⁻⁴ m³/h - case 0x3B: return 1000.0; // Volume flow l/h - case 0x3C: return 100.0; // Volume flow 10⁻² m³/h - case 0x3D: return 10.0; // Volume flow 10⁻¹ m³/h - case 0x3E: return 1.0; // Volume flow m³/h - case 0x3F: return 0.1; // Volume flow 10¹ m³/h - - // wmbusmeters always returns volume flow in m3h - case 0x40: return 600000000.0; // Volume flow ext. 10⁻⁷ m³/min - case 0x41: return 60000000.0; // Volume flow ext. cm³/min - case 0x42: return 6000000.0; // Volume flow ext. 10⁻⁵ m³/min - case 0x43: return 600000.0; // Volume flow ext. 10⁻⁴ m³/min - case 0x44: return 60000.0; // Volume flow ext. l/min - case 0x45: return 6000.0; // Volume flow ext. 10⁻² m³/min - case 0x46: return 600.0; // Volume flow ext. 10⁻¹ m³/min - case 0x47: return 60.0; // Volume flow ext. m³/min - - // this flow numbers will be small in the m3h unit, but it - // does not matter since double stores the scale factor in its exponent. - case 0x48: return 1000000000.0*3600; // Volume flow ext. mm³/s - case 0x49: return 100000000.0*3600; // Volume flow ext. 10⁻⁸ m³/s - case 0x4A: return 10000000.0*3600; // Volume flow ext. 10⁻⁷ m³/s - case 0x4B: return 1000000.0*3600; // Volume flow ext. cm³/s - case 0x4C: return 100000.0*3600; // Volume flow ext. 10⁻⁵ m³/s - case 0x4D: return 10000.0*3600; // Volume flow ext. 10⁻⁴ m³/s - case 0x4E: return 1000.0*3600; // Volume flow ext. l/s - case 0x4F: return 100.0*3600; // Volume flow ext. 10⁻² m³/s - - // wmbusmeters always returns mass flow as kgh - case 0x50: return 1000.0; // Mass g/h - case 0x51: return 100.0; // Mass 10⁻² kg/h - case 0x52: return 10.0; // Mass 10⁻¹ kg/h - case 0x53: return 1.0; // Mass kg/h - case 0x54: return 0.1; // Mass 10¹ kg/h - case 0x55: return 0.01; // Mass 10² kg/h - case 0x56: return 0.001; // Mass t/h - case 0x57: return 0.0001; // Mass 10⁴ kg/h - - // wmbusmeters always returns temperature in °C - case 0x58: return 1000.0; // Flow temperature 10⁻³ °C - case 0x59: return 100.0; // Flow temperature 10⁻² °C - case 0x5A: return 10.0; // Flow temperature 10⁻¹ °C - case 0x5B: return 1.0; // Flow temperature °C - - // wmbusmeters always returns temperature in c - case 0x5C: return 1000.0; // Return temperature 10⁻³ °C - case 0x5D: return 100.0; // Return temperature 10⁻² °C - case 0x5E: return 10.0; // Return temperature 10⁻¹ °C - case 0x5F: return 1.0; // Return temperature °C - - // or if Kelvin is used as a temperature, in K - // what kind of meter cares about -273.15 °C - // a flow pump for liquid helium perhaps? - case 0x60: return 1000.0; // Temperature difference mK - case 0x61: return 100.0; // Temperature difference 10⁻² K - case 0x62: return 10.0; // Temperature difference 10⁻¹ K - case 0x63: return 1.0; // Temperature difference K - - // wmbusmeters always returns temperature in c - case 0x64: return 1000.0; // External temperature 10⁻³ °C - case 0x65: return 100.0; // External temperature 10⁻² °C - case 0x66: return 10.0; // External temperature 10⁻¹ °C - case 0x67: return 1.0; // External temperature °C - - // wmbusmeters always returns pressure in bar - case 0x68: return 1000.0; // Pressure mbar - case 0x69: return 100.0; // Pressure 10⁻² bar - case 0x6A: return 10.0; // Pressure 10⁻¹ bar - case 0x6B: return 1.0; // Pressure bar - - case 0x6C: return 1.0; // Date type G - case 0x6D: return 1.0; // Date&Time type F - case 0x6E: return 1.0; // Units for H.C.A. are never scaled - case 0x6F: warning("(wmbus) warning: do not scale a reserved type!\n"); return -1.0; // Reserved - - // wmbusmeters always returns time in hours - case 0x70: return 3600.0; // Averaging duration seconds - case 0x71: return 60.0; // Averaging duration minutes - case 0x72: return 1.0; // Averaging duration hours - case 0x73: return (1.0/24.0); // Averaging duration days - - // wmbusmeters always returns time in hours - case 0x74: return 3600.0; // Actuality duration seconds - case 0x75: return 60.0; // Actuality duration minutes - case 0x76: return 1.0; // Actuality duration hours - case 0x77: return (1.0/24.0); // Actuality duration days - - // Active energy 0.1 or 1 MWh normalize to 100 KWh or 1000 KWh - // 7b00 33632 -> 3363.2 MWh -> 3363200 KWh - // 7b01 33632 -> 33632 MWh -> 33632000 KWh - case 0x7b00: - case 0x7b01: { double exp = (vif & 0x1)+2; return pow(10.0, -exp); } - - // Active energy 0.1 or 1 GJ normalize to 100 MJ or 1000 MJ - // 7b08 19 -> 1.9 G -> 1 900 KWh - // 7b09 19 -> 19 GJ -> 19 000 MJ - case 0x7b08: - case 0x7b09: { double exp = (vif & 0x1)+2; return pow(10.0, -exp); } - - // relative humidity is a dimensionless value. - case 0x7b1a: return 10.0; // Relative humidity 0.1 % - case 0x7b1b: return 1.0; // Relative humidity 1 % - - // wmbusmeters always returns time in hours - // 0x7d30 is not supposed to be used according to spec. - case 0x7d31: return 60.0; // Duration tariff minutes - case 0x7d32: return 1.0; // Duration tariff hours - case 0x7d33: return (1.0/24.0); // Duration tariff days - - // wmbusmeters always voltage in volts - case 0x7d40: - case 0x7d41: - case 0x7d42: - case 0x7d43: - case 0x7d44: - case 0x7d45: - case 0x7d46: - case 0x7d47: - case 0x7d48: - case 0x7d49: - case 0x7d4a: - case 0x7d4b: - case 0x7d4c: - case 0x7d4d: - case 0x7d4e: - case 0x7d4f: { double exp = (vif & 0xf)-9; return pow(10.0, -exp); } - - // wmbusmeters always return current in ampere - case 0x7d50: - case 0x7d51: - case 0x7d52: - case 0x7d53: - case 0x7d54: - case 0x7d55: - case 0x7d56: - case 0x7d57: - case 0x7d58: - case 0x7d59: - case 0x7d5a: - case 0x7d5b: - case 0x7d5c: - case 0x7d5d: - case 0x7d5e: - case 0x7d5f: { double exp = (vif & 0xf)-12; return pow(10.0, -exp); } - - // for remaining battery wmbusmeters returns number of days. - case 0x7d74: { return 1.0; } - - // wmbusmeters always returns time in hours - case 0x7d2c: return 3600.0; // Duration since readout seconds - case 0x7d2d: return 60.0; // Duration since readout minutes - case 0x7d2e: return 1.0; // Duration since readout hours - case 0x7d2f: return (1.0/24.0); // Duration since readout days - - /* - case 0x78: // Fabrication no - case 0x79: // Enhanced identification - case 0x80: // Address - - case 0x7C: // VIF in following std::string (length in first byte) - case 0x7E: // Any VIF - case 0x7F: // Manufacturer specific - */ - - default: warning("(wmbus) warning: type 0x%x cannot be scaled!\n", vif); - return -1; - } -} - -std::string vifKey(int vif) -{ - int t = vif & 0x7f; - - switch (t) { - - case 0x00: - case 0x01: - case 0x02: - case 0x03: - case 0x04: - case 0x05: - case 0x06: - case 0x07: return "energy"; - - case 0x08: - case 0x09: - case 0x0A: - case 0x0B: - case 0x0C: - case 0x0D: - case 0x0E: - case 0x0F: return "energy"; - - case 0x10: - case 0x11: - case 0x12: - case 0x13: - case 0x14: - case 0x15: - case 0x16: - case 0x17: return "volume"; - - case 0x18: - case 0x19: - case 0x1A: - case 0x1B: - case 0x1C: - case 0x1D: - case 0x1E: - case 0x1F: return "mass"; - - case 0x20: - case 0x21: - case 0x22: - case 0x23: return "on_time"; - - case 0x24: - case 0x25: - case 0x26: - case 0x27: return "operating_time"; - - case 0x28: - case 0x29: - case 0x2A: - case 0x2B: - case 0x2C: - case 0x2D: - case 0x2E: - case 0x2F: return "power"; - - case 0x30: - case 0x31: - case 0x32: - case 0x33: - case 0x34: - case 0x35: - case 0x36: - case 0x37: return "power"; - - case 0x38: - case 0x39: - case 0x3A: - case 0x3B: - case 0x3C: - case 0x3D: - case 0x3E: - case 0x3F: return "volume_flow"; - - case 0x40: - case 0x41: - case 0x42: - case 0x43: - case 0x44: - case 0x45: - case 0x46: - case 0x47: return "volume_flow_ext"; - - case 0x48: - case 0x49: - case 0x4A: - case 0x4B: - case 0x4C: - case 0x4D: - case 0x4E: - case 0x4F: return "volume_flow_ext"; - - case 0x50: - case 0x51: - case 0x52: - case 0x53: - case 0x54: - case 0x55: - case 0x56: - case 0x57: return "mass_flow"; - - case 0x58: - case 0x59: - case 0x5A: - case 0x5B: return "flow_temperature"; - - case 0x5C: - case 0x5D: - case 0x5E: - case 0x5F: return "return_temperature"; - - case 0x60: - case 0x61: - case 0x62: - case 0x63: return "temperature_difference"; - - case 0x64: - case 0x65: - case 0x66: - case 0x67: return "external_temperature"; - - case 0x68: - case 0x69: - case 0x6A: - case 0x6B: return "pressure"; - - case 0x6C: return "date"; // Date type G - case 0x6E: return "hca"; // Units for H.C.A. - case 0x6F: return "reserved"; // Reserved - - case 0x70: - case 0x71: - case 0x72: - case 0x73: return "average_duration"; - - case 0x74: - case 0x75: - case 0x76: - case 0x77: return "actual_duration"; - - case 0x78: return "fabrication_no"; // Fabrication no - case 0x79: return "enhanced_identification"; // Enhanced identification - - case 0x7C: // VIF in following std::string (length in first byte) - case 0x7E: // Any VIF - case 0x7F: // Manufacturer specific - - default: warning("(wmbus) warning: generic type %d cannot be scaled!\n", t); - return "unknown"; - } -} - -std::string vifUnit(int vif) -{ - int t = vif & 0x7f; - - switch (t) { - - case 0x00: - case 0x01: - case 0x02: - case 0x03: - case 0x04: - case 0x05: - case 0x06: - case 0x07: return "kwh"; - - case 0x08: - case 0x09: - case 0x0A: - case 0x0B: - case 0x0C: - case 0x0D: - case 0x0E: - case 0x0F: return "MJ"; - - case 0x10: - case 0x11: - case 0x12: - case 0x13: - case 0x14: - case 0x15: - case 0x16: - case 0x17: return "m3"; - - case 0x18: - case 0x19: - case 0x1A: - case 0x1B: - case 0x1C: - case 0x1D: - case 0x1E: - case 0x1F: return "kg"; - - case 0x20: - case 0x21: - case 0x22: - case 0x23: - case 0x24: - case 0x25: - case 0x26: - case 0x27: return "h"; - - case 0x28: - case 0x29: - case 0x2A: - case 0x2B: - case 0x2C: - case 0x2D: - case 0x2E: - case 0x2F: return "kw"; - - case 0x30: - case 0x31: - case 0x32: - case 0x33: - case 0x34: - case 0x35: - case 0x36: - case 0x37: return "MJ"; - - case 0x38: - case 0x39: - case 0x3A: - case 0x3B: - case 0x3C: - case 0x3D: - case 0x3E: - case 0x3F: return "m3/h"; - - case 0x40: - case 0x41: - case 0x42: - case 0x43: - case 0x44: - case 0x45: - case 0x46: - case 0x47: return "m3/h"; - - case 0x48: - case 0x49: - case 0x4A: - case 0x4B: - case 0x4C: - case 0x4D: - case 0x4E: - case 0x4F: return "m3/h"; - - case 0x50: - case 0x51: - case 0x52: - case 0x53: - case 0x54: - case 0x55: - case 0x56: - case 0x57: return "kg/h"; - - case 0x58: - case 0x59: - case 0x5A: - case 0x5B: return "c"; - - case 0x5C: - case 0x5D: - case 0x5E: - case 0x5F: return "c"; - - case 0x60: - case 0x61: - case 0x62: - case 0x63: return "k"; - - case 0x64: - case 0x65: - case 0x66: - case 0x67: return "c"; - - case 0x68: - case 0x69: - case 0x6A: - case 0x6B: return "bar"; - - case 0x6C: return ""; // Date type G - case 0x6D: return ""; // ?? - case 0x6E: return ""; // Units for H.C.A. - case 0x6F: return ""; // Reserved - - case 0x70: - case 0x71: - case 0x72: - case 0x73: return "h"; - - case 0x74: - case 0x75: - case 0x76: - case 0x77: return "h"; - - case 0x78: return ""; // Fabrication no - case 0x79: return ""; // Enhanced identification - - case 0x7C: // VIF in following std::string (length in first byte) - case 0x7E: // Any VIF - case 0x7F: // Manufacturer specific - - default: warning("(wmbus) warning: generic type %d cannot be scaled!\n", t); - return "unknown"; - } -} - -const char *timeNN(int nn) { - switch (nn) { - case 0: return "second(s)"; - case 1: return "minute(s)"; - case 2: return "hour(s)"; - case 3: return "day(s)"; - } - return "?"; -} - -const char *timePP(int nn) { - switch (nn) { - case 0: return "hour(s)"; - case 1: return "day(s)"; - case 2: return "month(s)"; - case 3: return "year(s)"; - } - return "?"; -} - -std::string vif_7B_FirstExtensionType(uchar dif, uchar vif, uchar vife) -{ - assert(vif == 0xfb); - - if ((vife & 0x7e) == 0x00) { - int n = vife & 0x01; - std::string s; - strprintf(&s, "10^%d MWh", n-1); - return s; - } - - if (((vife & 0x7e) == 0x02) || - ((vife & 0x7c) == 0x04)) { - return "Reserved"; - } - - if ((vife & 0x7e) == 0x08) { - int n = vife & 0x01; - std::string s; - strprintf(&s, "10^%d GJ", n-1); - return s; - } - - if ((vife & 0x7e) == 0x0a || - (vife & 0x7c) == 0x0c) { - return "Reserved"; - } - - if ((vife & 0x7e) == 0x10) { - int n = vife & 0x01; - std::string s; - strprintf(&s, "10^%d m3", n+2); - return s; - } - - if ((vife & 0x7e) == 0x12 || - (vife & 0x7c) == 0x14) { - return "Reserved"; - } - - if ((vife & 0x7e) == 0x18) { - int n = vife & 0x01; - std::string s; - strprintf(&s, "10^%d ton", n+2); - return s; - } - - if ((vife & 0x7e) == 0x1a) { - int n = vife & 0x01; - std::string s; - strprintf(&s, "Relative Humidity 10^%d %%", n-1); - return s; - } - - if ( (vif & 0x7e) >= 0x1a && (vif & 0x7e) <= 0x20) { - return "Reserved"; - } - - if ((vife & 0x7f) == 0x21) { - return "0.1 feet^3"; - } - - if ((vife & 0x7f) == 0x22) { - return "0.1 american gallon"; - } - - if ((vife & 0x7f) == 0x23) { - return "american gallon"; - } - - if ((vife & 0x7f) == 0x24) { - return "0.001 american gallon/min"; - } - - if ((vife & 0x7f) == 0x25) { - return "american gallon/min"; - } - - if ((vife & 0x7f) == 0x26) { - return "american gallon/h"; - } - - if ((vife & 0x7f) == 0x27) { - return "Reserved"; - } - - if ((vife & 0x7f) == 0x20) { - return "Volume feet"; - } - - if ((vife & 0x7f) == 0x21) { - return "Volume 0.1 feet"; - } - - if ((vife & 0x7e) == 0x28) { - // Come again? A unit of 1MW...do they intend to use m-bus to track the - // output from a nuclear power plant? - int n = vife & 0x01; - std::string s; - strprintf(&s, "10^%d MW", n-1); - return s; - } - - if ((vife & 0x7f) == 0x29 || - (vife & 0x7c) == 0x2c) { - return "Reserved"; - } - - if ((vife & 0x7e) == 0x30) { - int n = vife & 0x01; - std::string s; - strprintf(&s, "10^%d GJ/h", n-1); - return s; - } - - if ((vife & 0x7f) >= 0x32 && (vife & 0x7c) <= 0x57) { - return "Reserved"; - } - - if ((vife & 0x7c) == 0x58) { - int nn = vife & 0x03; - std::string s; - strprintf(&s, "Flow temperature 10^%d Fahrenheit", nn-3); - return s; - } - - if ((vife & 0x7c) == 0x5c) { - int nn = vife & 0x03; - std::string s; - strprintf(&s, "Return temperature 10^%d Fahrenheit", nn-3); - return s; - } - - if ((vife & 0x7c) == 0x60) { - int nn = vife & 0x03; - std::string s; - strprintf(&s, "Temperature difference 10^%d Fahrenheit", nn-3); - return s; - } - - if ((vife & 0x7c) == 0x64) { - int nn = vife & 0x03; - std::string s; - strprintf(&s, "External temperature 10^%d Fahrenheit", nn-3); - return s; - } - - if ((vife & 0x78) == 0x68) { - return "Reserved"; - } - - if ((vife & 0x7c) == 0x70) { - int nn = vife & 0x03; - std::string s; - strprintf(&s, "Cold / Warm Temperature Limit 10^%d Fahrenheit", nn-3); - return s; - } - - if ((vife & 0x7c) == 0x74) { - int nn = vife & 0x03; - std::string s; - strprintf(&s, "Cold / Warm Temperature Limit 10^%d Celsius", nn-3); - return s; - } - - if ((vife & 0x78) == 0x78) { - int nnn = vife & 0x07; - std::string s; - strprintf(&s, "Cumulative count max power 10^%d W", nnn-3); - return s; - } - - return "?"; -} - -std::string vif_7D_SecondExtensionType(uchar dif, uchar vif, uchar vife) -{ - assert(vif == 0xfd); - - if ((vife & 0x7c) == 0x00) { - int nn = vife & 0x03; - std::string s; - strprintf(&s, "Credit of 10^%d of the nominal local legal currency units", nn-3); - return s; - } - - if ((vife & 0x7c) == 0x04) { - int nn = vife & 0x03; - std::string s; - strprintf(&s, "Debit of 10^%d of the nominal local legal currency units", nn-3); - return s; - } - - if ((vife & 0x7f) == 0x08) { - return "Access Number (transmission count)"; - } - - if ((vife & 0x7f) == 0x09) { - return "Medium (as in fixed header)"; - } - - if ((vife & 0x7f) == 0x0a) { - return "Manufacturer (as in fixed header)"; - } - - if ((vife & 0x7f) == 0x0b) { - return "Parameter set identification"; - } - - if ((vife & 0x7f) == 0x0c) { - return "Model/Version"; - } - - if ((vife & 0x7f) == 0x0d) { - return "Hardware version #"; - } - - if ((vife & 0x7f) == 0x0e) { - return "Firmware version #"; - } - - if ((vife & 0x7f) == 0x0f) { - return "Software version #"; - } - - if ((vife & 0x7f) == 0x10) { - return "Customer location"; - } - - if ((vife & 0x7f) == 0x11) { - return "Customer"; - } - - if ((vife & 0x7f) == 0x12) { - return "Access Code User"; - } - - if ((vife & 0x7f) == 0x13) { - return "Access Code Operator"; - } - - if ((vife & 0x7f) == 0x14) { - return "Access Code System Operator"; - } - - if ((vife & 0x7f) == 0x15) { - return "Access Code Developer"; - } - - if ((vife & 0x7f) == 0x16) { - return "Password"; - } - - if ((vife & 0x7f) == 0x17) { - return "Error flags (binary)"; - } - - if ((vife & 0x7f) == 0x18) { - return "Error mask"; - } - - if ((vife & 0x7f) == 0x19) { - return "Reserved"; - } - - if ((vife & 0x7f) == 0x1a) { - return "Digital Output (binary)"; - } - - if ((vife & 0x7f) == 0x1b) { - return "Digital Input (binary)"; - } - - if ((vife & 0x7f) == 0x1c) { - return "Baudrate [Baud]"; - } - - if ((vife & 0x7f) == 0x1d) { - return "Response delay time [bittimes]"; - } - - if ((vife & 0x7f) == 0x1e) { - return "Retry"; - } - - if ((vife & 0x7f) == 0x1f) { - return "Reserved"; - } - - if ((vife & 0x7f) == 0x20) { - return "First storage # for cyclic storage"; - } - - if ((vife & 0x7f) == 0x21) { - return "Last storage # for cyclic storage"; - } - - if ((vife & 0x7f) == 0x22) { - return "Size of storage block"; - } - - if ((vife & 0x7f) == 0x23) { - return "Reserved"; - } - - if ((vife & 0x7c) == 0x24) { - int nn = vife & 0x03; - std::string s; - strprintf(&s, "Storage interval [%s]", timeNN(nn)); - return s; - } - - if ((vife & 0x7f) == 0x28) { - return "Storage interval month(s)"; - } - - if ((vife & 0x7f) == 0x29) { - return "Storage interval year(s)"; - } - - if ((vife & 0x7f) == 0x2a) { - return "Reserved"; - } - - if ((vife & 0x7f) == 0x2b) { - return "Reserved"; - } - - if ((vife & 0x7c) == 0x2c) { - int nn = vife & 0x03; - std::string s; - strprintf(&s, "Duration since last readout [%s]", timeNN(nn)); - return s; - } - - if ((vife & 0x7f) == 0x30) { - return "Start (date/time) of tariff"; - } - - if ((vife & 0x7c) == 0x30) { - int nn = vife & 0x03; - std::string s; - // nn == 0 (seconds) is not expected here! According to m-bus spec. - strprintf(&s, "Duration of tariff [%s]", timeNN(nn)); - return s; - } - - if ((vife & 0x7c) == 0x34) { - int nn = vife & 0x03; - std::string s; - strprintf(&s, "Period of tariff [%s]", timeNN(nn)); - return s; - } - - if ((vife & 0x7f) == 0x38) { - return "Period of tariff months(s)"; - } - - if ((vife & 0x7f) == 0x39) { - return "Period of tariff year(s)"; - } - - if ((vife & 0x7f) == 0x3a) { - return "Dimensionless / no VIF"; - } - - if ((vife & 0x7f) == 0x3b) { - return "Reserved"; - } - - if ((vife & 0x7c) == 0x3c) { - // int xx = vife & 0x03; - return "Reserved"; - } - - if ((vife & 0x70) == 0x40) { - int nnnn = vife & 0x0f; - std::string s; - strprintf(&s, "10^%d Volts", nnnn-9); - return s; - } - - if ((vife & 0x70) == 0x50) { - int nnnn = vife & 0x0f; - std::string s; - strprintf(&s, "10^%d Ampere", nnnn-12); - return s; - } - - if ((vife & 0x7f) == 0x60) { - return "Reset counter"; - } - - if ((vife & 0x7f) == 0x61) { - return "Cumulation counter"; - } - - if ((vife & 0x7f) == 0x62) { - return "Control signal"; - } - - if ((vife & 0x7f) == 0x63) { - return "Day of week"; - } - - if ((vife & 0x7f) == 0x64) { - return "Week number"; - } - - if ((vife & 0x7f) == 0x65) { - return "Time point of day change"; - } - - if ((vife & 0x7f) == 0x66) { - return "State of parameter activation"; - } - - if ((vife & 0x7f) == 0x67) { - return "Special supplier information"; - } - - if ((vife & 0x7c) == 0x68) { - int pp = vife & 0x03; - std::string s; - strprintf(&s, "Duration since last cumulation [%s]", timePP(pp)); - return s; - } - - if ((vife & 0x7c) == 0x6c) { - int pp = vife & 0x03; - std::string s; - strprintf(&s, "Operating time battery [%s]", timePP(pp)); - return s; - } - - if ((vife & 0x7f) == 0x70) { - return "Date and time of battery change"; - } - - if ((vife & 0x7f) >= 0x71) { - return "Reserved"; - } - - if ((vife & 0x7f) == 0x74) { - return "Remaining battery in days"; - } - - return "?"; -} - -std::string vif_6F_ThirdExtensionType(uchar dif, uchar vif, uchar vife) -{ - assert(vif == 0xef); - return "?"; -} - -std::string vif_7F_ManufacturerExtensionType(uchar dif, uchar vif, uchar vife) -{ - assert(vif == 0xff); - return "?"; -} - -std::string vifeType(int dif, int vif, int vife) -{ - if (vif == 0xfb) { // 0x7b without high bit - return vif_7B_FirstExtensionType(dif, vif, vife); - } - if (vif == 0xfd) { // 0x7d without high bit - return vif_7D_SecondExtensionType(dif, vif, vife); - } - if (vif == 0xef) { // 0x6f without high bit - return vif_6F_ThirdExtensionType(dif, vif, vife); - } - if (vif == 0xff) { // 0x7f without high bit - return vif_7F_ManufacturerExtensionType(dif, vif, vife); - } - vife = vife & 0x7f; // Strip the bit signifying more vifes after this. - if (vife == 0x1f) { - return "Compact profile without register"; - } - if (vife == 0x13) { - return "Reverse compact profile without register"; - } - if (vife == 0x1e) { - return "Compact profile with register"; - } - if (vife == 0x20) { - return "per second"; - } - if (vife == 0x21) { - return "per minute"; - } - if (vife == 0x22) { - return "per hour"; - } - if (vife == 0x23) { - return "per day"; - } - if (vife == 0x24) { - return "per week"; - } - if (vife == 0x25) { - return "per month"; - } - if (vife == 0x26) { - return "per year"; - } - if (vife == 0x27) { - return "per revolution/measurement"; - } - if (vife == 0x28) { - return "incr per input pulse on input channel 0"; - } - if (vife == 0x29) { - return "incr per input pulse on input channel 1"; - } - if (vife == 0x2a) { - return "incr per output pulse on input channel 0"; - } - if (vife == 0x2b) { - return "incr per output pulse on input channel 1"; - } - if (vife == 0x2c) { - return "per litre"; - } - if (vife == 0x2d) { - return "per m3"; - } - if (vife == 0x2e) { - return "per kg"; - } - if (vife == 0x2f) { - return "per kelvin"; - } - if (vife == 0x30) { - return "per kWh"; - } - if (vife == 0x31) { - return "per GJ"; - } - if (vife == 0x32) { - return "per kW"; - } - if (vife == 0x33) { - return "per kelvin*litre"; - } - if (vife == 0x34) { - return "per volt"; - } - if (vife == 0x35) { - return "per ampere"; - } - if (vife == 0x36) { - return "multiplied by s"; - } - if (vife == 0x37) { - return "multiplied by s/V"; - } - if (vife == 0x38) { - return "multiplied by s/A"; - } - if (vife == 0x39) { - return "start date/time of a,b"; - } - if (vife == 0x3a) { - return "uncorrected meter unit"; - } - if (vife == 0x3b) { - return "forward flow"; - } - if (vife == 0x3c) { - return "backward flow"; - } - if (vife == 0x3d) { - return "reserved for non-metric unit systems"; - } - if (vife == 0x3e) { - return "value at base conditions c"; - } - if (vife == 0x3f) { - return "obis-declaration"; - } - if (vife == 0x40) { - return "obis-declaration"; - } - if (vife == 0x40) { - return "lower limit"; - } - if (vife == 0x48) { - return "upper limit"; - } - if (vife == 0x41) { - return "number of exceeds of lower limit"; - } - if (vife == 0x49) { - return "number of exceeds of upper limit"; - } - if ((vife & 0x72) == 0x42) { - std::string msg = "date/time of "; - if (vife & 0x01) msg += "end "; - else msg += "beginning "; - msg +=" of "; - if (vife & 0x04) msg += "last "; - else msg += "first "; - if (vife & 0x08) msg += "upper "; - else msg += "lower "; - msg += "limit exceed"; - return msg; - } - if ((vife & 0x70) == 0x50) { - std::string msg = "duration of limit exceed "; - if (vife & 0x04) msg += "last "; - else msg += "first "; - if (vife & 0x08) msg += "upper "; - else msg += "lower "; - int nn = vife & 0x03; - msg += " is "+std::to_string(nn); - return msg; - } - if ((vife & 0x78) == 0x60) { - std::string msg = "duration of a,b "; - if (vife & 0x04) msg += "last "; - else msg += "first "; - int nn = vife & 0x03; - msg += " is "+std::to_string(nn); - return msg; - } - if ((vife & 0x7B) == 0x68) { - std::string msg = "value during "; - if (vife & 0x04) msg += "upper "; - else msg += "lower "; - msg += "limit exceed"; - return msg; - } - if (vife == 0x69) { - return "leakage values"; - } - if (vife == 0x6d) { - return "overflow values"; - } - if ((vife & 0x7a) == 0x6a) { - std::string msg = "date/time of a: "; - if (vife & 0x01) msg += "end "; - else msg += "beginning "; - msg +=" of "; - if (vife & 0x04) msg += "last "; - else msg += "first "; - if (vife & 0x08) msg += "upper "; - else msg += "lower "; - return msg; - } - if ((vife & 0x78) == 0x70) { - int nnn = vife & 0x07; - return "multiplicative correction factor: 10^"+std::to_string(nnn-6); - } - if ((vife & 0x78) == 0x78) { - int nn = vife & 0x03; - return "additive correction constant: unit of VIF * 10^"+std::to_string(nn-3); - } - if (vife == 0x7c) { - return "extension of combinable vife"; - } - if (vife == 0x7d) { - return "multiplicative correction factor for value"; - } - if (vife == 0x7e) { - return "future value"; - } - if (vif == 0x7f) { - return "manufacturer specific"; - } - return "?"; -} - -double toDoubleFromBytes (std::vector &bytes, int len) { - double d = 0; - for (int i=0; i &bytes, int len) { - double d = 0; - for (int i=0; i>4; - d += xx*(10^(1+i*2)); - } - return d; -} - -double dataAsDouble(int dif, int vif, int vife, std::string data) -{ - std::vector bytes; - hex2bin(data, &bytes); - - int t = dif & 0x0f; - switch (t) { - case 0x0: return 0.0; - case 0x1: return toDoubleFromBytes(bytes, 1); - case 0x2: return toDoubleFromBytes(bytes, 2); - case 0x3: return toDoubleFromBytes(bytes, 3); - case 0x4: return toDoubleFromBytes(bytes, 4); - case 0x5: return -1; // How is REAL stored? - case 0x6: return toDoubleFromBytes(bytes, 6); - // Note that for 64 bit data, storing it into a double might lose precision - // since the mantissa is less than 64 bit. It is unlikely that anyone - // really needs true 64 bit precision in their measurements from a physical meter though. - case 0x7: return toDoubleFromBytes(bytes, 8); - case 0x8: return -1; // Selection for Readout? - case 0x9: return toDoubleFromBCD(bytes, 1); - case 0xA: return toDoubleFromBCD(bytes, 2); - case 0xB: return toDoubleFromBCD(bytes, 3); - case 0xC: return toDoubleFromBCD(bytes, 4); - case 0xD: return -1; // variable length - case 0xE: return toDoubleFromBCD(bytes, 6); - case 0xF: return -1; // Special Functions - } - return -1; -} - -uint64_t dataAsUint64(int dif, int vif, int vife, std::string data) -{ - std::vector bytes; - hex2bin(data, &bytes); - - int t = dif & 0x0f; - switch (t) { - case 0x0: return 0.0; - case 0x1: return toDoubleFromBytes(bytes, 1); - case 0x2: return toDoubleFromBytes(bytes, 2); - case 0x3: return toDoubleFromBytes(bytes, 3); - case 0x4: return toDoubleFromBytes(bytes, 4); - case 0x5: return -1; // How is REAL stored? - case 0x6: return toDoubleFromBytes(bytes, 6); - // Note that for 64 bit data, storing it into a double might lose precision - // since the mantissa is less than 64 bit. It is unlikely that anyone - // really needs true 64 bit precision in their measurements from a physical meter though. - case 0x7: return toDoubleFromBytes(bytes, 8); - case 0x8: return -1; // Selection for Readout? - case 0x9: return toDoubleFromBCD(bytes, 1); - case 0xA: return toDoubleFromBCD(bytes, 2); - case 0xB: return toDoubleFromBCD(bytes, 3); - case 0xC: return toDoubleFromBCD(bytes, 4); - case 0xD: return -1; // variable length - case 0xE: return toDoubleFromBCD(bytes, 6); - case 0xF: return -1; // Special Functions - } - return -1; -} - -std::string linkModeName(LinkMode link_mode) -{ - - for (auto& s : link_modes_) { - if (link_mode == s.mode) { - return s.name; - } - } - return "UnknownLinkMode"; -} - -std::string measurementTypeName(MeasurementType mt) -{ - switch (mt) { - case MeasurementType::Any: return "any"; - case MeasurementType::Instantaneous: return "instantaneous"; - case MeasurementType::Maximum: return "maximum"; - case MeasurementType::Minimum: return "minimum"; - case MeasurementType::AtError: return "aterror"; - case MeasurementType::Unknown: return "unknown"; - } - assert(0); -} - -bool Telegram::findFormatBytesFromKnownMeterSignatures (std::vector *format_bytes) -{ - bool ok = true; - if (format_signature == 0xa8ed) - { - hex2bin("02FF2004134413615B6167", format_bytes); - debug("(wmbus) using hard coded format for hash a8ed\n"); - } - else if (format_signature == 0xc412) - { - hex2bin("02FF20041392013BA1015B8101E7FF0F", format_bytes); - debug("(wmbus) using hard coded format for hash c412\n"); - } - else if (format_signature == 0x61eb) - { - hex2bin("02FF2004134413A1015B8101E7FF0F", format_bytes); - debug("(wmbus) using hard coded format for hash 61eb\n"); - } - else if (format_signature == 0xd2f7) - { - hex2bin("02FF2004134413615B5167", format_bytes); - debug("(wmbus) using hard coded format for hash d2f7\n"); - } - else if (format_signature == 0xdd34) - { - hex2bin("02FF2004134413", format_bytes); - debug("(wmbus) using hard coded format for hash dd34\n"); - } - else if (format_signature == 0x7c0e) - { - hex2bin("02FF200413523B", format_bytes); - debug("(wmbus) using hard coded format for hash 7c0e\n"); - } - else - { - ok = false; - } - return ok; -} - - -static bool ignore_duplicate_telegrams_ = false; - -void setIgnoreDuplicateTelegrams(bool idt) -{ - ignore_duplicate_telegrams_ = idt; -} - -static bool detailed_first_ = false; - -void setDetailedFirst(bool df) -{ - detailed_first_ = df; -} - -bool getDetailedFirst() -{ - return detailed_first_; -} - -int toInt(TPLSecurityMode tsm) -{ - switch (tsm) { - -#define X(name,nr) case TPLSecurityMode::name : return nr; -LIST_OF_TPL_SECURITY_MODES -#undef X - } - - return 16; -} - -const char *toString(TPLSecurityMode tsm) -{ - switch (tsm) { - -#define X(name,nr) case TPLSecurityMode::name : return #name; -LIST_OF_TPL_SECURITY_MODES -#undef X - } - - return "Reserved"; -} - -TPLSecurityMode fromIntToTPLSecurityMode(int i) -{ - switch (i) { - -#define X(name,nr) case nr: return TPLSecurityMode::name; -LIST_OF_TPL_SECURITY_MODES -#undef X - } - - return TPLSecurityMode::SPECIFIC_16_31; -} - -int toInt(ELLSecurityMode esm) -{ - switch (esm) { - -#define X(name,nr) case ELLSecurityMode::name : return nr; -LIST_OF_ELL_SECURITY_MODES -#undef X - } - - return 2; -} - -const char *toString(ELLSecurityMode esm) -{ - switch (esm) { - -#define X(name,nr) case ELLSecurityMode::name : return #name; -LIST_OF_ELL_SECURITY_MODES -#undef X - } - - return "?"; -} - -ELLSecurityMode fromIntToELLSecurityMode(int i) -{ - switch (i) { - -#define X(name,nr) case nr: return ELLSecurityMode::name; -LIST_OF_ELL_SECURITY_MODES -#undef X - } - - return ELLSecurityMode::RESERVED; -} - -void Telegram::extractMfctData (std::vector *pl) -{ - pl->clear(); - if (mfct_0f_index == -1) return; - - std::vector::iterator from = frame.begin()+header_size+mfct_0f_index; - std::vector::iterator to = frame.end()-suffix_size; - pl->insert(pl->end(), from, to); -} - -void Telegram::extractPayload (std::vector *pl) -{ - pl->clear(); - std::vector::iterator from = frame.begin()+header_size; - std::vector::iterator to = frame.end()-suffix_size; - pl->insert(pl->end(), from, to); -} - -void Telegram::extractFrame (std::vector *fr) -{ - *fr = frame; -} - -int toInt(AFLAuthenticationType aat) -{ - switch (aat) { - -#define X(name,nr,len) case AFLAuthenticationType::name : return nr; -LIST_OF_AFL_AUTH_TYPES -#undef X - } - - return 16; -} - -int toLen(AFLAuthenticationType aat) -{ - switch (aat) { - -#define X(name,nr,len) case AFLAuthenticationType::name : return len; -LIST_OF_AFL_AUTH_TYPES -#undef X - } - - return 0; -} - -const char *toString(AFLAuthenticationType tsm) -{ - switch (tsm) { - -#define X(name,nr,len) case AFLAuthenticationType::name : return #name; -LIST_OF_AFL_AUTH_TYPES -#undef X - } - - return "Reserved"; -} - -AFLAuthenticationType fromIntToAFLAuthenticationType(int i) -{ - switch (i) { -#define X(name,nr,len) case nr: return AFLAuthenticationType::name; -LIST_OF_AFL_AUTH_TYPES -#undef X - } - - return AFLAuthenticationType::Reserved1; -} - -bool trimCRCsFrameFormatAInternal(std::vector &payload, bool fail_is_ok) -{ - if (payload.size() < 12) { - if (!fail_is_ok) - { - debug("(wmbus) not enough bytes! expected at least 12 but got (%zu)!\n", payload.size()); - } - return false; - } - size_t len = payload.size(); - if (!fail_is_ok) - { - debugPayload("(wmbus) trimming frame A", payload); - } - - std::vector out; - - uint16_t calc_crc = crc16_EN13757(safeButUnsafeVectorPtr(payload), 10); - uint16_t check_crc = payload[10] << 8 | payload[11]; - - if (calc_crc != check_crc && !FUZZING) - { - if (!fail_is_ok) - { - debug("(wmbus) ff a dll crc first (calculated %04x) did not match (expected %04x) for bytes 0-%zu!\n", calc_crc, check_crc, 10); - } - return false; - } - out.insert(out.end(), payload.begin(), payload.begin()+10); - if (!fail_is_ok) - { - debug("(wmbus) ff a dll crc 0-%zu %04x ok\n", 10-1, calc_crc); - } - - size_t pos = 12; - for (pos = 12; pos+18 <= len; pos += 18) - { - size_t to = pos+16; - calc_crc = crc16_EN13757(&payload[pos], 16); - check_crc = payload[to] << 8 | payload[to+1]; - if (calc_crc != check_crc && !FUZZING) - { - if (!fail_is_ok) - { - debug("(wmbus) ff a dll crc mid (calculated %04x) did not match (expected %04x) for bytes %zu-%zu!\n", - calc_crc, check_crc, pos, to-1); - } - return false; - } - out.insert(out.end(), payload.begin()+pos, payload.begin()+pos+16); - if (!fail_is_ok) - { - debug("(wmbus) ff a dll crc mid %zu-%zu %04x ok\n", pos, to-1, calc_crc); - } - } - - if (pos < len-2) - { - size_t tto = len-2; - size_t blen = (tto-pos); - calc_crc = crc16_EN13757(&payload[pos], blen); - check_crc = payload[tto] << 8 | payload[tto+1]; - if (calc_crc != check_crc && !FUZZING) - { - if (!fail_is_ok) - { - debug("(wmbus) ff a dll crc final (calculated %04x) did not match (expected %04x) for bytes %zu-%zu!\n", - calc_crc, check_crc, pos, tto-1); - } - return false; - } - out.insert(out.end(), payload.begin()+pos, payload.begin()+tto); - if (!fail_is_ok) - { - debug("(wmbus) ff a dll crc final %zu-%zu %04x ok\n", pos, tto-1, calc_crc); - } - } - - debugPayload("(wmbus) trimming frame A", payload); - - out[0] = out.size()-1; - size_t new_len = out[0]+1; - size_t old_size = payload.size(); - payload = out; - size_t new_size = payload.size(); - - debug("(wmbus) trimmed %zu dll crc bytes from frame a and ignored %zu suffix bytes.\n", (len-new_len), (old_size-new_size)-(len-new_len)); - debugPayload("(wmbus) trimmed frame A", payload); - - return true; -} - -bool trimCRCsFrameFormatBInternal(std::vector &payload, bool fail_is_ok) -{ - if (payload.size() < 12) { - if (!fail_is_ok) - { - debug("(wmbus) not enough bytes! expected at least 12 but got (%zu)!\n", payload.size()); - } - return false; - } - size_t len = payload.size(); - if (!fail_is_ok) - { - debugPayload("(wmbus) trimming frame B", payload); - } - - std::vector out; - size_t crc1_pos, crc2_pos; - if (len <= 128) - { - crc1_pos = len-2; - crc2_pos = 0; - } - else - { - crc1_pos = 126; - crc2_pos = len-2; - } - - uchar *from1 = &payload[0]; - size_t len1 = crc1_pos; - uint16_t calc_crc = crc16_EN13757(from1, len1); - uint16_t check_crc = payload[crc1_pos] << 8 | payload[crc1_pos+1]; - - if (calc_crc != check_crc && !FUZZING) - { - if (!fail_is_ok) - { - debug("(wmbus) ff b dll crc (calculated %04x) did not match (expected %04x) for bytes 0-%zu!\n", calc_crc, check_crc, crc1_pos); - } - return false; - } - - out.insert(out.end(), payload.begin(), payload.begin()+crc1_pos); - if (!fail_is_ok) - { - debug("(wmbus) ff b dll crc first 0-%zu %04x ok\n", crc1_pos, calc_crc); - } - - if (crc2_pos > 0) - { - uchar *from2 = &payload[crc1_pos+2]; - size_t len2 = crc2_pos-crc1_pos-2; - calc_crc = crc16_EN13757(from2, len2); - check_crc = payload[crc2_pos] << 8 | payload[crc2_pos+1]; - - if (calc_crc != check_crc && !FUZZING) - { - if (!fail_is_ok) - { - debug("(wmbus) ff b dll crc (calculated %04x) did not match (expected %04x) for bytes %zu-%zu!\n", - calc_crc, check_crc, crc1_pos+2, crc2_pos); - } - return false; - } - - out.insert(out.end(), payload.begin()+crc1_pos+2, payload.begin()+crc2_pos); - if (!fail_is_ok) - { - debug("(wmbus) ff b dll crc final %zu-%zu %04x ok\n", crc1_pos+2, crc2_pos, calc_crc); - } - } - - debugPayload("(wmbus) trimming frame B", payload); - - out[0] = out.size()-1; - size_t new_len = out[0]+1; - size_t old_size = payload.size(); - payload = out; - size_t new_size = payload.size(); - - debug("(wmbus) trimmed %zu dll crc bytes from frame b and ignored %zu suffix bytes.\n", (len-new_len), (old_size-new_size)-(len-new_len)); - debugPayload("(wmbus) trimmed frame B", payload); - - return true; -} - -void removeAnyDLLCRCs(std::vector &payload) -{ - bool trimmed = trimCRCsFrameFormatAInternal(payload, true); - if (!trimmed) trimCRCsFrameFormatBInternal(payload, true); -} - -bool trimCRCsFrameFormatA(std::vector &payload) -{ - return trimCRCsFrameFormatAInternal(payload, false); -} - -bool trimCRCsFrameFormatB(std::vector &payload) -{ - return trimCRCsFrameFormatBInternal(payload, false); -} - -FrameStatus checkWMBusFrame (std::vector &data, - size_t *frame_length, - int *payload_len_out, - int *payload_offset, - bool only_test) -{ - // Nice clean: 2A442D2C998734761B168D2021D0871921|58387802FF2071000413F81800004413F8180000615B - // Ugly: 00615B2A442D2C998734761B168D2021D0871921|58387802FF2071000413F81800004413F8180000615B - // Here the frame is prefixed with some random data. - - debugPayload("(wmbus) checkWMBUSFrame", data); - - if (data.size() < 11) - { - debug("(wmbus) less than 11 bytes, partial frame\n"); - return PartialFrame; - } - int payload_len = data[0]; - int type = data[1]; - int offset = 1; - - if (data[0] == 0x68 && data[3] == 0x68 && data[1] == data[2]) - { - // Ooups this is not a wmbus frame. Its clearly an mbus frame. - return PartialFrame; - } - if (!isValidWMBusCField(type)) - { - // Ouch, we are out of sync with the wmbus frames that we expect! - // Since we currently do not handle any other type of frame, we can - // look for a valid c field (ie 0x44 0x46 etc) in the buffer. - // If we find such a byte and the length byte before maps to the end - // of the buffer, then we have found a valid telegram. - bool found = false; - for (size_t i = 0; i < data.size()-2; ++i) - { - if (isValidWMBusCField(data[i+1])) - { - payload_len = data[i]; - size_t remaining = data.size()-i; - if (data[i]+1 == (uchar)remaining && data[i+1] == 0x44) - { - found = true; - offset = i+1; - verbose("(wmbus) out of sync, skipping %d bytes.\n", (int)i); - break; - } - } - } - if (!found) - { - // No sensible telegram in the buffer. Flush it! - if (!only_test) - { - verbose("(wmbus) no sensible telegram found, clearing buffer.\n"); - data.clear(); - } - else - { - debug("(wmbus) not a proper wmbus frame.\n"); - } - return ErrorInFrame; - } - } - *payload_len_out = payload_len; - *payload_offset = offset; - *frame_length = payload_len+offset; - if (data.size() < *frame_length) - { - // Not enough bytes for this payload_len.... - if (only_test) - { - // This is used from simulate files and hex in command line and analyze. - // Lets be lax and just adjust the length to what is available. - payload_len = data.size() - offset; - *payload_len_out = payload_len; - *frame_length = payload_len+offset; - warning("(wmbus) not enough bytes, frame length byte changed from %d(%02x) to %d(%02x)!\n", - data[offset-1], data[offset-1], - payload_len, payload_len); - data[offset-1] = payload_len; - - return FullFrame; - } - debug("(wmbus) not enough bytes, partial frame %d %d\n", data.size(), *frame_length); - return PartialFrame; - } - - if (!only_test) - { - debug("(wmbus) received full frame.\n"); - } - return FullFrame; -} - -FrameStatus checkMBusFrame (std::vector &data, - size_t *frame_length, - int *payload_len_out, - int *payload_offset, - bool only_test) -{ - // Example: - // E5 - // 68383868 start length 0x38 = 56 - // 56 bytes of payload data: - // 080072840200102941011B010000000265AE084265C208B20165000002FB1A450142FB1A4C01B201FB1A00000C788402001002FD0F21000F - // 5E checksum - // 16 stop - - debugPayload("(mbus) checkMBUSFrame", data); - - if (data.size() > 0 && data[0] == 0xe5) - { - // Single character confirmation frame. - if (only_test) - { - // For testing purposes we require the frame to be a single char as well. - // This happens when we pass a frame on the command line or in a simulation file. - // Otherwise a normal wmbus telegram with length e5 will triggere this mbus command. - // (When reading from a serial line, we might have data coming after the e5, - // but then we know that we are talking mbus.) - if (data.size() != 1) - { - return ErrorInFrame; - } - } - *payload_len_out = 1; - *payload_offset = 0; - *frame_length = 1; - if (!only_test) - { - debug("(mbus) received E5 single byte frame.\n"); - } - return FullFrame; - } - if (data.size() < 6) - { - // 4 byte start, 1 checksum, 1 stop - if (!only_test) - { - debug("(mbus) less than 6 bytes, partial frame\n"); - } - return PartialFrame; - } - if (data[0] != 0x68 && data[3] != 0x68) - { - if (!only_test) - { - verbose("(mbus) no 0x68 byte found, clearing buffer.\n"); - data.clear(); - } - return ErrorInFrame; - } - - if (data[1] != data[2]) - { - if (!only_test) - { - verbose("(mbus) lengths not matching, clearing buffer.\n"); - data.clear(); - } - return ErrorInFrame; - } - int payload_len = data[1]; - *frame_length = payload_len+4+1+1; // start(4)+cs(1)+stop(1) - if (data.size() < *frame_length) - { - if (!only_test) - { - debug("(mbus) not enough bytes, partial frame %d %d\n", data.size(), *frame_length); - } - return PartialFrame; - } - uchar stop = data[*frame_length-1]; - if (stop != 0x16) - { - warning("(mbus) stop byte (0x%02x) at pos %d is not 0x16, clearing buffer.\n", stop, *frame_length-1); - data.clear(); - return ErrorInFrame; - } - uchar csc = 0; - for (size_t i=4; i<(*frame_length-2); ++i) csc += data[i]; - uchar cs = data[*frame_length-2]; - if (cs != csc) - { - warning("(mbus) expected checksum 0x%02x but got 0x%02x, clearing buffer.\n", csc, cs); - data.clear(); - return ErrorInFrame; - } - - *payload_len_out = *frame_length; - *payload_offset = 0; - if (!only_test) - { - debug("(mbus) received full frame.\n"); - } - return FullFrame; -} - -std::string decodeTPLStatusByteOnlyStandardBits(uchar sts) -{ - // Bits 0-4 are standard defined. Bits 5-7 are mfct specific. - std::string s; - - if (sts == 0) return "OK"; - if ((sts & 0x03) == 0x01) s += "BUSY "; // Meter busy, cannot respond. - if ((sts & 0x03) == 0x02) s += "ERROR "; // E.g. meter failed to understand a message sent to it. - // More information about the error can be sent using error reporting, EN13757-3:2018 §10 - if ((sts & 0x03) == 0x03) s += "ALARM "; // E.g. an abnormal condition like water is continuously running. - - if ((sts & 0x04) == 0x04) s += "POWER_LOW "; // E.g. battery end of life or external power supply failure - if ((sts & 0x08) == 0x08) s += "PERMANENT_ERROR "; // E.g. meter needs service to work again. - if ((sts & 0x10) == 0x10) s += "TEMPORARY_ERROR "; - - while (s.size() > 0 && s.back() == ' ') s.pop_back(); - return s; -} - -std::string decodeTPLStatusByteNoMfct(uchar sts) -{ - std::string t = "OK"; - - if ((sts & 0xe0) != 0) - { - t = tostrprintf("UNKNOWN_%02X", sts & 0xe0); - } - - return t; -} - -std::string decodeTPLStatusByteWithMfct(uchar sts, Translate::Lookup &lookup) -{ - std::string s = decodeTPLStatusByteOnlyStandardBits(sts); - std::string t = "OK"; - - if ((sts & 0xe0) != 0) - { - // Vendor specific bits are set, lets translate them. - if (lookup.hasLookups()) - { - t = lookup.translate(sts & 0xe0); - } - else - { - t = decodeTPLStatusByteNoMfct(sts & 0xe0); - } - } - - if (t == "OK" || t == "") return s; - if (s == "OK" || s == "") return t; - - return s+" "+t; -} - -bool is_command(std::string b, std::string *cmd) -{ - // Check if CMD(.) - if (b.length() < 6) return false; - if (b.rfind("CMD(", 0) != 0) return false; - if (b.back() != ')') return false; - *cmd = b.substr(4, b.length()-5); - return true; -} - -const char *toString(TelegramFormat format) -{ - if (format == TelegramFormat::WMBUS_C_FIELD) return "wmbus_c_field"; - if (format == TelegramFormat::WMBUS_CI_FIELD) return "wmbus_ci_field"; - if (format == TelegramFormat::MBUS_SHORT_FRAME) return "mbus_short_frame"; - if (format == TelegramFormat::MBUS_LONG_FRAME) return "mbus_long_frame"; - - return "unknown"; -} - -TelegramFormat toTelegramFormat(const char *s) -{ - if (!strcmp(s, "wmbus_c_field")) return TelegramFormat::WMBUS_C_FIELD; - if (!strcmp(s, "wmbus_ci_field")) return TelegramFormat::WMBUS_CI_FIELD; - if (!strcmp(s, "mbus_short_frame")) return TelegramFormat::MBUS_SHORT_FRAME; - if (!strcmp(s, "mbus_long_frame")) return TelegramFormat::MBUS_LONG_FRAME; - - return TelegramFormat::UNKNOWN; -} - -const char *toString(DeviceMode mode) -{ - if (mode == DeviceMode::METER) return "meter"; - if (mode == DeviceMode::OTHER) return "other"; - return "unknown"; -} - -DeviceMode toDeviceMode(const char *s) -{ - if (!strcmp(s, "meter")) return DeviceMode::METER; - if (!strcmp(s, "other")) return DeviceMode::OTHER; - - return DeviceMode::UNKNOWN; -} - -bool SendBusContent::isLikely(const std::string &s) -{ - return s.rfind("send:", 0) == 0; -} - -bool SendBusContent::parse(const std::string &s) -{ - bus = ""; - content = ""; - // Examples: - // - // send:LINK_MODE:FORMAT:BUS:DATA - // - // send:t2:c_field:OUTBUS:01020304050607 - // send:t1:ci_field:OUTBUS:01020304050607 - // send:mbus:short:OUTMBUS:001122 - // send:mbus:long:OUTMBUS:001122 - // c1 c2 c3 c4 - - size_t c1 = s.find(":"); - if (c1 == std::string::npos) return false; - size_t c2 = s.find(":", c1+1); - if (c2 == std::string::npos) return false; - size_t c3 = s.find(":", c2+1); - if (c3 == std::string::npos) return false; - size_t c4 = s.find(":", c3+1); - if (c4 == std::string::npos) return false; - - std::string cmd = s.substr(0, c1); - if (cmd != "send") return false; - - link_mode = toLinkMode(s.substr(c1+1, c2-c1-1).c_str()); - if (link_mode == LinkMode::UNKNOWN) return false; - - format = toTelegramFormat(s.substr(c2+1, c3-c2-1).c_str()); - if (format == TelegramFormat::UNKNOWN) return false; - - bus = s.substr(c3+1, c4-c3-1); - if (bus.size() == 0) return false; - - content = s.substr(c4+1); - if (content.size() == 0) return false; - if (content.size() % 2 == 1) return false; - - return true; -} - -const char *toString(FrameType ft) -{ - switch (ft) { - case FrameType::WMBUS: return "wmbus"; - case FrameType::MBUS: return "mbus"; - case FrameType::HAN: return "han"; - } - return "?"; -} - -const char *toString(FrameStatus fs) -{ - switch (fs) { - case FrameStatus::PartialFrame: return "PartialFrame"; - case FrameStatus::FullFrame: return "FullFrame"; - case FrameStatus::ErrorInFrame: return "ErrorInFrame"; - case FrameStatus::TextAndNotFrame: return "TextAndNotFrame"; - } - return "?"; -} - -int genericifyMedia(int media) -{ - if (media == 0x06 || // Warm Water (30°C-90°C) meter - media == 0x07 || // Water meter - media == 0x15 || // Hot water (>=90°C) meter - media == 0x16 || // Cold water meter - media == 0x28) // Waste water - { - return 0x07; // Return plain water - } - return media; -} - -bool isCloseEnough(int media1, int media2) -{ - return genericifyMedia(media1) == genericifyMedia(media2); -} diff --git a/components/wmbus_common/wmbus.h b/components/wmbus_common/wmbus.h deleted file mode 100644 index bc8eeddc..00000000 --- a/components/wmbus_common/wmbus.h +++ /dev/null @@ -1,638 +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 WMBUS_H -#define WMBUS_H - -#include"address.h" -#include"dvparser.h" -#include"manufacturers.h" -#include"translatebits.h" -#include"util.h" - -#include -#include -#include - -// Check and remove the data link layer CRCs from a wmbus telegram. -// If the CRCs do not pass the test, return false. -void removeAnyDLLCRCs(std::vector &payload); -bool trimCRCsFrameFormatA(std::vector &payload); -bool trimCRCsFrameFormatB(std::vector &payload); - -#define LIST_OF_MBUS_DEVICES \ - X(UNKNOWN,unknown,false,false,detectUNKNOWN) \ - X(MBUS,mbus,true,false,detectMBUS) \ - X(AUTO,auto,false,false,detectAUTO) \ - X(AMB8465,amb8465,true,false,detectAMB8465AMB3665)\ - X(AMB3665,amb3665,true,false,detectSKIP) \ - X(CUL,cul,true,false,detectCUL) \ - X(IM871A,im871a,true,false,detectIM871AIM170A) \ - X(IM170A,im170a,true,false,detectSKIP) \ - X(IU891A,iu891a,true,false,detectIU891A) \ - X(RAWTTY,rawtty,true,false,detectRAWTTY) \ - X(HEXTTY,hextty,true,false,detectSKIP) \ - X(XMQTTY,xmqtty,true,false,detectSKIP) \ - X(RC1180,rc1180,true,false,detectRC1180) \ - X(RTL433,rtl433,false,true,detectRTL433) \ - X(RTLWMBUS,rtlwmbus,false,true,detectRTLWMBUS) \ - X(IU880B,iu880b,true,false,detectSKIP) \ - X(SIMULATION,simulation,false,false,detectSIMULATION) - -enum BusDeviceType { -#define X(name,text,tty,rtlsdr,detector) DEVICE_ ## name, -LIST_OF_MBUS_DEVICES -#undef X -}; - -enum class TelegramFormat -{ - UNKNOWN, - WMBUS_C_FIELD, // The payload begins with the c-field - WMBUS_CI_FIELD, // The payload begins with the ci-field (ie the c-field + dll is auto-prefixed.) - MBUS_SHORT_FRAME, // Short mbus frame (ie ack etc) - MBUS_LONG_FRAME // Long mbus frame (ie data frame) -}; -const char *toString(TelegramFormat format); -TelegramFormat toTelegramFormat(const char *s); - -enum class DeviceMode -{ - UNKNOWN, - OTHER, - METER -}; -const char *toString(DeviceMode mode); -DeviceMode toDeviceMode(const char *s); - -void setIgnoreDuplicateTelegrams(bool idt); -void setDetailedFirst(bool df); -bool getDetailedFirst(); - -// In link mode S1, is used when both the transmitter and receiver are stationary. -// It can be transmitted relatively seldom. - -// In link mode T1, the meter transmits a telegram every few seconds or minutes. -// Suitable for drive-by/walk-by collection of meter values. - -// Link mode C1 is like T1 but uses less energy when transmitting due to -// a different radio encoding. Also significant is: -// S1/T1 usually uses the A format for the data link layer, more CRCs. -// C1 usually uses the B format for the data link layer, less CRCs = less overhead. - -// The im871a can for example receive C1a, but it is unclear if there are any meters that use it. - -#define LIST_OF_LINK_MODES \ - X(Any,any,--anylinkmode,(~0UL)) \ - X(MBUS,mbus,--mbus,(1UL<<1)) \ - X(S1,s1,--s1, (1UL<<2)) \ - X(S1m,s1m,--s1m, (1UL<<3)) \ - X(S2,s2,--s2, (1UL<<4)) \ - X(T1,t1,--t1, (1UL<<5)) \ - X(T2,t2,--t2, (1UL<<6)) \ - X(C1,c1,--c1, (1UL<<7)) \ - X(C2,c2,--c2, (1UL<<8)) \ - X(N1a,n1a,--n1a, (1UL<<9)) \ - X(N2a,n2a,--n2a, (1UL<<10)) \ - X(N1b,n1b,--n1b, (1UL<<11)) \ - X(N2b,n2b,--n2b, (1UL<<12)) \ - X(N1c,n1c,--n1c, (1UL<<13)) \ - X(N2c,n2c,--n2c, (1UL<<14)) \ - X(N1d,n1d,--n1d, (1UL<<15)) \ - X(N2d,n2d,--n2d, (1UL<<16)) \ - X(N1e,n1e,--n1e, (1UL<<17)) \ - X(N2e,n2e,--n2e, (1UL<<18)) \ - X(N1f,n1f,--n1f, (1UL<<19)) \ - X(N2f,n2f,--n2f, (1UL<<20)) \ - X(R2a,r2a,--r2a, (1UL<<21)) \ - X(R2b,r2b,--r2b, (1UL<<22)) \ - X(R2c,r2c,--r2c, (1UL<<23)) \ - X(R2d,r2d,--r2d, (1UL<<24)) \ - X(R2e,r2e,--r2e, (1UL<<25)) \ - X(R2f,r2f,--r2f, (1UL<<26)) \ - X(R2g,r2g,--r2g, (1UL<<27)) \ - X(R2h,r2h,--r2h, (1UL<<28)) \ - X(R2i,r2i,--r2i, (1UL<<29)) \ - X(R2j,r2j,--r2j, (1UL<<30)) \ - X(LORA,lora,--lora, (1UL<<31)) \ - X(UNKNOWN,unknown,----,0x0UL) - -enum class LinkMode { -#define X(name,lcname,option,val) name, -LIST_OF_LINK_MODES -#undef X -}; - -#define X(name,lcname,option,val) const uint64_t name##_bit = val; -LIST_OF_LINK_MODES -#undef X - -LinkMode toLinkMode(const char *arg); -LinkMode isLinkModeOption(const char *arg); -const char *toString(LinkMode lm); - -struct LinkModeSet -{ - // Add the link mode to the set of link modes. - LinkModeSet &addLinkMode(LinkMode lm); - void unionLinkModeSet(LinkModeSet lms); - void disjunctionLinkModeSet(LinkModeSet lms); - // Does this set support listening to the given link mode set? - // If this set is C1 and T1 and the supplied set contains just C1, - // then supports returns true. - // Or if this set is just T1 and the supplied set contains just C1, - // then supports returns false. - // Or if this set is just C1 and the supplied set contains C1 and T1, - // then supports returns true. - // Or if this set is S1 and T1, and the supplied set contains C1 and T1, - // then supports returns true. - // - // It will do a bitwise and of the linkmode bits. If the result - // of the and is not zero, then support returns true. - bool supports(LinkModeSet lms); - // Check if this set contains the given link mode. - bool has(LinkMode lm); - // Check if all link modes are supported. - bool hasAll(LinkModeSet lms); - // Check if any link mode has been set. - bool empty() { return set_ == 0; } - // Clear the set to empty. - void clear() { set_ = 0; } - // Mark set as all linkmodes! - void setAll() { set_ = (int)LinkMode::Any; } - // For bit counting etc. - int asBits() { return set_; } - - // Return a human readable std::string. - std::string hr(); - - LinkModeSet() { } - LinkModeSet(uint64_t s) : set_(s) {} - -private: - - uint64_t set_ {}; -}; - -LinkModeSet parseLinkModes(std::string modes); -bool isValidLinkModes(std::string modes); - -enum class CI_TYPE -{ - ELL, NWL, AFL, TPL -}; - -enum class TPL_LENGTH -{ - NONE, SHORT, LONG -}; - -#define CC_B_BIDIRECTIONAL_BIT 0x80 -#define CC_RD_RESPONSE_DELAY_BIT 0x40 -#define CC_S_SYNCH_FRAME_BIT 0x20 -#define CC_R_RELAYED_BIT 0x10 -#define CC_P_HIGH_PRIO_BIT 0x08 - -// Bits 31-29 in SN, ie 0xc0 of the final byte in the stream, -// since the bytes arrive with the least significant first -// aka little endian. -#define SN_ENC_BITS 0xc0 - -#define LIST_OF_ELL_SECURITY_MODES \ - X(NoSecurity, 0) \ - X(AES_CTR, 1) \ - X(RESERVED, 2) - -enum class ELLSecurityMode { -#define X(name,nr) name, -LIST_OF_ELL_SECURITY_MODES -#undef X -}; - -int toInt(ELLSecurityMode esm); -const char *toString(ELLSecurityMode esm); -ELLSecurityMode fromIntToELLSecurityMode(int i); - -#define LIST_OF_TPL_SECURITY_MODES \ - X(NoSecurity, 0) \ - X(MFCT_SPECIFIC, 1) \ - X(DES_NO_IV_DEPRECATED, 2) \ - X(DES_IV_DEPRECATED, 3) \ - X(SPECIFIC_4, 4) \ - X(AES_CBC_IV, 5) \ - X(RESERVED_6, 6) \ - X(AES_CBC_NO_IV, 7) \ - X(AES_CTR_CMAC, 8) \ - X(AES_CGM, 9) \ - X(AES_CCM, 10) \ - X(RESERVED_11, 11) \ - X(RESERVED_12, 12) \ - X(SPECIFIC_13, 13) \ - X(RESERVED_14, 14) \ - X(SPECIFIC_15, 15) \ - X(SPECIFIC_16_31, 16) - -enum class TPLSecurityMode { -#define X(name,nr) name, -LIST_OF_TPL_SECURITY_MODES -#undef X -}; - -int toInt(TPLSecurityMode tsm); -TPLSecurityMode fromIntToTPLSecurityMode(int i); -const char *toString(TPLSecurityMode tsm); - -#define LIST_OF_AFL_AUTH_TYPES \ - X(NoAuth, 0, 0) \ - X(Reserved1, 1, 0) \ - X(Reserved2, 2, 0) \ - X(AES_CMAC_128_2, 3, 2) \ - X(AES_CMAC_128_4, 4, 4) \ - X(AES_CMAC_128_8, 5, 8) \ - X(AES_CMAC_128_12, 6, 12) \ - X(AES_CMAC_128_16, 7, 16) \ - X(AES_GMAC_128_12, 8, 12) - -enum class AFLAuthenticationType { -#define X(name,nr,len) name, -LIST_OF_AFL_AUTH_TYPES -#undef X -}; - -int toInt(AFLAuthenticationType aat); -AFLAuthenticationType fromIntToAFLAuthenticationType(int i); -const char *toString(AFLAuthenticationType aat); -int toLen(AFLAuthenticationType aat); - -struct MeterKeys -{ - std::vector confidentiality_key; - std::vector authentication_key; - - bool hasConfidentialityKey() { return confidentiality_key.size() > 0; } - bool hasAuthenticationKey() { return authentication_key.size() > 0; } -}; - -enum class FrameType -{ - WMBUS, - MBUS, - HAN -}; - -const char *toString(FrameType ft); - -struct AboutTelegram -{ - // wmbus device used to receive this telegram. - std::string device; - // The device's opinion of the rssi, best effort conversion into the dbm scale. - // -100 dbm = 0.1 pico Watt to -20 dbm = 10 micro W - // Measurements smaller than -100 and larger than -10 are unlikely. - int rssi_dbm {}; - // If the LinkMode is known, then it is stored here. - LinkMode link_mode {}; - // WMBus or MBus - FrameType type {}; - // time the telegram was received - time_t timestamp; - - AboutTelegram(std::string dv, int rs, LinkMode lm, FrameType t, time_t ts = 0) : device(dv), rssi_dbm(rs), link_mode(lm), type(t), timestamp(ts) {} - AboutTelegram() {} -}; - -// Mark understood bytes as either PROTOCOL, ie dif vif, acc and other header bytes. -// Or CONTENT, ie the value fields found inside the transport layer. -enum class KindOfData -{ - PROTOCOL, CONTENT -}; - -// Content can be not understood at all NONE, partially understood PARTIAL when typically bitsets have -// been partially decoded, or FULL when the volume or energy field is by itself complete. -// Encrypted if it yet decrypted. Compressed and no format signature is known. -enum class Understanding -{ - NONE, ENCRYPTED, COMPRESSED, PARTIAL, FULL -}; - -struct Explanation -{ - int pos {}; - int len {}; - std::string info; - KindOfData kind {}; - Understanding understanding {}; - - Explanation(int p, int l, const std::string &i, KindOfData k, Understanding u) : - pos(p), len(l), info(i), kind(k), understanding(u) {} -}; - -struct Meter; - -struct Telegram -{ -private: - Telegram(Telegram&t) { } - -public: - Telegram() = default; - - AboutTelegram about; - - Meter *meter {}; - - // If set to true then this telegram should be trigger updates. - bool discard {}; - - // If a warning is printed mark this. - bool triggered_warning {}; - - // The different addresses found, - // the first is the dll_id_mvt, ell_id_mvt, nwl_id_mvt, and the last is the tpl_id_mvt. - std::vector
addresses; - - // If decryption failed, set this to true, to prevent further processing. - bool decryption_failed {}; - - // DLL - int dll_len {}; // The length of the telegram, 1 byte. - int dll_c {}; // 1 byte control code, SND_NR=0x44 - - uchar dll_mfct_b[2]; // 2 bytes - int dll_mfct {}; - - uchar mbus_primary_address; // Single byte address 0-250 for mbus devices. - uchar mbus_ci; // MBus control information field. - - std::vector dll_a; // A field 6 bytes - // The 6 a field bytes are composed of 4 id bytes, version and type. - uchar dll_id_b[4] {}; // 4 bytes, address in BCD = 8 decimal 00000000...99999999 digits. - std::vector dll_id; // 4 bytes, human readable order. - uchar dll_version {}; // 1 byte - uchar dll_type {}; // 1 byte - - // ELL - uchar ell_ci {}; // 1 byte - uchar ell_cc {}; // 1 byte - uchar ell_acc {}; // 1 byte - uchar ell_sn_b[4] {}; // 4 bytes - int ell_sn {}; // 4 bytes - uchar ell_sn_session {}; // 4 bits - int ell_sn_time {}; // 25 bits - uchar ell_sn_sec {}; // 3 bits - ELLSecurityMode ell_sec_mode {}; // Based on 3 bits from above. - uchar ell_pl_crc_b[2] {}; // 2 bytes - uint16_t ell_pl_crc {}; // 2 bytes - - uchar ell_mfct_b[2] {}; // 2 bytes; - int ell_mfct {}; - bool ell_id_found {}; - uchar ell_id_b[6] {}; // 4 bytes; - uchar ell_version {}; // 1 byte - uchar ell_type {}; // 1 byte - - // NWL - int nwl_ci {}; // 1 byte - - // AFL - uchar afl_ci {}; // 1 byte - uchar afl_len {}; // 1 byte - uchar afl_fc_b[2] {}; // 2 byte fragmentation control - uint16_t afl_fc {}; - uchar afl_mcl {}; // 1 byte message control - - bool afl_ki_found {}; - uchar afl_ki_b[2] {}; // 2 byte key information - uint16_t afl_ki {}; - - bool afl_counter_found {}; - uchar afl_counter_b[4] {}; // 4 bytes - uint32_t afl_counter {}; - - bool afl_mlen_found {}; - int afl_mlen {}; - - bool must_check_mac {}; - std::vector afl_mac_b; - - // TPL - std::vector::iterator tpl_start; - int tpl_ci {}; // 1 byte - int tpl_acc {}; // 1 byte - int tpl_sts {}; // 1 byte - int tpl_sts_offset {}; // Remember where the sts field is in the telegram, so - // that we can add more vendor specific decodings to it. - int tpl_cfg {}; // 2 bytes - TPLSecurityMode tpl_sec_mode {}; // Based on 5 bits extracted from cfg. - int tpl_num_encr_blocks {}; - int tpl_cfg_ext {}; // 1 byte - int tpl_kdf_selection {}; // 1 byte - std::vector tpl_generated_key; // 16 bytes - std::vector tpl_generated_mac_key; // 16 bytes - - bool tpl_id_found {}; // If set to true, then tpl_id_b contains valid values. - std::vector tpl_a; // A field 6 bytes - // The 6 a field bytes are composed of 4 id bytes, version and type. - uchar tpl_id_b[4] {}; // 4 bytes - uchar tpl_mfct_b[2] {}; // 2 bytes - int tpl_mfct {}; - uchar tpl_version {}; // 1 bytes - uchar tpl_type {}; // 1 bytes - - // The format signature is used for compact frames. - int format_signature {}; - - std::vector frame; // Content of frame, potentially decrypted. - std::vector parsed; // Parsed bytes with explanations. - int header_size {}; // Size of headers before the APL content. - int suffix_size {}; // Size of suffix after the APL content. Usually empty, but can be MACs! - int mfct_0f_index = -1; // -1 if not found, else index of the 0f byte, if found, inside the difvif data after the header. - int mfct_1f_index = -1; // -1 if not found, else index of the 1f byte, if found, then there are more records in the next telegram. - int force_mfct_index = -1; // Force all data after this offset to be mfct specific. Used for meters not using 0f. - void extractFrame (std::vector *fr); // Extract to full frame. - void extractPayload (std::vector *pl); // Extract frame data containing the measurements, after the header and not the suffix. - void extractMfctData (std::vector *pl); // Extract frame data after the DIF 0x0F. - - bool handled {}; // Set to true, when a meter has accepted the telegram. - - bool parseHeader (std::vector &input_frame); - bool parse (std::vector &input_frame, MeterKeys *mk, bool warn); - - bool parseMBUSHeader (std::vector &input_frame); - bool parseMBUS (std::vector &input_frame, MeterKeys *mk, bool warn); - - bool parseWMBUSHeader (std::vector &input_frame); - bool parseWMBUS (std::vector &input_frame, MeterKeys *mk, bool warn); - - bool parseHANHeader (std::vector &input_frame); - bool parseHAN (std::vector &input_frame, MeterKeys *mk, bool warn); - - void addAddressMfctFirst(const std::vector::iterator &pos); - void addAddressIdFirst(const std::vector::iterator &pos); - - void print(); - - // A std::vector of indentations and explanations, to be printed - // below the raw data bytes to explain the telegram content. - std::vector explanations; - void addExplanationAndIncrementPos (std::vector::iterator &pos, int len, KindOfData k, Understanding u, const char* fmt, ...); - void setExplanation (std::vector::iterator &pos, int len, KindOfData k, Understanding u, const char* fmt, ...); - void addMoreExplanation(int pos, const char* fmt, ...); - void addMoreExplanation(int pos, std::string json); - - // Add an explanation of data inside manufacturer specific data. - void addSpecialExplanation(int offset, int len, KindOfData k, Understanding u, const char* fmt, ...); - void explainParse(std::string intro, int from); - std::string analyzeParse(OutputFormat o, int *content_length, int *understood_content_length); - - bool parserWarns() { return parser_warns_; } - bool isSimulated() { return is_simulated_; } - bool beingAnalyzed() { return being_analyzed_; } - void markAsSimulated() { is_simulated_ = true; } - void markAsBeingAnalyzed() { being_analyzed_ = true; } - - // The actual content of the (w)mbus telegram. The DifVif entries. - // Mapped from their key for quick access to their offset and content. - std::map> dv_entries; - - std::string autoDetectPossibleDrivers(); - - // part of original telegram bytes, only filled if pre-processing modifies it - std::vector original; - -private: - - bool is_simulated_ {}; - bool being_analyzed_ {}; - bool parser_warns_ = true; - MeterKeys *meter_keys {}; - - // Fixes quirks from non-compliant meters to make telegram compatible with the standard - void preProcess(); - - bool parseMBusDLLandTPL(std::vector::iterator &pos); - - bool parseDLL(std::vector::iterator &pos); - bool parseELL(std::vector::iterator &pos); - bool parseNWL(std::vector::iterator &pos); - bool parseAFL(std::vector::iterator &pos); - bool parseTPL(std::vector::iterator &pos); - - void printDLL(); - void printELL(); - void printNWL(); - void printAFL(); - void printTPL(); - - bool parse_TPL_72 (std::vector::iterator &pos); - bool parse_TPL_78 (std::vector::iterator &pos); - bool parse_TPL_79 (std::vector::iterator &pos); - bool parse_TPL_7A (std::vector::iterator &pos); - bool alreadyDecryptedCBC (std::vector::iterator &pos); - bool potentiallyDecrypt (std::vector::iterator &pos); - bool parseTPLConfig(std::vector::iterator &pos); - static std::string toStringFromELLSN(int sn); - static std::string toStringFromTPLConfig(int cfg); - static std::string toStringFromAFLFC(int fc); - static std::string toStringFromAFLMC(int mc); - - bool parseShortTPL(std::vector::iterator &pos); - bool parseLongTPL(std::vector::iterator &pos); - bool checkMAC(std::vector &frame, - std::vector::iterator from, - std::vector::iterator to, - std::vector &mac, - std::vector &mackey); - bool findFormatBytesFromKnownMeterSignatures(std::vector *format_bytes); -}; - -struct SendBusContent -{ - LinkMode link_mode; - TelegramFormat format; - std::string bus; - std::string content; - - static bool isLikely(const std::string &s); - bool parse(const std::string &s); -}; - -struct Meter; - -std::string manufacturer(int m_field); -std::string mediaType(int a_field_device_type, int m_field); -std::string mediaTypeJSON(int a_field_device_type, int m_field); -bool isCiFieldOfType(int ci_field, CI_TYPE type); -int ciFieldLength(int ci_field); -bool isCiFieldManufacturerSpecific(int ci_field); -std::string ciType(int ci_field); -std::string cType(int c_field); -bool isValidWMBusCField(int c_field); -bool isValidMBusCField(int c_field); -std::string ccType(int cc_field); -std::string difType(int dif); -double vifScale(int vif); -std::string vifKey(int vif); // E.g. temperature energy power mass_flow volume_flow -std::string vifUnit(int vif); // E.g. m3 c kwh kw MJ MJh -std::string vifType(int vif); // Long description -std::string vifeType(int dif, int vif, int vife); // Long description - -// Decode only the standard defined bits in the tpl status byte. Ignore the top 3 bits. -// Return "OK" if sts == 0 -std::string decodeTPLStatusByteOnlyStandardBits(uchar sts); -// Decode the standard bits and report the top 3 bits if set as for example: UNKNOWN_0x80 -// Return "OK" if sts == 0 -std::string decodeTPLStatusByteNoMfct(uchar sts); -// Decode the standard bits and translate the top 3 bits if set. -// Return "OK" if sts == 0 -std::string decodeTPLStatusByteWithMfct(uchar sts, Translate::Lookup &lookup); - -int difLenBytes(int dif); -MeasurementType difMeasurementType(int dif); - -std::string linkModeName(LinkMode link_mode); -std::string measurementTypeName(MeasurementType mt); - -enum FrameStatus { PartialFrame, FullFrame, ErrorInFrame, TextAndNotFrame }; -const char *toString(FrameStatus fs); - - -FrameStatus checkWMBusFrame (std::vector &data, - size_t *frame_length, - int *payload_len_out, - int *payload_offset, - bool only_test); - -FrameStatus checkMBusFrame (std::vector &data, - size_t *frame_length, - int *payload_len_out, - int *payload_offset, - bool only_test); - -// Remember meters id/mfct/ver/type combos that we should only warn once for. -bool warned_for_telegram_before(Telegram *t, std::vector &dll_a); - -////////////////// MBUS - -const char *mbusCField(uchar c_field); -const char *mbusCiField(uchar ci_field); - -int genericifyMedia(int media); -bool isCloseEnough(int media1, int media2); - -#endif diff --git a/components/wmbus_common/wmbus_utils.cpp b/components/wmbus_common/wmbus_utils.cpp deleted file mode 100644 index 13df93be..00000000 --- a/components/wmbus_common/wmbus_utils.cpp +++ /dev/null @@ -1,276 +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"aes.h" -#include"util.h" -#include"wmbus.h" - -#include -#include - -bool decrypt_ELL_AES_CTR(Telegram *t, std::vector &frame, std::vector::iterator &pos, std::vector &aeskey) -{ - if (aeskey.size() == 0) return true; - - std::vector encrypted_bytes; - std::vector decrypted_bytes; - encrypted_bytes.insert(encrypted_bytes.end(), pos, frame.end()); - debugPayload("(ELL) decrypting", encrypted_bytes); - - uchar iv[16]; - int i=0; - // M-field - iv[i++] = t->dll_mfct_b[0]; iv[i++] = t->dll_mfct_b[1]; - // A-field - for (int j=0; j<6; ++j) { iv[i++] = t->dll_a[j]; } - // CC-field - // Two bits should be zeroed out: - // 0x10 H-field Hop-count set when telegram is repeated - // 0x02 R-field Repeated access field - iv[i++] = t->ell_cc & ~(0x10) & ~(0x02); - // SN-field - for (int j=0; j<4; ++j) { iv[i++] = t->ell_sn_b[j]; } - // FN - iv[i++] = 0; iv[i++] = 0; - // BC - iv[i++] = 0; - - std::vector ivv(iv, iv+16); - std::string s = bin2hex(ivv); - debug("(ELL) IV %s\n", s.c_str()); - - int block = 0; - for (size_t offset = 0; offset < encrypted_bytes.size(); offset += 16) - { - size_t block_size = 16; - if (offset + block_size > encrypted_bytes.size()) - { - block_size = encrypted_bytes.size() - offset; - } - - assert(block_size > 0 && block_size <= 16); - - // Generate the pseudo-random bits from the IV and the key. - uchar xordata[16]; - AES_ECB_encrypt(iv, safeButUnsafeVectorPtr(aeskey), xordata, 16); - - // Xor the data with the pseudo-random bits to decrypt into tmp. - uchar tmp[block_size]; - xorit(xordata, &encrypted_bytes[offset], tmp, block_size); - - debug("(ELL) block %d block_size %d offset %zu\n", block, block_size, offset); - block++; - - std::vector tmpv(tmp, tmp+block_size); - debugPayload("(ELL) decrypted", tmpv); - - decrypted_bytes.insert(decrypted_bytes.end(), tmpv.begin(), tmpv.end()); - - incrementIV(iv, sizeof(iv)); - } - debugPayload("(ELL) decrypted", decrypted_bytes); - - // Remove the encrypted bytes. - frame.erase(pos, frame.end()); - // Insert the decrypted bytes. - frame.insert(frame.end(), decrypted_bytes.begin(), decrypted_bytes.end()); - - return true; -} - -std::string frameTypeKamstrupC1(int ft) { - if (ft == 0x78) return "long frame"; - if (ft == 0x79) return "short frame"; - return "?"; -} - -bool decrypt_TPL_AES_CBC_IV(Telegram *t, - std::vector &frame, - std::vector::iterator &pos, - std::vector &aeskey, - int *num_encrypted_bytes, - int *num_not_encrypted_at_end) -{ - std::vector buffer; - buffer.insert(buffer.end(), pos, frame.end()); - - size_t num_bytes_to_decrypt = frame.end()-pos; - - if (t->tpl_num_encr_blocks) - { - num_bytes_to_decrypt = t->tpl_num_encr_blocks*16; - } - - *num_encrypted_bytes = num_bytes_to_decrypt; - - if (buffer.size() < num_bytes_to_decrypt) - { - warning("(TPL) warning: aes-cbc-iv decryption received less bytes than expected for decryption! " - "Got %zu bytes but expected at least %zu bytes since num encr blocks was %d.\n", - buffer.size(), num_bytes_to_decrypt, - t->tpl_num_encr_blocks); - num_bytes_to_decrypt = buffer.size(); - *num_encrypted_bytes = num_bytes_to_decrypt; - - // We must have at least 16 bytes to decrypt. Give up otherwise. - if (num_bytes_to_decrypt < 16) return false; - } - - *num_not_encrypted_at_end = buffer.size()-num_bytes_to_decrypt; - - debug("(TPL) num encrypted blocks %zu (%d bytes and remaining unencrypted %zu bytes)\n", - t->tpl_num_encr_blocks, num_bytes_to_decrypt, buffer.size()-num_bytes_to_decrypt); - - if (aeskey.size() == 0) return false; - - debugPayload("(TPL) AES CBC IV decrypting", buffer); - - // The content should be a multiple of 16 since we are using AES CBC mode. - if (num_bytes_to_decrypt % 16 != 0) - { - warning("(TPL) warning: decryption received non-multiple of 16 bytes! " - "Got %zu bytes shrinking message to %zu bytes.\n", - num_bytes_to_decrypt, num_bytes_to_decrypt - num_bytes_to_decrypt % 16); - num_bytes_to_decrypt -= num_bytes_to_decrypt % 16; - *num_encrypted_bytes = num_bytes_to_decrypt; - assert (num_bytes_to_decrypt % 16 == 0); - // There must be at least 16 bytes remaining. - if (num_bytes_to_decrypt < 16) return false; - } - - uchar iv[16]; - int i=0; - // If there is a tpl_id, then use it, else use ddl_id. - if (t->tpl_id_found) - { - // M-field - iv[i++] = t->tpl_mfct_b[0]; iv[i++] = t->tpl_mfct_b[1]; - - // A-field - for (int j=0; j<6; ++j) { iv[i++] = t->tpl_a[j]; } - } - else - { - // M-field - iv[i++] = t->dll_mfct_b[0]; iv[i++] = t->dll_mfct_b[1]; - - // A-field - for (int j=0; j<6; ++j) { iv[i++] = t->dll_a[j]; } - } - - // ACC - for (int j=0; j<8; ++j) { iv[i++] = t->tpl_acc; } - - std::vector ivv(iv, iv+16); - std::string s = bin2hex(ivv); - debug("(TPL) IV %s\n", s.c_str()); - - uchar buffer_data[num_bytes_to_decrypt]; - memcpy(buffer_data, safeButUnsafeVectorPtr(buffer), num_bytes_to_decrypt); - uchar decrypted_data[num_bytes_to_decrypt]; - - AES_CBC_decrypt_buffer(decrypted_data, buffer_data, num_bytes_to_decrypt, safeButUnsafeVectorPtr(aeskey), iv); - - // Remove the encrypted bytes. - frame.erase(pos, frame.end()); - - // Insert the decrypted bytes. - frame.insert(frame.end(), decrypted_data, decrypted_data+num_bytes_to_decrypt); - - debugPayload("(TPL) decrypted ", frame, pos); - - if (num_bytes_to_decrypt < buffer.size()) - { - frame.insert(frame.end(), buffer.begin()+num_bytes_to_decrypt, buffer.end()); - debugPayload("(TPL) appended ", frame, pos); - } - return true; -} - -bool decrypt_TPL_AES_CBC_NO_IV(Telegram *t, std::vector &frame, std::vector::iterator &pos, std::vector &aeskey, - int *num_encrypted_bytes, - int *num_not_encrypted_at_end) -{ - if (aeskey.size() == 0) return true; - - std::vector buffer; - buffer.insert(buffer.end(), pos, frame.end()); - - size_t num_bytes_to_decrypt = buffer.size(); - - if (t->tpl_num_encr_blocks) - { - num_bytes_to_decrypt = t->tpl_num_encr_blocks*16; - } - - *num_encrypted_bytes = num_bytes_to_decrypt; - if (buffer.size() < num_bytes_to_decrypt) - { - warning("(TPL) warning: aes-cbc-no-iv decryption received less bytes than expected for decryption! " - "Got %zu bytes but expected at least %zu bytes since num encr blocks was %d.\n", - buffer.size(), num_bytes_to_decrypt, - t->tpl_num_encr_blocks); - num_bytes_to_decrypt = buffer.size(); - } - - *num_not_encrypted_at_end = buffer.size()-num_bytes_to_decrypt; - - debug("(TPL) num encrypted blocks %d (%d bytes and remaining unencrypted %d bytes)\n", - t->tpl_num_encr_blocks, num_bytes_to_decrypt, buffer.size()-num_bytes_to_decrypt); - - if (aeskey.size() == 0) return false; - - // The content should be a multiple of 16 since we are using AES CBC mode. - if (num_bytes_to_decrypt % 16 != 0) - { - warning("(TPL) warning: decryption received non-multiple of 16 bytes! " - "Got %zu bytes shrinking message to %zu bytes.\n", - num_bytes_to_decrypt, num_bytes_to_decrypt - num_bytes_to_decrypt % 16); - num_bytes_to_decrypt -= num_bytes_to_decrypt % 16; - assert (num_bytes_to_decrypt % 16 == 0); - } - - uchar iv[16]; - memset(iv, 0, sizeof(iv)); - - std::vector ivv(iv, iv+16); - std::string s = bin2hex(ivv); - debug("(TPL) IV %s\n", s.c_str()); - - uchar buffer_data[num_bytes_to_decrypt]; - memcpy(buffer_data, safeButUnsafeVectorPtr(buffer), num_bytes_to_decrypt); - uchar decrypted_data[num_bytes_to_decrypt]; - - AES_CBC_decrypt_buffer(decrypted_data, buffer_data, num_bytes_to_decrypt, safeButUnsafeVectorPtr(aeskey), iv); - - // Remove the encrypted bytes and any potentially not decryptes bytes after. - frame.erase(pos, frame.end()); - - // Insert the decrypted bytes. - frame.insert(frame.end(), decrypted_data, decrypted_data+num_bytes_to_decrypt); - - debugPayload("(TPL) decrypted ", frame, pos); - - if (num_bytes_to_decrypt < buffer.size()) - { - frame.insert(frame.end(), buffer.begin()+num_bytes_to_decrypt, buffer.end()); - debugPayload("(TPL) appended ", frame, pos); - } - - return true; -} diff --git a/components/wmbus_common/wmbus_utils.h b/components/wmbus_common/wmbus_utils.h deleted file mode 100644 index 650a8d4e..00000000 --- a/components/wmbus_common/wmbus_utils.h +++ /dev/null @@ -1,34 +0,0 @@ -/* - Copyright (C) 2018-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 . -*/ - -#ifndef WMBUS_UTILS_H -#define WMBUS_UTILS_H - -#include "util.h" -#include "wmbus.h" - -bool decrypt_ELL_AES_CTR(Telegram *t, std::vector &frame, std::vector::iterator &pos, std::vector &aeskey); -bool decrypt_TPL_AES_CBC_IV(Telegram *t, std::vector &frame, std::vector::iterator &pos, std::vector &aeskey, - int *num_encrypted_bytes, - int *num_not_encrypted_at_end); -bool decrypt_TPL_AES_CBC_NO_IV(Telegram *t, std::vector &frame, std::vector::iterator &pos, std::vector &aeskey, - int *num_encrypted_bytes, - int *num_not_encrypted_at_end); - -std::string frameTypeKamstrupC1(int ft); - -#endif diff --git a/components/wmbus_meter/README.md b/components/wmbus_meter/README.md deleted file mode 100644 index 4d39948d..00000000 --- a/components/wmbus_meter/README.md +++ /dev/null @@ -1,14 +0,0 @@ -`wmbus_common` component -======================== - -This component is based on git subtree of https://github.com/wmbusmeters/wmbusmeters - -In order to update the subtree, run the following command: - -```bash -git subtree pull --prefix components/wmbus_common https://github.com/wmbusmeters/wmbusmeters.git REF --squash -m "Merge wmbusmeters REF" -``` - -Where `REF` is the branch or tag you want to pull from the `wmbusmeters` repository. - -**Important:** Do not change merge commit message, as it is used to track the source of the subtree. \ No newline at end of file diff --git a/components/wmbus_meter/__init__.py b/components/wmbus_meter/__init__.py index 4ed9fc63..f4cf8386 100644 --- a/components/wmbus_meter/__init__.py +++ b/components/wmbus_meter/__init__.py @@ -6,16 +6,20 @@ MQTTPublishAction, mqtt_publish_action_to_code, ) -from esphome.components.wmbus_common.driver_loader import DriverManager +from esphome.components.wmbus_common.drivers.loader import ( + AutoDriver, + DriverManager, + validate_meter_type, +) from esphome.components.wmbus_radio import RadioComponent from esphome.const import ( CONF_ID, CONF_KEY, - CONF_MODE, CONF_PAYLOAD, CONF_TRIGGER_ID, CONF_TYPE, ) +from esphome.core import HexInt CONF_METER_ID = "meter_id" CONF_RADIO_ID = "radio_id" @@ -42,10 +46,11 @@ def hex_key_validator(key): try: key = cv.bind_key(key) - return key except cv.Invalid as e: raise cv.Invalid(e.msg.replace("Bind key", "Key")) + return bytes.fromhex(key) + def meter_id_validator(meter_id): meter_id = cv.string(meter_id).lower().removeprefix("0x") @@ -58,7 +63,7 @@ def meter_id_validator(meter_id): if value < 0: raise cv.Invalid("Meter ID must be a positive hexadecimal integer") - return f"{value:08x}" + return HexInt(value) CONFIG_SCHEMA = cv.Schema( @@ -66,10 +71,7 @@ def meter_id_validator(meter_id): cv.GenerateID(): cv.declare_id(Meter), cv.GenerateID(CONF_RADIO_ID): cv.use_id(RadioComponent), cv.Required(CONF_METER_ID): meter_id_validator, - cv.Required(CONF_TYPE): cv.All( - cv.one_of(*DriverManager.available_drivers), - DriverManager.request_driver, - ), + cv.Required(CONF_TYPE): validate_meter_type, cv.Optional(CONF_KEY): cv.Any( cv.All(cv.string_strict, lambda s: s.encode().hex(), hex_key_validator), hex_key_validator, @@ -77,22 +79,26 @@ def meter_id_validator(meter_id): cv.Optional(CONF_ON_TELEGRAM): automation.validate_automation( {cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(TelegramTrigger)}, ), - cv.Optional(CONF_MODE): cv.one_of("c1", "t1", lower=True), }, ).extend(cv.COMPONENT_SCHEMA) async def to_code(config): meter = cg.new_Pvariable(config[CONF_ID]) - driver_type = config[CONF_TYPE].name - if CONF_MODE in config: - driver_type += ":" + config[CONF_MODE] + driver = config[CONF_TYPE] + + # `auto` detects the driver from the first telegram. + if isinstance(driver, AutoDriver): + spec = cg.RawExpression("nullptr") + else: + index = DriverManager.registry_index(driver) + spec = cg.RawExpression(f"wmbus::registered_drivers[{index}] /* {driver.name} */") + key = config.get(CONF_KEY) + bytes_or_nothing = cg.ArrayInitializer(*map(HexInt, key)) if key else None cg.add( - meter.set_meter_params( - config[CONF_METER_ID], - driver_type, - config.get(CONF_KEY, ""), + meter.set_driver( + spec, config[CONF_METER_ID], cg.ArrayInitializer(bytes_or_nothing) ) ) diff --git a/components/wmbus_meter/base_sensor.cpp b/components/wmbus_meter/base_sensor.cpp index 03975df8..74eb6931 100644 --- a/components/wmbus_meter/base_sensor.cpp +++ b/components/wmbus_meter/base_sensor.cpp @@ -5,7 +5,7 @@ namespace esphome { namespace wmbus_meter { static const char *TAG = "wmbus_meter.base_sensor"; -void BaseSensor::set_field_name(std::string field_name) { this->field_name = field_name; } +void BaseSensor::set_field_name(std::string field_name) { this->field_name = std::move(field_name); } void BaseSensor::set_parent(Meter *parent) { Parented::set_parent(parent); diff --git a/components/wmbus_meter/base_sensor.py b/components/wmbus_meter/base_sensor.py index f72a7196..be5ea3cf 100644 --- a/components/wmbus_meter/base_sensor.py +++ b/components/wmbus_meter/base_sensor.py @@ -24,12 +24,10 @@ def get_driver(config): return fc.get_config_for_path(parent_type_path)["type"] -def make_field_validator(field_type): +def make_field_validator(kind): def field_validator(config): driver = get_driver(config) - field_name = config[CONF_FIELD] - - driver.request_field(field_name, field_type) + driver.request_field(config[CONF_FIELD], kind) return field_validator diff --git a/components/wmbus_meter/sensor.cpp b/components/wmbus_meter/sensor.cpp index 2f2740af..015de56d 100644 --- a/components/wmbus_meter/sensor.cpp +++ b/components/wmbus_meter/sensor.cpp @@ -2,32 +2,12 @@ namespace esphome { namespace wmbus_meter { -static const char *TAG = "wmbus_meter.sensor"; void Sensor::handle_update() { auto val = this->parent_->get_numeric_field(this->field_name); - if (!val.has_value()) - return; - - this->publish_state(*val); - - if (this->dynamic_decimals_) { - auto str = std::to_string(this->state); - - while (str.back() == '0') - str.pop_back(); - - auto decimals = 0; - while (str.back() != '.') { - str.pop_back(); - decimals++; - } - - this->set_accuracy_decimals(decimals); - } + if (val.has_value()) + this->publish_state(*val); } -void Sensor::set_dynamic_decimals(bool dynamic_decimals) { this->dynamic_decimals_ = dynamic_decimals; } - } // namespace wmbus_meter } // namespace esphome diff --git a/components/wmbus_meter/sensor.h b/components/wmbus_meter/sensor.h index adb48260..0defc93b 100644 --- a/components/wmbus_meter/sensor.h +++ b/components/wmbus_meter/sensor.h @@ -8,10 +8,6 @@ namespace wmbus_meter { class Sensor : public sensor::Sensor, public BaseSensor { public: void handle_update(); - void set_dynamic_decimals(bool dynamic_decimals); - - protected: - bool dynamic_decimals_{true}; }; } // namespace wmbus_meter } // namespace esphome diff --git a/components/wmbus_meter/sensor.py b/components/wmbus_meter/sensor.py index 33ab07c6..5fdc24bf 100755 --- a/components/wmbus_meter/sensor.py +++ b/components/wmbus_meter/sensor.py @@ -1,10 +1,7 @@ from esphome import codegen as cg from esphome import config_validation as cv from esphome.components import sensor -from esphome.components.wmbus_common.driver_loader import ( - FieldType, - get_human_readable_unit, -) +from esphome.components.wmbus_common.drivers.loader import UNITS, FieldKind, Unit, split_unit from esphome.const import CONF_ACCURACY_DECIMALS, CONF_UNIT_OF_MEASUREMENT from . import wmbus_meter_ns @@ -18,23 +15,47 @@ RegularSensor = wmbus_meter_ns.class_("Sensor", BaseSensor, sensor.Sensor) - -def default_unit_of_measurement(config): - if CONF_UNIT_OF_MEASUREMENT not in config: - config[CONF_UNIT_OF_MEASUREMENT] = get_human_readable_unit(config[CONF_FIELD]) +# The resolution meters commonly send each unit in; any other unit is a whole number. +DEFAULT_ACCURACY_DECIMALS = { + Unit.M3: 3, + Unit.M3H: 3, + Unit.KWH: 3, + Unit.KVARH: 3, + Unit.KVAH: 3, + Unit.KW: 3, + Unit.KVAR: 3, + Unit.KVA: 3, + Unit.Ampere: 3, + Unit.FACTOR: 3, + Unit.C: 2, + Unit.Volt: 2, + Unit.Hertz: 2, + Unit.BAR: 2, + Unit.RH: 1, + Unit.DEGREE: 1, + Unit.Year: 1, +} + + +def unit_defaults(config): + _, unit = split_unit(config[CONF_FIELD]) + if unit: + config.setdefault(CONF_UNIT_OF_MEASUREMENT, UNITS[unit].display) + config.setdefault(CONF_ACCURACY_DECIMALS, DEFAULT_ACCURACY_DECIMALS.get(unit, 0)) + else: + config.setdefault(CONF_ACCURACY_DECIMALS, 0) return config CONFIG_SCHEMA = cv.All( BASE_SCHEMA.extend(sensor.sensor_schema(RegularSensor)), - default_unit_of_measurement, + unit_defaults, ) -FINAL_VALIDATE_SCHEMA = make_field_validator(FieldType.NUMERIC) +FINAL_VALIDATE_SCHEMA = make_field_validator(FieldKind.Numeric) async def to_code(config): cg.add_define("USE_WMBUS_METER_SENSOR") sensor_ = await sensor.new_sensor(config) - cg.add(sensor_.set_dynamic_decimals(CONF_ACCURACY_DECIMALS not in config)) await register_meter(sensor_, config) diff --git a/components/wmbus_meter/text_sensor.py b/components/wmbus_meter/text_sensor.py index 96e8cd65..266eaba2 100755 --- a/components/wmbus_meter/text_sensor.py +++ b/components/wmbus_meter/text_sensor.py @@ -1,6 +1,6 @@ from esphome import codegen as cg from esphome.components import text_sensor -from esphome.components.wmbus_common.driver_loader import FieldType +from esphome.components.wmbus_common.drivers.loader import FieldKind from . import wmbus_meter_ns from .base_sensor import ( @@ -14,7 +14,7 @@ CONFIG_SCHEMA = BASE_SCHEMA.extend(text_sensor.text_sensor_schema(TextSensor)) -FINAL_VALIDATE_SCHEMA = make_field_validator(FieldType.STRING) +FINAL_VALIDATE_SCHEMA = make_field_validator(FieldKind.Text) async def to_code(config): diff --git a/components/wmbus_meter/wmbus_meter.cpp b/components/wmbus_meter/wmbus_meter.cpp index 3da341b5..afa297b0 100644 --- a/components/wmbus_meter/wmbus_meter.cpp +++ b/components/wmbus_meter/wmbus_meter.cpp @@ -1,32 +1,18 @@ #include "wmbus_meter.h" +#include "esphome/core/alloc_helpers.h" +#include "esphome/core/log.h" + +#include namespace esphome { namespace wmbus_meter { static const char *TAG = "wmbus_meter"; -void Meter::set_meter_params(std::string id, std::string driver, std::string key) { - MeterInfo meter_info; - meter_info.parse(driver + '-' + id, driver, id + ',', key); - - this->meter = createMeter(&meter_info); +void Meter::set_driver(const wmbus::DriverSpec *spec, uint32_t id, std::optional> key) { + this->state_ = wmbus::MeterState(spec, id, std::move(key)); } -std::string Meter::get_id() const { - if (!this->meter) - return ""; - return this->meter->addressExpressions()[0].id; -} -MeterType Meter::get_type() const { - if (!this->meter) - return MeterType::UnknownMeter; - return this->meter->driverInfo()->type(); -} -std::string Meter::get_driver_name() const { - if (!this->meter) - return ""; - return this->meter->driverName().str(); -} -bool Meter::is_encrypted() const { return this->meter && this->meter->meterKeys()->hasConfidentialityKey(); } +std::string Meter::get_id() const { return str_sprintf("%08x", (unsigned) this->state_.id); } void Meter::set_radio(wmbus_radio::Radio *radio) { this->radio = radio; @@ -35,68 +21,74 @@ void Meter::set_radio(wmbus_radio::Radio *radio) { void Meter::dump_config() { ESP_LOGCONFIG(TAG, "wM-Bus Meter:"); - ESP_LOGCONFIG(TAG, " ID: %s", this->get_id().c_str()); - ESP_LOGCONFIG(TAG, " Driver: %s", this->get_driver_name().c_str()); - ESP_LOGCONFIG(TAG, " Key: %s", this->is_encrypted() ? "***" : "not-encrypted"); + ESP_LOGCONFIG(TAG, " ID: %08x", (unsigned) this->state_.id); + ESP_LOGCONFIG(TAG, " Driver: %s", this->state_.spec == nullptr ? "auto" : this->state_.spec->name); + ESP_LOGCONFIG(TAG, " Key: %s", this->state_.key ? "***" : "not-encrypted"); } -void Meter::handle_frame(wmbus_radio::Frame *frame) { - auto about = AboutTelegram(App.get_friendly_name(), frame->rssi(), frame->link_mode(), FrameType::WMBUS); - - std::vector
adresses; - bool id_match = false; - auto telegram = std::make_unique(); - - this->meter->handleTelegram(about, frame->data(), false, &adresses, &id_match, telegram.get()); - - if (id_match) { - this->last_telegram = std::move(telegram); - this->defer([this]() { - this->on_telegram_callback_manager(); - this->last_telegram = nullptr; - }); +void Meter::on_telegram(std::function &&callback) { + this->on_telegram_callback_manager.add(std::move(callback)); +} - frame->mark_as_handled(); +void Meter::handle_frame(wmbus_radio::Frame *frame) { + switch (this->state_.handle_telegram(frame->data(), frame->rssi())) { + case wmbus::Result::NotForThisMeter: + return; + + case wmbus::Result::NotDecoded: + // Ours but unreadable: sensors are not refreshed with stale values. + frame->mark_as_handled(); + ESP_LOGW(TAG, "Telegram from %08x could not be decoded: %s", (unsigned) this->state_.id, + this->state_.last_error.c_str()); + return; + + case wmbus::Result::Ok: + frame->mark_as_handled(); + this->defer([this]() { this->on_telegram_callback_manager(); }); + return; } } -std::string Meter::as_json(bool pretty_print) { - std::string json; - this->meter->printMeter(this->last_telegram.get(), nullptr, nullptr, '\t', &json, nullptr, nullptr, nullptr, - pretty_print); - return json; +std::string Meter::as_json() { + if (!this->state_.timestamp) + return "{}"; + return this->state_.to_json(); } optional Meter::get_string_field(std::string field_name) { - auto field_info = this->meter->findFieldInfo(field_name, Quantity::Text); - if (field_info) - return this->meter->getStringValue(field_info); + if (!this->state_.timestamp) + return {}; + if (field_name == "media") + return std::string(this->state_.media_type); + if (field_name == "timestamp") + return wmbus::format_time((double) *this->state_.timestamp, wmbus::TimeFormat::TimestampUTC); - return {}; + const wmbus::Value value = this->state_.value(field_name); + if (const double *number = std::get_if(&value)) { + const wmbus::FieldSpec *f = this->state_.find_field(field_name); + if (f && f->json_date && !std::isnan(*number)) + return wmbus::format_time(*number, *f->json_date); + } + if (!std::holds_alternative(value)) + return {}; + return std::get(value); } optional Meter::get_numeric_field(std::string field_name) { - // RSSI is not handled by meter but by telegram :/ - if (field_name == "rssi_dbm") - return this->last_telegram->about.rssi_dbm; + if (!this->state_.timestamp) + return {}; + // RSSI and timestamp describe the reception, not the meter, so neither is a field. + if (field_name == "rssi_dbm") + return this->state_.rssi_dbm; if (field_name == "timestamp") - return this->meter->timestampLastUpdate(); - - std::string name; - Unit unit; - extractUnit(field_name, &name, &unit); + return *this->state_.timestamp; - auto value = this->meter->getNumericValue(name, unit); - - if (!std::isnan(value)) - return value; - - return {}; -} - -void Meter::on_telegram(std::function &&callback) { - this->on_telegram_callback_manager.add(std::move(callback)); + const wmbus::Value value = this->state_.value(field_name); + if (!std::holds_alternative(value)) + return {}; + const double number = std::get(value); + return std::isnan(number) ? optional{} : optional((float) number); } } // namespace wmbus_meter diff --git a/components/wmbus_meter/wmbus_meter.h b/components/wmbus_meter/wmbus_meter.h index 8a7354e1..0c7bb4de 100644 --- a/components/wmbus_meter/wmbus_meter.h +++ b/components/wmbus_meter/wmbus_meter.h @@ -1,15 +1,20 @@ #pragma once +#include +#include +#include + #include "esphome/core/component.h" #include "esphome/core/helpers.h" #include "esphome/components/wmbus_radio/component.h" -#include "esphome/components/wmbus_common/meters.h" +#include "esphome/components/wmbus_common/meter.h" namespace esphome { namespace wmbus_meter { class Meter : public Component { public: - void set_meter_params(std::string id, std::string driver, std::string key); + // A null spec is `type: auto`: the driver is detected from the first telegram. + void set_driver(const wmbus::DriverSpec *spec, uint32_t id, std::optional> key); void set_radio(wmbus_radio::Radio *radio); void dump_config() override; @@ -17,24 +22,19 @@ class Meter : public Component { void on_telegram(std::function &&callback); std::string get_id() const; - MeterType get_type() const; - std::string as_json(bool pretty_print = false); + std::string as_json(); optional get_string_field(std::string field_name); optional get_numeric_field(std::string field_name); protected: - std::string get_driver_name() const; - bool is_encrypted() const; + wmbus::MeterState state_; wmbus_radio::Radio *radio; - std::shared_ptr<::Meter> meter; - std::unique_ptr last_telegram; - CallbackManager on_telegram_callback_manager; virtual void handle_frame(wmbus_radio::Frame *frame); }; } // namespace wmbus_meter -} // namespace esphome \ No newline at end of file +} // namespace esphome diff --git a/components/wmbus_radio/packet.cpp b/components/wmbus_radio/packet.cpp index 891adef5..b4fbe6a1 100644 --- a/components/wmbus_radio/packet.cpp +++ b/components/wmbus_radio/packet.cpp @@ -1,26 +1,28 @@ #include "packet.h" +#include #include -#include "esphome/core/helpers.h" -#include "esphome/components/wmbus_common/wmbus.h" - #include "decode3of6.h" +#include "esphome/components/wmbus_common/telegram.h" +#include "esphome/core/helpers.h" #include "esphome/core/log.h" -// 3 bytes for mode C marks + len or first 3 bytes of mode T to decode into 2 bytes -#define WMBUS_PACKET_HEAD_SIZE (3) - -// Mode C frame starts with \x54\xCD or \x54\x3D -#define WMBUS_MODE_C_MARK (0x54) -#define WMBUS_MODE_C_BLOCK_A_MARK (0xCD) -#define WMBUS_MODE_C_BLOCK_B_MARK (0x3D) -#define WMBUS_MODE_C_MARKS_LEN (2) - namespace esphome { namespace wmbus_radio { static const char *TAG = "wmbus"; +const char *toString(LinkMode link_mode) { + switch (link_mode) { + case LinkMode::C1: + return "C1"; + case LinkMode::T1: + return "T1"; + default: + return ""; + } +} + const char *toString(BlockType type) { switch (type) { case BlockType::A: @@ -32,81 +34,244 @@ const char *toString(BlockType type) { } } -Packet::Packet() : data_(WMBUS_PACKET_HEAD_SIZE) {} +bool FrameFormat::crc_ok(std::span data, std::span crc_bytes) { + uint16_t calc = esphome::crc16be(data.data(), data.size(), 0x0000, 0x3D65, false, true); + uint16_t check = esphome::encode_uint16(crc_bytes[0], crc_bytes[1]); + return calc == check; +} -std::span Packet::head() { return std::span(this->data_).first(WMBUS_PACKET_HEAD_SIZE); } +bool FrameFormat::is_complete(std::span data) const { + return data.size() >= 11 && (data[1] == 0x44 || data[1] == 0x46); +} -std::span Packet::rest() { - const auto head = this->head(); +namespace { + +class FrameFormatA final : public FrameFormat { + public: + BlockType type() const override { return BlockType::A; } + + size_t frame_size(uint8_t l_field) const override { + if (l_field == 0) + return 0; + const size_t nr_blocks = l_field < 26 ? 2 : (l_field - 26) / 16 + 3; + return 1 + l_field + 2 * nr_blocks; + } + + bool strip_crcs(std::vector &payload) const override { + const std::span bytes{payload}; + const size_t len = bytes.size(); + if (len < 12) + return false; + if (!crc_ok(bytes.first(10), bytes.subspan(10).first<2>())) + return false; + + size_t pos = 12; + for (; pos + 18 <= len; pos += 18) { + if (!crc_ok(bytes.subspan(pos, 16), bytes.subspan(pos + 16).first<2>())) + return false; + } + const size_t tail = pos + 2 < len ? len - 2 - pos : 0; + if (tail > 0 && !crc_ok(bytes.subspan(pos, tail), bytes.subspan(pos + tail).first<2>())) + return false; - uint8_t l_field; - if (head[0] == WMBUS_MODE_C_MARK) { - this->link_mode_ = LinkMode::C1; - // C1 frame must have valid block type: A or B - switch (head[1]) { - case WMBUS_MODE_C_BLOCK_A_MARK: - this->block_type_ = BlockType::A; - break; - case WMBUS_MODE_C_BLOCK_B_MARK: - this->block_type_ = BlockType::B; - break; - default: - return {}; + size_t out = 10; + for (pos = 12; pos + 18 <= len; pos += 18) { + memmove(payload.data() + out, payload.data() + pos, 16); + out += 16; } - l_field = head[WMBUS_MODE_C_MARKS_LEN]; - } else { - // T1 frame has no block type - this->link_mode_ = LinkMode::T1; - const auto decoded = decode3of6(head); + memmove(payload.data() + out, payload.data() + pos, tail); + out += tail; + payload.resize(out); + payload[0] = static_cast(out - 1); + return true; + } +}; + +class FrameFormatB final : public FrameFormat { + public: + BlockType type() const override { return BlockType::B; } + + size_t frame_size(uint8_t l_field) const override { + if (l_field == 0) + return 0; + return 1 + l_field; + } + + bool strip_crcs(std::vector &payload) const override { + const std::span bytes{payload}; + const size_t len = bytes.size(); + if (len < 12) + return false; + + size_t crc1_pos, crc2_pos; + if (len <= 128) { + crc1_pos = len - 2; + crc2_pos = 0; + } else { + crc1_pos = 126; + crc2_pos = len - 2; + } + + if (!crc_ok(bytes.first(crc1_pos), bytes.subspan(crc1_pos).first<2>())) + return false; + size_t out = crc1_pos; + if (crc2_pos > 0) { + const size_t from2 = crc1_pos + 2; + if (!crc_ok(bytes.subspan(from2, crc2_pos - from2), bytes.subspan(crc2_pos).first<2>())) + return false; + memmove(payload.data() + out, payload.data() + from2, crc2_pos - from2); + out += crc2_pos - from2; + } + payload.resize(out); + payload[0] = static_cast(out - 1); + return true; + } +}; + +class ModeC1Handler final : public LinkModeHandler { + public: + static constexpr uint8_t MARK = 0x54; + static constexpr uint8_t BLOCK_A_MARK = 0xCD; + static constexpr uint8_t BLOCK_B_MARK = 0x3D; + static constexpr size_t MARKS_LEN = 2; + + LinkMode mode() const override { return LinkMode::C1; } + + BlockType detect_block_type(std::span head) const override { + if (head.size() >= MARKS_LEN && head[0] == MARK) { + if (head[1] == BLOCK_A_MARK) + return BlockType::A; + if (head[1] == BLOCK_B_MARK) + return BlockType::B; + } + return BlockType::UNKNOWN; + } + + uint8_t extract_l_field(std::span head) const override { + return head.size() > MARKS_LEN ? head[MARKS_LEN] : 0; + } + + size_t wire_size(size_t frame_size) const override { return MARKS_LEN + frame_size; } + + bool decode_payload(std::vector &data) const override { + if (data.size() < MARKS_LEN) + return false; + data.erase(data.begin(), data.begin() + MARKS_LEN); + return true; + } +}; + +class ModeT1Handler final : public LinkModeHandler { + public: + LinkMode mode() const override { return LinkMode::T1; } + + BlockType detect_block_type(std::span /*head*/) const override { return BlockType::A; } + + uint8_t extract_l_field(std::span head) const override { + auto decoded = decode3of6(head); + return (decoded && !decoded->empty()) ? (*decoded)[0] : 0; + } + + size_t wire_size(size_t frame_size) const override { return encoded_size(frame_size); } + + bool decode_payload(std::vector &data) const override { + auto decoded = decode3of6(data); if (!decoded) - return {}; - l_field = (*decoded)[0]; + return false; + data = std::move(*decoded); + return true; + } +}; + +} // namespace + +const FrameFormat *FrameFormat::create(BlockType type) { + switch (type) { + case BlockType::A: { + static const FrameFormatA format_a; + return &format_a; + } + case BlockType::B: { + static const FrameFormatB format_b; + return &format_b; + } + default: + return nullptr; + } +} + +const LinkModeHandler *LinkModeHandler::create(std::span initial_bytes) { + if (initial_bytes.empty()) + return nullptr; + if (initial_bytes[0] == ModeC1Handler::MARK) { + static const ModeC1Handler c1_handler; + return &c1_handler; } + static const ModeT1Handler t1_handler; + return &t1_handler; +} + +Packet::Packet() : data_(HEAD_SIZE) {} + +std::span Packet::head() { return std::span(this->data_).first(HEAD_SIZE); } +std::span Packet::rest() { + const auto head = this->head(); + + this->mode_handler_ = LinkModeHandler::create(head); + if (!this->mode_handler_) + return {}; + + this->link_mode_ = this->mode_handler_->mode(); + this->block_type_ = this->mode_handler_->detect_block_type(head); + if (this->block_type_ == BlockType::UNKNOWN) + return {}; + + this->format_ = FrameFormat::create(this->block_type_); + if (!this->format_) + return {}; + + const uint8_t l_field = this->mode_handler_->extract_l_field(head); if (l_field == 0) return {}; - // Format A - // L-field = length without CRC fields and without L (1 byte) - // Format B - // L-field = length with CRC fields and without L (1 byte) - // The 2 first blocks contains 25 bytes when excluding CRC and the L-field - // The other blocks contains 16 bytes when excluding the CRC-fields - const size_t blocks = l_field < 26 ? 2 : (l_field - 26) / 16 + 3; - const size_t frame_size = this->block_type_ == BlockType::B ? 1 + l_field : 1 + l_field + 2 * blocks; - - this->data_.resize(this->link_mode_ == LinkMode::C1 ? WMBUS_MODE_C_MARKS_LEN + frame_size : encoded_size(frame_size)); - return std::span(this->data_).subspan(WMBUS_PACKET_HEAD_SIZE); + const size_t frame_bytes = this->format_->frame_size(l_field); + const size_t total_wire_bytes = this->mode_handler_->wire_size(frame_bytes); + if (total_wire_bytes < HEAD_SIZE) + return {}; + + this->data_.resize(total_wire_bytes); + return std::span(this->data_).subspan(HEAD_SIZE); } void Packet::set_rssi(int8_t rssi) { this->rssi_ = rssi; } std::optional Packet::convert_to_frame() { - ESP_LOGD(TAG, "Try to make frame from packet %s%s of size %zu", linkModeName(this->link_mode_).c_str(), + ESP_LOGD(TAG, "Try to make frame from packet %s%s of size %zu", toString(this->link_mode_), toString(this->block_type_), this->data_.size()); - std::vector data; - if (this->link_mode_ == LinkMode::C1) { - const auto frame = std::span(this->data_).subspan(WMBUS_MODE_C_MARKS_LEN); - data.assign(frame.begin(), frame.end()); - } else { - auto decoded = decode3of6(this->data_); - if (!decoded) - return {}; - data = std::move(*decoded); + if (!this->mode_handler_ || !this->format_) + return {}; + + if (!this->mode_handler_->decode_payload(this->data_)) { + ESP_LOGD(TAG, "Dropped a packet of %zu bytes: decode payload failed", this->data_.size()); + return {}; } - removeAnyDLLCRCs(data); - int dummy; - if (checkWMBusFrame(data, (size_t *) &dummy, &dummy, &dummy, false) != FrameStatus::FullFrame) { - ESP_LOGD(TAG, "Dropped a packet of %zu bytes: bad CRC or not a wM-Bus frame", data.size()); + if (!this->format_->strip_crcs(this->data_)) { + ESP_LOGD(TAG, "Dropped a packet of %zu bytes: bad CRC", this->data_.size()); return {}; } - return Frame(std::move(data), this->link_mode_, this->block_type_, this->rssi_); + if (!this->format_->is_complete(this->data_)) { + ESP_LOGD(TAG, "Dropped a packet of %zu bytes: not a complete wM-Bus frame", this->data_.size()); + return {}; + } + + return Frame(std::move(this->data_), this->link_mode_, this->block_type_, this->rssi_); } -const std::vector &Packet::get_raw_data() const { return data_; } +const std::vector &Packet::get_raw_data() const { return this->data_; } std::vector &Frame::data() { return this->data_; } LinkMode Frame::link_mode() { return this->link_mode_; } @@ -124,21 +289,23 @@ std::string Frame::as_rtlwmbus() { auto output = std::string{}; output.reserve(2 + 5 + 24 + 1 + 4 + 5 + 2 * this->data_.size() + 1); - output += linkModeName(this->link_mode_); // size 2 - output += ";1;1;"; // size 5 - output += time_buffer; // size 24 - output += ';'; // size 1 - output += std::to_string(this->rssi_); // size up to 4 - output += ";;;0x"; // size 5 - output += this->as_hex(); // size 2 * frame.size() - output += "\n"; // size 1 + output += toString(this->link_mode_); // size 2 + output += ";1;1;"; // size 5 + output += time_buffer; // size 24 + output += ';'; // size 1 + output += std::to_string(this->rssi_); // size up to 4 + output += ";;;0x"; // size 5 + output += this->as_hex(); // size 2 * frame.size() + output += "\n"; // size 1 return output; } + std::string Frame::meter_id() { - Telegram telegram; - telegram.parseWMBUSHeader(this->data_); - return telegram.addresses[0].str(); + wmbus::Telegram telegram; + if (!telegram.parse_envelope(this->data_)) + return ""; + return str_sprintf("%08x", (unsigned) telegram.sender().id); } void Frame::mark_as_handled() { this->frame_handlers_count_++; } diff --git a/components/wmbus_radio/packet.h b/components/wmbus_radio/packet.h index 74ad8abf..b68f59f4 100644 --- a/components/wmbus_radio/packet.h +++ b/components/wmbus_radio/packet.h @@ -1,6 +1,6 @@ #pragma once -#include #include +#include #include #include #include @@ -8,19 +8,48 @@ #include #include "esphome/core/helpers.h" -#include "esphome/components/wmbus_common/wmbus.h" namespace esphome { namespace wmbus_radio { +enum class LinkMode { UNKNOWN = 0, C1, T1 }; enum class BlockType { UNKNOWN = 0, A, B }; +const char *toString(LinkMode link_mode); const char *toString(BlockType type); +class FrameFormat { + public: + virtual ~FrameFormat() = default; + virtual BlockType type() const = 0; + virtual size_t frame_size(uint8_t l_field) const = 0; + virtual bool strip_crcs(std::vector &payload) const = 0; + virtual bool is_complete(std::span data) const; + + static const FrameFormat *create(BlockType type); + + protected: + static bool crc_ok(std::span data, std::span crc_bytes); +}; + +class LinkModeHandler { + public: + virtual ~LinkModeHandler() = default; + virtual LinkMode mode() const = 0; + virtual BlockType detect_block_type(std::span head) const = 0; + virtual uint8_t extract_l_field(std::span head) const = 0; + virtual size_t wire_size(size_t frame_size) const = 0; + virtual bool decode_payload(std::vector &data) const = 0; + + static const LinkModeHandler *create(std::span initial_bytes); +}; + struct Frame; struct Packet { public: + static constexpr size_t HEAD_SIZE = 3; + Packet(); std::span head(); @@ -31,11 +60,16 @@ struct Packet { std::optional convert_to_frame(); const std::vector &get_raw_data() const; + LinkMode link_mode() const { return this->link_mode_; } + BlockType block_type() const { return this->block_type_; } + protected: std::vector data_; LinkMode link_mode_ = LinkMode::UNKNOWN; BlockType block_type_ = BlockType::UNKNOWN; - int8_t rssi_; + const LinkModeHandler *mode_handler_{nullptr}; + const FrameFormat *format_{nullptr}; + int8_t rssi_{0}; }; struct Frame { diff --git a/scripts/wmbus_common/check_drivers.py b/scripts/wmbus_common/check_drivers.py new file mode 100755 index 00000000..19b24c6e --- /dev/null +++ b/scripts/wmbus_common/check_drivers.py @@ -0,0 +1,81 @@ +#!/usr/bin/env python3 +"""Runs the XMQ generator over drivers/src and groups the failures by construct. + +Needs esphome importable: run it from the esphome venv. +""" + +import argparse +import sys +from collections import defaultdict +from pathlib import Path + +REPO_ROOT = Path(__file__).resolve().parents[2] +COMPONENTS_DIR = REPO_ROOT / "components" +DRIVERS_DIR = COMPONENTS_DIR / "wmbus_common" / "drivers" / "src" + +sys.path.insert(0, str(COMPONENTS_DIR)) + +from wmbus_common.drivers.loader.driver import Driver # noqa: E402 + + +def describe(error: Exception) -> set[str]: + """voluptuous paths without list indices, so one construct groups across fields.""" + # MultipleInvalid carries every violation, not just the first. + errors = getattr(error, "errors", None) or [error] + + described = set() + for item in errors: + path = getattr(item, "path", None) + if path: + readable = ".".join(str(p) for p in path if not isinstance(p, int)) + described.add(f"unsupported: {readable}") + else: + described.add(f"{type(item).__name__}: {item}") + + return described + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument( + "--verbose", + action="store_true", + help="list every driver, not just the failures", + ) + args = parser.parse_args() + + paths = sorted(DRIVERS_DIR.glob("*.xmq")) + + generated: list[tuple[str, int]] = [] + failures: dict[str, list[str]] = defaultdict(list) + + for path in paths: + driver = Driver.from_source(path) + try: + driver.serialize() + except Exception as error: # noqa: BLE001 - reporting tool + for reason in describe(error): + failures[reason].append(path.stem) + continue + generated.append((path.stem, len(driver.fields))) + + if args.verbose: + for name, fields in generated: + print(f" ok {name:24s} {fields:3d} fields") + + for reason, names in sorted(failures.items(), key=lambda kv: -len(kv[1])): + print(f" FAIL {reason}") + print(f" {len(names)}: {', '.join(names)}") + + total = len(paths) + print( + f"\n{len(generated)}/{total} drivers generated, " + f"{sum(f for _, f in generated)} fields, " + f"{total - len(generated)} failing" + ) + + return 1 if failures else 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/wmbus_common/fetch_upstream.py b/scripts/wmbus_common/fetch_upstream.py new file mode 100755 index 00000000..9ed806e6 --- /dev/null +++ b/scripts/wmbus_common/fetch_upstream.py @@ -0,0 +1,250 @@ +#!/usr/bin/env python3 +"""Fetch a wmbusmeters ref into components/wmbus_common: drivers/src/*.xmq and tables/*.yaml. + +Nothing is committed: review the diff. --diff instead prints upstream's log and diff from the recorded ref. +""" + +import argparse +import contextlib +import io +import math +import re +import subprocess +import sys +import tarfile +import tempfile +from pathlib import Path + +import yaml + +COMPONENT_DIR = Path(__file__).resolve().parents[2] / "components" / "wmbus_common" +DRIVERS_DIR = COMPONENT_DIR / "drivers" / "src" +LIBRARY_FILE = COMPONENT_DIR / "drivers" / "library.xmq" +TABLES_DIR = COMPONENT_DIR / "tables" +TAG_FILE = COMPONENT_DIR / ".wmbusmeters_tag" +UPSTREAM_URL = "https://github.com/wmbusmeters/wmbusmeters.git" +# The C++ this component ports; drivers and tables show up in our own diff. +DIFF_PATHS = ("src",) + +# HZ is a macro in the ESP32-C3 toolchain's sys/param.h. +RENAMED_UNITS = {"HZ": "Hertz"} + + +def unit_name(cname: str) -> str: + return RENAMED_UNITS.get(cname, cname) + + +# SIExp's setters in units.h; unixTimestamp() is spelled out, the rest are the keys. +EXPONENT_KEYS = ("s", "m", "kg", "a", "mol", "cd", "k", "c", "f", "month", "year", "ut") +_SETTER_TO_KEY = {**{k: k for k in EXPONENT_KEYS}, "unixTimestamp": "ut"} + + +def macro_entries(text: str, macro: str) -> list[str]: + start = text.index(f"#define {macro}") + end = text.index("\n\n", start) + body = text[start:end] + entries = [] + pos = 0 + while True: + pos = body.find("X(", pos) + if pos < 0: + break + depth = 0 + i = pos + 1 + while True: + if body[i] == "(": + depth += 1 + elif body[i] == ")": + depth -= 1 + if depth == 0: + break + i += 1 + entries.append(body[pos + 2 : i]) + pos = i + return entries + + +def parse_quantities(units_h: str) -> dict[str, str]: + result = {} + for entry in macro_entries(units_h, "LIST_OF_QUANTITIES"): + quantity, unit = [p.strip() for p in entry.split(",")] + result[quantity] = unit_name(unit) + return result + + +def eval_scale(expr: str) -> float: + if not re.fullmatch(r"[0-9.eE+\-*/ M_PI()]+", expr): + sys.exit(f"unexpected scale expression in units.cc: {expr!r}") + return float(eval(expr, {"__builtins__": {}}, {"M_PI": math.pi})) # noqa: S307 + + +def si_exponents(chain: str) -> dict: + """SIExp().kg(1).m(2).s(-2) -> {kg: 1, m: 2, s: -2}, plus nonlinear when flagged.""" + exps = {} + for name, args in re.findall(r"\.(\w+)\(([^()]*)\)", chain): + if name == "nonlinear": + exps["nonlinear"] = True + else: + exps[_SETTER_TO_KEY[name]] = int(args) + return exps + + +def parse_units(units_h: str, units_cc: str) -> dict[str, dict]: + si = {} + for entry in macro_entries(units_cc, "LIST_OF_SI_CONVERSIONS"): + cname, rest = entry.split(",", 1) + scale_text, chain = rest.split("SIExp()", 1) + scale_text = re.sub(r"/\*.*?\*/", "", scale_text).strip().rstrip(",").strip() + si[cname.strip()] = {"scale": eval_scale(scale_text), **si_exponents(chain)} + + units = {} + for entry in macro_entries(units_h, "LIST_OF_UNITS"): + cname, lcname, hrname, quantity, _explanation = [p.strip() for p in entry.split(",", 4)] + unit = {"symbol": lcname, "display": hrname.strip('"'), "quantity": quantity} + if cname in si: + unit["si"] = si[cname] + units[unit_name(cname)] = unit + return units + + +class Hex(int): + """Written as 0x.., which YAML reads back as the same int.""" + + +def parse_vif_ranges(dvparser_h: str) -> dict[str, dict]: + result = {} + for entry in macro_entries(dvparser_h, "LIST_OF_VIF_RANGES"): + name, low, high, _quantity, unit = [p.strip() for p in entry.split(",")] + result[name] = {"from": Hex(low, 0), "to": Hex(high, 0), "unit": unit_name(unit.removeprefix("Unit::"))} + return result + + +def parse_vif_combinables(dvparser_h: str) -> dict[str, dict]: + result = {} + for entry in macro_entries(dvparser_h, "LIST_OF_VIF_COMBINABLES"): + name, low, high = [p.strip() for p in entry.split(",")] + result[name] = {"from": Hex(low, 0), "to": Hex(high, 0)} + return result + + +class _Dumper(yaml.SafeDumper): + pass + + +_Dumper.add_representer(Hex, lambda dumper, v: dumper.represent_scalar("tag:yaml.org,2002:int", f"0x{v:02x}")) + + +def write_tables(ref: str, units_h: str, units_cc: str, dvparser_h: str) -> dict[str, int]: + TABLES_DIR.mkdir(exist_ok=True) + tables = { + "quantities": parse_quantities(units_h), + "units": parse_units(units_h, units_cc), + "vif_ranges": parse_vif_ranges(dvparser_h), + "vif_combinables": parse_vif_combinables(dvparser_h), + } + header = f"# Generated by scripts/wmbus_common/fetch_upstream.py from wmbusmeters {ref}; do not edit.\n" + for name, data in tables.items(): + body = yaml.dump(data, Dumper=_Dumper, sort_keys=False, allow_unicode=True) + (TABLES_DIR / f"{name}.yaml").write_text(header + body, encoding="utf-8") + return {name: len(data) for name, data in tables.items()} + + +@contextlib.contextmanager +def upstream(): + """A throwaway blobless clone: ~2.5 MB and a second, and never a stale ref.""" + with tempfile.TemporaryDirectory() as tmp: + repo = Path(tmp) / "wmbusmeters.git" + subprocess.run( + ["git", "clone", "--quiet", "--bare", "--filter=blob:none", UPSTREAM_URL, str(repo)], + check=True, + ) + + def git(*args: str, binary: bool = False, quiet: bool = False): + done = subprocess.run( + ["git", "-C", str(repo), *args], + check=True, + stdout=subprocess.PIPE, + stderr=subprocess.DEVNULL if quiet else None, + ) + return done.stdout if binary else done.stdout.decode().strip() + + yield git + + +def describe(git, ref: str) -> str: + """What a ref resolves to: 3.0.0, or 3.0.0-106-gce0ced0 past that tag.""" + try: + return git("describe", "--tags", ref, quiet=True) + except subprocess.CalledProcessError: + sys.exit(f"no such ref in wmbusmeters: {ref}") + + +def show_diff(git, ref: str, paths: list[str]) -> None: + old = TAG_FILE.read_text().strip() + new = describe(git, ref) + if old == new: + print(f"wmbusmeters {new}: already the recorded ref") + return + print(f"wmbusmeters {old} -> {new}, over {' '.join(paths)}\n") + for command in (("log", "--oneline", "--no-decorate"), ("diff",)): + # Going backwards to an older ref leaves the log empty. + if output := git(*command, f"{old}..{new}", "--", *paths): + print(output + "\n") + + +def fetch(git, ref: str) -> None: + new = describe(git, ref) + for old in DRIVERS_DIR.glob("*.xmq"): + old.unlink() + if LIBRARY_FILE.exists(): + LIBRARY_FILE.unlink() + drivers = 0 + archive = tarfile.open(fileobj=io.BytesIO(git("archive", new, "--", "drivers", binary=True))) + for member in archive.getmembers(): + if not member.isfile() or not member.name.endswith(".xmq"): + continue + content = archive.extractfile(member).read() + if member.name == "drivers/library.xmq": + LIBRARY_FILE.write_bytes(content) + elif member.name.startswith("drivers/src/"): + text = content.decode("utf-8", errors="replace") + # Skip drivers deprecated at the driver level (before fields {) + fields_pos = text.find("fields {") + head = text[:fields_pos] if fields_pos > 0 else text + if re.search(r"^\s*deprecated_by\s*=", head, re.MULTILINE): + continue + (DRIVERS_DIR / Path(member.name).name).write_bytes(content) + drivers += 1 + + def text(relpath: str) -> str: + return git("show", f"{new}:{relpath}") + + counts = write_tables(new, text("src/units.h"), text("src/units.cc"), text("src/dvparser.h")) + TAG_FILE.write_text(new + "\n") + print(f"wmbusmeters {new}: {drivers} drivers, " + ", ".join(f"{n} {k}" for k, n in counts.items())) + + +def main() -> None: + parser = argparse.ArgumentParser(description=__doc__.split("\n\n")[0]) + parser.add_argument("ref", help="tag, branch or commit of wmbusmeters") + parser.add_argument( + "--diff", + action="store_true", + help="print upstream's history and diff from the recorded ref instead of fetching", + ) + parser.add_argument( + "paths", nargs="*", help=f"what --diff covers (default: {' '.join(DIFF_PATHS)})" + ) + args = parser.parse_args() + if args.paths and not args.diff: + parser.error("paths only mean something with --diff") + + with upstream() as git: + if args.diff: + show_diff(git, args.ref, args.paths or list(DIFF_PATHS)) + else: + fetch(git, args.ref) + + +if __name__ == "__main__": + main() diff --git a/scripts/wmbus_common/pull_wmbusmeters.py b/scripts/wmbus_common/pull_wmbusmeters.py deleted file mode 100755 index 0227bd02..00000000 --- a/scripts/wmbus_common/pull_wmbusmeters.py +++ /dev/null @@ -1,40 +0,0 @@ -#!/usr/bin/env python3 - -import argparse -import subprocess -import sys -from pathlib import Path - -# Resolve project root (parent of script directory) once -parent_dir = Path(__file__).resolve().parents[2] - - -def run(cmd): - result = subprocess.run(cmd, shell=True, cwd=parent_dir) - if result.returncode != 0: - sys.exit(result.returncode) - - -def main(): - parser = argparse.ArgumentParser( - description="Pull a subtree from wmbusmeters/wmbusmeters repo." - ) - parser.add_argument("tag", help="Tag name to pull from wmbusmeters/wmbusmeters") - args = parser.parse_args() - - remote_url = "https://github.com/wmbusmeters/wmbusmeters" - prefix = "components/wmbus_common" - - # Fetch the tag directly without adding a remote - run(f"git fetch {remote_url} tag {args.tag}") - - # Pull subtree, squash into one commit - run(f"git subtree pull --prefix={prefix} {remote_url} {args.tag} --squash") - - # Create or overwrite a hidden file in the target directory with the tag name - tag_file = parent_dir / prefix / ".wmbusmeters_tag" - tag_file.write_text(args.tag) - - -if __name__ == "__main__": - main() diff --git a/tests/wmbus/host/components/wmbus_test_runner/__init__.py b/tests/wmbus/host/components/wmbus_test_runner/__init__.py new file mode 100644 index 00000000..85f2db66 --- /dev/null +++ b/tests/wmbus/host/components/wmbus_test_runner/__init__.py @@ -0,0 +1,18 @@ +"""Test-only; lives under tests/ so it never reaches users.""" + +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.const import CONF_ID + +CODEOWNERS = ["@kubasaw"] +DEPENDENCIES = ["wmbus_common"] + +wmbus_test_runner_ns = cg.esphome_ns.namespace("wmbus_test_runner") +TestRunner = wmbus_test_runner_ns.class_("TestRunner", cg.Component) + +CONFIG_SCHEMA = cv.Schema({cv.GenerateID(): cv.declare_id(TestRunner)}) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) diff --git a/tests/wmbus/host/components/wmbus_test_runner/wmbus_test_runner.cpp b/tests/wmbus/host/components/wmbus_test_runner/wmbus_test_runner.cpp new file mode 100644 index 00000000..2693ba48 --- /dev/null +++ b/tests/wmbus/host/components/wmbus_test_runner/wmbus_test_runner.cpp @@ -0,0 +1,122 @@ +#include "wmbus_test_runner.h" + +#include "esphome/components/wmbus_common/meter.h" + +#include +#include +#include +#include +#include +#include +#include + +namespace esphome { +namespace wmbus_test_runner { + +// Results share stdout with ESPHome's logs. +static const char *RESULT_PREFIX = "@@WMBUS@@ "; +static const char *ERROR_PREFIX = "@@WMBUS-ERR@@ "; + +namespace { + +const wmbus::DriverSpec *find_driver(const std::string &name) { + for (const wmbus::DriverSpec *spec : wmbus::registered_drivers) { + if (name == spec->name) + return spec; + } + return nullptr; +} + +std::vector split(const std::string &line, char sep) { + std::vector parts; + size_t start = 0; + while (true) { + size_t at = line.find(sep, start); + if (at == std::string::npos) { + parts.push_back(line.substr(start)); + return parts; + } + parts.push_back(line.substr(start, at - start)); + start = at + 1; + } +} + +// driveridkeytelegram[,telegram...]; key "NOKEY" means unencrypted, as upstream writes it. +void run_case(const std::string &line) { + std::vector args = split(line, '\t'); + if (args.size() != 4) { + printf("%smalformed case\n", ERROR_PREFIX); + return; + } + + const std::string &driver = args[0]; + const std::string &id = args[1]; + + std::optional> key; + if (args[2] != "NOKEY") { + key.emplace(); + if (args[2].size() != 2 * key->size() || !esphome::parse_hex(args[2], key->data(), key->size())) { + printf("%sbad key %s\n", ERROR_PREFIX, args[2].c_str()); + return; + } + } + + // "auto" leaves the spec null, as `type: auto` does. + const wmbus::DriverSpec *spec = nullptr; + if (driver != "auto") { + spec = find_driver(driver); + if (spec == nullptr) { + printf("%sunknown driver %s\n", ERROR_PREFIX, driver.c_str()); + return; + } + } + + wmbus::MeterState meter(spec, (uint32_t) strtoul(id.c_str(), nullptr, 16), key); + bool any_match = false; + for (const std::string &hex : split(args[3], ',')) { + if (hex.empty()) + continue; + + std::vector frame; + if (!esphome::parse_hex(hex, frame, hex.size() / 2)) { + printf("%sbad hex in telegram\n", ERROR_PREFIX); + return; + } + + if (meter.handle_telegram(frame) != wmbus::Result::NotForThisMeter) + any_match = true; + } + + if (!any_match) { + printf("%sno telegram matched meter id %s\n", ERROR_PREFIX, id.c_str()); + return; + } + + // Only `auto` gets here without a driver: no detect{} in the registry matched. + if (meter.spec == nullptr) { + printf("%s%s\n", ERROR_PREFIX, meter.last_error.c_str()); + return; + } + + printf("%s%s\t%s\n", RESULT_PREFIX, meter.spec->name, meter.to_json().c_str()); +} + +} // namespace + +void TestRunner::setup() { + setenv("TZ", "UTC", 1); + tzset(); + std::string line; + while (std::getline(std::cin, line)) { + if (!line.empty() && line.back() == '\r') + line.pop_back(); + if (line.empty()) + continue; + run_case(line); + } + fflush(stdout); + std::exit(0); +} + +} // namespace wmbus_test_runner +} // namespace esphome diff --git a/tests/wmbus/host/components/wmbus_test_runner/wmbus_test_runner.h b/tests/wmbus/host/components/wmbus_test_runner/wmbus_test_runner.h new file mode 100644 index 00000000..5e9f76b3 --- /dev/null +++ b/tests/wmbus/host/components/wmbus_test_runner/wmbus_test_runner.h @@ -0,0 +1,16 @@ +#pragma once + +#include "esphome/core/component.h" + +namespace esphome { +namespace wmbus_test_runner { + +// Decodes the vectors from stdin in setup() and exits. +class TestRunner : public Component { + public: + void setup() override; + float get_setup_priority() const override { return setup_priority::LATE; } +}; + +} // namespace wmbus_test_runner +} // namespace esphome diff --git a/tests/wmbus/host/test.yaml b/tests/wmbus/host/test.yaml new file mode 100644 index 00000000..4727b65b --- /dev/null +++ b/tests/wmbus/host/test.yaml @@ -0,0 +1,14 @@ +esphome: + name: wmbus-host-test + +host: + +external_components: + - source: ../../../components + - source: components + +wmbus_common: + # The runner picks drivers by name at run time. + drivers: all + +wmbus_test_runner: diff --git a/tests/wmbus/host/test_telegrams.py b/tests/wmbus/host/test_telegrams.py new file mode 100644 index 00000000..71fb20d6 --- /dev/null +++ b/tests/wmbus/host/test_telegrams.py @@ -0,0 +1,343 @@ +"""Replays the XMQ drivers' tests{} telegrams through the host build against upstream's JSON. + +Needs esphome importable and wmbus-test-runner, the compiled test.yaml, on PATH: run it from the esphome venv. +""" + +import json +import os +import re +import subprocess +import sys +from dataclasses import dataclass +from functools import cache +from pathlib import Path + +import pytest + +HERE = Path(__file__).resolve().parent +REPO_ROOT = HERE.parents[2] +COMPONENTS_DIR = REPO_ROOT / "components" +DRIVERS_DIR = COMPONENTS_DIR / "wmbus_common" / "drivers" / "src" + +sys.path.insert(0, str(COMPONENTS_DIR)) + +# The runner answers exactly one line per case, in input order. +RESULT_PREFIX = "@@WMBUS@@ " +ERROR_PREFIX = "@@WMBUS-ERR@@ " + +# What upstream's JSON carries that the runner does not print. +ENVELOPE_KEYS = {"_", "driver", "id", "name", "timestamp", "rssi_dbm"} + +# Each traced to upstream's own code; the xfail is strict, so a fix must delete its line. +KNOWN_DECODE_DIFFS = { + "actislink-ANYID": "addressed as ANYID and encrypted with DES", + "hcae2-54423117#4": "delta_14_half_months_ago_hca untraced", + "kamwater-52525252#2": "synthetic test telegram has wrong length and KAM manufacturer instead of KAW", + "kamwater-52525252#3": "synthetic test telegram has wrong length and KAM manufacturer instead of KAW", +} + +# Overlapping mvt lines: `type: auto` picks whichever driver the registry holds first. +KNOWN_DETECTION_DIFFS = { + "actislink-ANYID": "addressed as ANYID and encrypted with DES", + "kamwater-52525252#2": "synthetic test telegram has wrong length and KAM manufacturer instead of KAW", + "kamwater-52525252#3": "synthetic test telegram has wrong length and KAM manufacturer instead of KAW", + "mkradio4a-01770002": "same mvt line as weh_07", +} + +# Upstream separates frame layers for readability; stripping here keeps the decoder's hex parsing strict. +SEPARATORS = str.maketrans("", "", " |#_-") + + +@dataclass +class Case: + id: str + driver: str + meter_id: str + key: str + telegrams: list[str] + expected: dict + comment: str + + +def split_telegrams(value: str) -> list[str]: + """Upstream separates a test's frames by commas or by newlines.""" + parts = (part.translate(SEPARATORS) for part in re.split(r"[,\n]", value)) + return [part for part in parts if part] + + +def as_wireless(frame: bytes) -> bytes: + """A wired M-Bus frame as a radio would hear it; every wired test frame has a long TPL header (CI 72).""" + tpl = frame[6:-2] + meter_id, mfct, version_type = tpl[1:5], tpl[5:7], tpl[7:9] + body = bytes([0x44]) + mfct + meter_id + version_type + tpl + return bytes([len(body)]) + body + + +# The bytes an ELL header takes after its CI. +ELL_LENGTH = {0x8C: 2, 0x8D: 8, 0x8E: 10, 0x8F: 16} + + +# Diehl's manufacturer codes, as in quirks.cpp. +DIEHL = {"DME", "EWT", "HYD", "SAP", "SPL"} + + +def diehl_real_data_log(frame: bytes) -> bool: + """A decrypted Diehl LFSR real-data log (CI 7A, mode >= 16): its clear payload sums to the TPL config's check.""" + from wmbus_common.drivers.loader.xmq_loader import manufacturer_code + + return ( + len(frame) > 15 + and (frame[3] << 8 | frame[2]) in {manufacturer_code(m) for m in DIEHL} + and frame[1] in (0x44, 0x46) + and frame[10] == 0x7A + and frame[14] & 0x10 + and sum(frame[15:]) & 0xEF == frame[14] & 0xEF + ) + + +def crc16_en13757(data: bytes) -> int: + crc = 0 + for byte in data: + for _ in range(8): + if ((crc >> 8) & 0x80) ^ (byte & 0x80): + crc = ((crc << 1) ^ 0x3D65) & 0xFFFF + else: + crc = (crc << 1) & 0xFFFF + byte = (byte << 1) & 0xFF + return (~crc) & 0xFFFF + + +def declassified(frame: bytes) -> bytes: + """Upstream's already-decrypted logs whose headers still announce encryption, as the plain frames they are. + + Drops the security mode and what only an encrypted TPL has (2F2F, mode 7's extra byte, Diehl's + reordered address). A frame whose ELL CRC fails is really encrypted and stays. + """ + if diehl_real_data_log(frame): + # Real data puts the DLL's version and type in front of the id. + return frame[:4] + frame[6:10] + frame[4:6] + frame[10:14] + bytes([frame[14] & 0xE0]) + frame[15:] + + out = bytearray(frame) + pos = 10 + ell_crc = None + if pos < len(out) and out[pos] in ELL_LENGTH: + ci = out[pos] + if ci in (0x8D, 0x8F): + session = pos + (3 if ci == 0x8D else 11) + ell_crc = session + 4 + if int.from_bytes(out[ell_crc : ell_crc + 2], "little") != crc16_en13757(out[ell_crc + 2 :]): + return frame + out[session + 3] &= 0x1F # the mode, in the session number's top three bits + pos += 1 + ELL_LENGTH[ci] + if pos < len(out) and out[pos] == 0x81: # NWL + pos += 2 + if pos < len(out) and out[pos] == 0x90: # AFL + pos += 2 + out[pos + 1] + if pos < len(out) and out[pos] in (0x72, 0x7A): + cfg = pos + (11 if out[pos] == 0x72 else 3) + mode = out[cfg + 1] & 0x1F + records = cfg + 2 + (mode == 7) + if mode in (5, 7) and out[records : records + 2] == b"\x2f\x2f": + out[cfg + 1] &= 0xE0 + del out[records : records + 2] + if mode == 7: + del out[cfg + 2] + out[0] -= 2 + (mode == 7) + if ell_crc is not None: + out[ell_crc : ell_crc + 2] = crc16_en13757(out[ell_crc + 2 :]).to_bytes(2, "little") + return bytes(out) + + +def as_heard(telegram: str) -> str: + """The telegram as a radio delivers it. Whole bytes only: one upstream telegram ends in half a byte.""" + frame = bytes.fromhex(telegram[: len(telegram) // 2 * 2]) + if frame[0] == 0x68 and frame[3] == 0x68 and frame[1] == frame[2]: + frame = as_wireless(frame) + return declassified(frame).hex().upper() + + +def as_list(value) -> list: + if value is None: + return [] + return value if isinstance(value, list) else [value] + + +def collect_cases() -> list[Case]: + from wmbus_common.drivers.loader.xmq import parse_xmq + + cases = [] + seen: dict[str, int] = {} + for path in sorted(DRIVERS_DIR.glob("*.xmq")): + definition = parse_xmq(path.read_text())["driver"] + for test in as_list((definition.get("tests") or {}).get("test")): + args = test.get("args", "").split() + if len(args) != 4 or "telegram" not in test or "json" not in test: + continue + + _name, driver, meter_id, key = args + try: + expected = json.loads(test["json"]) + except json.JSONDecodeError: + continue + + name = f"{driver}-{meter_id}" + seen[name] = seen.get(name, 0) + 1 + if seen[name] > 1: + name = f"{name}#{seen[name]}" + + cases.append( + Case( + id=name, + driver=driver, + meter_id=meter_id, + key=key, + telegrams=[as_heard(t) for t in split_telegrams(test["telegram"])], + expected=expected, + comment=" ".join(as_list(test.get("comment"))), + ) + ) + return cases + + +def driver_names() -> dict[str, str]: + """Every driver name this build knows, aliases included, mapped to the driver it resolves to.""" + from wmbus_common.drivers.loader.driver_manager import DriverManager as Manager + + manager = Manager() + manager.load_drivers() + return {name: manager.request_driver(name).name for name in manager.available_drivers} + + +NAMES = driver_names() +CASES = collect_cases() + + +def parameters(known_diffs: dict[str, str]): + """Skips cases of drivers upstream still ships as C++ (qwater in qwaterv2.xmq).""" + params = [] + for case in CASES: + marks = [] + if case.driver not in NAMES: + marks.append(pytest.mark.skip(reason=f"no driver {case.driver} in this build")) + elif case.id in known_diffs: + marks.append(pytest.mark.xfail(reason=known_diffs[case.id], strict=True)) + params.append(pytest.param(case, marks=marks, id=case.id)) + return params + + +def run_batch(as_auto: bool) -> dict[str, str]: + """as_auto sends every case to a meter without a driver, as `type: auto` leaves one.""" + cases = [c for c in CASES if c.driver in NAMES] + stdin = "".join( + "\t".join( + ["auto" if as_auto else NAMES[c.driver], c.meter_id, c.key, ",".join(c.telegrams)] + ) + + "\n" + for c in cases + ) + result = subprocess.run( + ["wmbus-test-runner"], + input=stdin, + capture_output=True, + text=True, + env={**os.environ, "TZ": "UTC"}, + ) + + answers = [ + line + for line in result.stdout.splitlines() + if line.startswith(RESULT_PREFIX) or line.startswith(ERROR_PREFIX) + ] + if len(answers) != len(cases): + pytest.fail( + f"runner answered {len(answers)} of {len(cases)} cases " + f"(exit {result.returncode})\n{result.stderr.strip()[:2000]}" + ) + return {case.id: answer for case, answer in zip(cases, answers)} + + +@pytest.fixture(scope="session") +def decoded() -> dict[str, str]: + return run_batch(as_auto=False) + + +@pytest.fixture(scope="session") +def detected() -> dict[str, str]: + return run_batch(as_auto=True) + + +def expected_fields(expected: dict) -> dict: + """Upstream's JSON without the envelope and the synthetic "_deprecated_by" warnings.""" + return {k: v for k, v in expected.items() if k not in ENVELOPE_KEYS and not k.endswith("_deprecated_by")} + + +@cache +def hidden_fields(driver: str) -> set[str]: + """The names upstream leaves out of its JSON: those whose every definition is HIDE.""" + from wmbus_common.drivers.loader.xmq import parse_xmq + from wmbus_common.drivers.loader.xmq_loader import SCHEMA, Attribute, concrete_fields + + path = DRIVERS_DIR / f"{driver}.xmq" + hidden: dict[str, bool] = {} + for f in concrete_fields(SCHEMA(parse_xmq(path.read_text()))["driver"]): + hidden[f.json_name] = hidden.get(f.json_name, True) and Attribute.Hide in f.definition.attributes + return {name for name, is_hidden in hidden.items() if is_hidden} + + +def upstream_view(driver: str, fields: dict) -> dict: + """The runner's fields as upstream prints them: HIDE ones dropped.""" + return { + name: value + for name, value in fields.items() + if name not in hidden_fields(driver) and name not in ENVELOPE_KEYS + } + + +def same_value(actual, expected) -> bool: + """Upstream prints six decimals, ArduinoJson nine significant digits.""" + if isinstance(actual, (int, float)) and isinstance(expected, (int, float)): + return actual == pytest.approx(expected, rel=1e-6, abs=1e-6) + return actual == expected + + +def answer_of(line: str) -> tuple[str, dict]: + assert not line.startswith(ERROR_PREFIX), line[len(ERROR_PREFIX) :] + driver, _, fields = line[len(RESULT_PREFIX) :].partition("\t") + try: + return driver, json.loads(fields) + except json.JSONDecodeError: + pytest.fail(f"not JSON: {fields[:200]}") + + +@pytest.mark.parametrize("case", parameters(KNOWN_DECODE_DIFFS)) +def test_decode(case: Case, decoded: dict[str, str]) -> None: + actual, expected = upstream_view(*answer_of(decoded[case.id])), expected_fields(case.expected) + + missing = {k: v for k, v in expected.items() if k not in actual} + extra = {k: v for k, v in actual.items() if k not in expected} + wrong = { + k: f"{actual[k]!r} != {v!r}" + for k, v in expected.items() + if k in actual and not same_value(actual[k], v) + } + if not (missing or extra or wrong): + return + + parts = [] + if wrong: + parts.append(f"wrong={wrong}") + if missing: + parts.append(f"missing={missing}") + if extra: + parts.append(f"extra={extra}") + if case.comment: + parts.append(case.comment) + pytest.fail("; ".join(parts)) + + +@pytest.mark.parametrize("case", parameters(KNOWN_DETECTION_DIFFS)) +def test_detection(case: Case, detected: dict[str, str]) -> None: + driver, _ = answer_of(detected[case.id]) + assert NAMES.get(driver, driver) == NAMES.get(case.driver, case.driver), ( + f"detected as {driver!r}" + ) diff --git a/tests/wmbus/test.yaml b/tests/wmbus/test.yaml new file mode 100644 index 00000000..d874202f --- /dev/null +++ b/tests/wmbus/test.yaml @@ -0,0 +1,114 @@ +# Uses every knob of the wmbus_* components: no other configuration catches a codegen break. +# One driver per generator path: all of them overflow the default 4 MB partition. + +esphome: + name: test-wmbus + +esp32: + board: esp32dev + framework: + type: esp-idf + +external_components: + - source: ../../components + +logger: + +wifi: + ssid: TestSSID + password: TestPass + +mqtt: + broker: 192.168.1.1 + port: 1883 + +spi: + clk_pin: GPIO0 + mosi_pin: GPIO1 + miso_pin: GPIO2 + +socket_transmitter: + - id: tcp_transmitter + ip_address: 192.168.1.1 + port: 12345 + protocol: TCP + - id: udp_transmitter + ip_address: 192.168.1.1 + port: 12346 + protocol: UDP + +wmbus_common: + drivers: + - apator162 # ixml grammar + - kamwater # compact frames, lookup + - rfmamb # TPL status bits + - izarv2 # calculate, transform_payload, diehl_prios + - iem3000 # add_combinable_raw + - uiws # null_value + - nemo # link_mode + - name: esyswm # readable_string, and the per-driver field list + fields: + - total_energy_consumption_kwh + +wmbus_radio: + radio_type: SX1276 + cs_pin: GPIO3 + reset_pin: GPIO4 + irq_pin: GPIO5 + on_packet: + - then: + - logger.log: + format: "packet of %d bytes" + args: ["(int) packet->get_raw_data().size()"] + on_frame: + - then: + - wmbus_radio.send_frame_with_socket: + id: tcp_transmitter + format: rtlwmbus + - mark_as_handled: true + then: + - wmbus_radio.send_frame_with_socket: + id: udp_transmitter + format: raw + - wmbus_radio.send_frame_with_socket: + id: udp_transmitter + format: hex + +wmbus_meter: + - id: test_meter + meter_id: "01234567" + type: apator162 + key: "00000000000000000000000000000000" + on_telegram: + - wmbus_meter.send_telegram_with_mqtt: + topic: test_topic + - socket_transmitter.send: + id: tcp_transmitter + data: !lambda return meter->as_json(); + - id: auto_meter + meter_id: "89abcdef" + type: auto + +sensor: + - platform: wmbus_meter + id: test_sensor + parent_id: test_meter + field: total_m3 + name: "Test Water Consumption" + - platform: wmbus_meter + id: test_timestamp_sensor + parent_id: test_meter + field: timestamp + name: "Test Last Update" + device_class: timestamp + +text_sensor: + - platform: wmbus_meter + id: test_text_sensor + parent_id: test_meter + field: timestamp + name: "Test Meter Timestamp" + - platform: wmbus_meter + parent_id: test_meter + field: media + name: "Test Meter Media"