diff --git a/CODEOWNERS b/CODEOWNERS index cd828395a6..b5603ea1c0 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -37,6 +37,7 @@ esphome/components/debug/* @OttoWinter esphome/components/dfplayer/* @glmnet esphome/components/dht/* @OttoWinter esphome/components/ds1307/* @badbadc0ffee +esphome/components/dsmr/* @glmnet @zuidwijk esphome/components/esp32_ble/* @jesserockz esphome/components/esp32_ble_server/* @jesserockz esphome/components/esp32_improv/* @jesserockz diff --git a/esphome/components/dsmr/__init__.py b/esphome/components/dsmr/__init__.py new file mode 100644 index 0000000000..df11a14ee8 --- /dev/null +++ b/esphome/components/dsmr/__init__.py @@ -0,0 +1,59 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import uart +from esphome.const import ( + CONF_ID, + CONF_UART_ID, +) + +CODEOWNERS = ["@glmnet", "@zuidwijk"] + +DEPENDENCIES = ["uart"] +AUTO_LOAD = ["sensor", "text_sensor"] + +CONF_DSMR_ID = "dsmr_id" +CONF_DECRYPTION_KEY = "decryption_key" + +# Hack to prevent compile error due to ambiguity with lib namespace +dsmr_ns = cg.esphome_ns.namespace("esphome::dsmr") +Dsmr = dsmr_ns.class_("Dsmr", cg.Component, uart.UARTDevice) + + +def _validate_key(value): + value = cv.string_strict(value) + parts = [value[i : i + 2] for i in range(0, len(value), 2)] + if len(parts) != 16: + raise cv.Invalid("Decryption key must consist of 16 hexadecimal numbers") + parts_int = [] + if any(len(part) != 2 for part in parts): + raise cv.Invalid("Decryption key must be format XX") + for part in parts: + try: + parts_int.append(int(part, 16)) + except ValueError: + # pylint: disable=raise-missing-from + raise cv.Invalid("Decryption key must be hex values from 00 to FF") + + return "".join(f"{part:02X}" for part in parts_int) + + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(): cv.declare_id(Dsmr), + cv.Optional(CONF_DECRYPTION_KEY): _validate_key, + } +).extend(uart.UART_DEVICE_SCHEMA) + + +async def to_code(config): + uart_component = await cg.get_variable(config[CONF_UART_ID]) + var = cg.new_Pvariable(config[CONF_ID], uart_component) + if CONF_DECRYPTION_KEY in config: + cg.add(var.set_decryption_key(config[CONF_DECRYPTION_KEY])) + await cg.register_component(var, config) + + # DSMR Parser + cg.add_library("glmnet/Dsmr", "0.3") + + # Crypto + cg.add_library("rweather/Crypto", "0.2.0") diff --git a/esphome/components/dsmr/dsmr.cpp b/esphome/components/dsmr/dsmr.cpp new file mode 100644 index 0000000000..9bce7a382f --- /dev/null +++ b/esphome/components/dsmr/dsmr.cpp @@ -0,0 +1,182 @@ +#include "dsmr.h" +#include "esphome/core/log.h" + +#include +#include +#include + +namespace esphome { +namespace dsmr { + +static const char *const TAG = "dsmr"; + +void Dsmr::loop() { + if (this->decryption_key_.empty()) + this->receive_telegram_(); + else + this->receive_encrypted_(); +} + +void Dsmr::receive_telegram_() { + while (available()) { + const char c = read(); + + if (c == '/') { // header: forward slash + ESP_LOGV(TAG, "Header found"); + header_found_ = true; + footer_found_ = false; + telegram_len_ = 0; + } + + if (!header_found_) + continue; + if (telegram_len_ >= MAX_TELEGRAM_LENGTH) { // Buffer overflow + header_found_ = false; + footer_found_ = false; + ESP_LOGE(TAG, "Error: Message larger than buffer"); + return; + } + + telegram_[telegram_len_] = c; + telegram_len_++; + if (c == '!') { // footer: exclamation mark + ESP_LOGV(TAG, "Footer found"); + footer_found_ = true; + } else { + if (footer_found_ && c == 10) { // last \n after footer + header_found_ = false; + // Parse message + if (parse_telegram()) + return; + } + } + } +} + +void Dsmr::receive_encrypted_() { + // Encrypted buffer + uint8_t buffer[MAX_TELEGRAM_LENGTH]; + size_t buffer_length = 0; + + size_t packet_size = 0; + while (available()) { + const char c = read(); + + if (!header_found_) { + if ((uint8_t) c == 0xdb) { + ESP_LOGV(TAG, "Start byte 0xDB found"); + header_found_ = true; + } + } + + // Sanity check + if (!header_found_ || buffer_length >= MAX_TELEGRAM_LENGTH) { + if (buffer_length == 0) { + ESP_LOGE(TAG, "First byte of encrypted telegram should be 0xDB, aborting."); + } else { + ESP_LOGW(TAG, "Unexpected data"); + } + this->status_momentary_warning("unexpected_data"); + this->flush(); + while (available()) + read(); + return; + } + + buffer[buffer_length++] = c; + + if (packet_size == 0 && buffer_length > 20) { + // Complete header + a few bytes of data + packet_size = buffer[11] << 8 | buffer[12]; + } + if (buffer_length == packet_size + 13 && packet_size > 0) { + ESP_LOGV(TAG, "Encrypted data: %d bytes", buffer_length); + + GCM *gcmaes128{new GCM()}; + gcmaes128->setKey(this->decryption_key_.data(), gcmaes128->keySize()); + // the iv is 8 bytes of the system title + 4 bytes frame counter + // system title is at byte 2 and frame counter at byte 15 + for (int i = 10; i < 14; i++) + buffer[i] = buffer[i + 4]; + constexpr uint16_t iv_size{12}; + gcmaes128->setIV(&buffer[2], iv_size); + gcmaes128->decrypt(reinterpret_cast(this->telegram_), + // the ciphertext start at byte 18 + &buffer[18], + // cipher size + buffer_length - 17); + delete gcmaes128; + + telegram_len_ = strnlen(this->telegram_, sizeof(this->telegram_)); + ESP_LOGV(TAG, "Decrypted data length: %d", telegram_len_); + ESP_LOGVV(TAG, "Decrypted data %s", this->telegram_); + + parse_telegram(); + telegram_len_ = 0; + return; + } + + if (!available()) { + // baud rate is 115200 for encrypted data, this means a few byte should arrive every time + // program runs faster than buffer loading then available() might return false in the middle + delay(4); // Wait for data + } + } + if (buffer_length > 0) + ESP_LOGW(TAG, "Timeout while waiting for encrypted data or invalid data received."); +} + +bool Dsmr::parse_telegram() { + MyData data; + ESP_LOGV(TAG, "Trying to parse"); + ::dsmr::ParseResult res = + ::dsmr::P1Parser::parse(&data, telegram_, telegram_len_, + false); // Parse telegram according to data definition. Ignore unknown values. + if (res.err) { + // Parsing error, show it + auto err_str = res.fullError(telegram_, telegram_ + telegram_len_); + ESP_LOGE(TAG, "%s", err_str.c_str()); + return false; + } else { + this->status_clear_warning(); + publish_sensors(data); + return true; + } +} + +void Dsmr::dump_config() { + ESP_LOGCONFIG(TAG, "dsmr:"); + +#define DSMR_LOG_SENSOR(s) LOG_SENSOR(" ", #s, this->s_##s##_); + DSMR_SENSOR_LIST(DSMR_LOG_SENSOR, ) + +#define DSMR_LOG_TEXT_SENSOR(s) LOG_TEXT_SENSOR(" ", #s, this->s_##s##_); + DSMR_TEXT_SENSOR_LIST(DSMR_LOG_TEXT_SENSOR, ) +} + +void Dsmr::set_decryption_key(const std::string &decryption_key) { + if (decryption_key.length() == 0) { + ESP_LOGI(TAG, "Disabling decryption"); + this->decryption_key_.clear(); + return; + } + + if (decryption_key.length() != 32) { + ESP_LOGE(TAG, "Error, decryption key must be 32 character long."); + return; + } + this->decryption_key_.clear(); + + ESP_LOGI(TAG, "Decryption key is set."); + // Verbose level prints decryption key + ESP_LOGV(TAG, "Using decryption key: %s", decryption_key.c_str()); + + char temp[3] = {0}; + for (int i = 0; i < 16; i++) { + strncpy(temp, &(decryption_key.c_str()[i * 2]), 2); + decryption_key_.push_back(std::strtoul(temp, nullptr, 16)); + } +} + +} // namespace dsmr +} // namespace esphome diff --git a/esphome/components/dsmr/dsmr.h b/esphome/components/dsmr/dsmr.h new file mode 100644 index 0000000000..984f2596db --- /dev/null +++ b/esphome/components/dsmr/dsmr.h @@ -0,0 +1,104 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/text_sensor/text_sensor.h" +#include "esphome/components/uart/uart.h" +#include "esphome/core/log.h" +#include "esphome/core/defines.h" + +// don't include because it puts everything in global namespace +#include +#include + +namespace esphome { +namespace dsmr { + +static constexpr uint32_t MAX_TELEGRAM_LENGTH = 1500; +static constexpr uint32_t POLL_TIMEOUT = 1000; + +using namespace ::dsmr::fields; + +// DSMR_**_LIST generated by ESPHome and written in esphome/core/defines + +#if !defined(DSMR_SENSOR_LIST) && !defined(DSMR_TEXT_SENSOR_LIST) +// Neither set, set it to a dummy value to not break build +#define DSMR_TEXT_SENSOR_LIST(F, SEP) F(identification) +#endif + +#if defined(DSMR_SENSOR_LIST) && defined(DSMR_TEXT_SENSOR_LIST) +#define DSMR_BOTH , +#else +#define DSMR_BOTH +#endif + +#ifndef DSMR_SENSOR_LIST +#define DSMR_SENSOR_LIST(F, SEP) +#endif + +#ifndef DSMR_TEXT_SENSOR_LIST +#define DSMR_TEXT_SENSOR_LIST(F, SEP) +#endif + +#define DSMR_DATA_SENSOR(s) s +#define DSMR_COMMA , + +using MyData = ::dsmr::ParsedData; + +class Dsmr : public Component, public uart::UARTDevice { + public: + Dsmr(uart::UARTComponent *uart) : uart::UARTDevice(uart) {} + + void loop() override; + + bool parse_telegram(); + + void publish_sensors(MyData &data) { +#define DSMR_PUBLISH_SENSOR(s) \ + if (data.s##_present && this->s_##s##_ != nullptr) \ + s_##s##_->publish_state(data.s); + DSMR_SENSOR_LIST(DSMR_PUBLISH_SENSOR, ) + +#define DSMR_PUBLISH_TEXT_SENSOR(s) \ + if (data.s##_present && this->s_##s##_ != nullptr) \ + s_##s##_->publish_state(data.s.c_str()); + DSMR_TEXT_SENSOR_LIST(DSMR_PUBLISH_TEXT_SENSOR, ) + }; + + void dump_config() override; + + void set_decryption_key(const std::string &decryption_key); + +// Sensor setters +#define DSMR_SET_SENSOR(s) \ + void set_##s(sensor::Sensor *sensor) { s_##s##_ = sensor; } + DSMR_SENSOR_LIST(DSMR_SET_SENSOR, ) + +#define DSMR_SET_TEXT_SENSOR(s) \ + void set_##s(text_sensor::TextSensor *sensor) { s_##s##_ = sensor; } + DSMR_TEXT_SENSOR_LIST(DSMR_SET_TEXT_SENSOR, ) + + protected: + void receive_telegram_(); + void receive_encrypted_(); + + // Telegram buffer + char telegram_[MAX_TELEGRAM_LENGTH]; + int telegram_len_{0}; + + // Serial parser + bool header_found_{false}; + bool footer_found_{false}; + +// Sensor member pointers +#define DSMR_DECLARE_SENSOR(s) sensor::Sensor *s_##s##_{nullptr}; + DSMR_SENSOR_LIST(DSMR_DECLARE_SENSOR, ) + +#define DSMR_DECLARE_TEXT_SENSOR(s) text_sensor::TextSensor *s_##s##_{nullptr}; + DSMR_TEXT_SENSOR_LIST(DSMR_DECLARE_TEXT_SENSOR, ) + + std::vector decryption_key_{}; +}; +} // namespace dsmr +} // namespace esphome diff --git a/esphome/components/dsmr/sensor.py b/esphome/components/dsmr/sensor.py new file mode 100644 index 0000000000..05c568e21a --- /dev/null +++ b/esphome/components/dsmr/sensor.py @@ -0,0 +1,210 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import sensor +from esphome.const import ( + DEVICE_CLASS_CURRENT, + DEVICE_CLASS_EMPTY, + DEVICE_CLASS_ENERGY, + DEVICE_CLASS_POWER, + DEVICE_CLASS_VOLTAGE, + ICON_EMPTY, + LAST_RESET_TYPE_NEVER, + STATE_CLASS_MEASUREMENT, + STATE_CLASS_NONE, + UNIT_AMPERE, + UNIT_EMPTY, + UNIT_VOLT, + UNIT_WATT, +) +from . import Dsmr, CONF_DSMR_ID + +AUTO_LOAD = ["dsmr"] + + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(CONF_DSMR_ID): cv.use_id(Dsmr), + cv.Optional("energy_delivered_lux"): sensor.sensor_schema( + "kWh", + ICON_EMPTY, + 3, + DEVICE_CLASS_ENERGY, + STATE_CLASS_MEASUREMENT, + LAST_RESET_TYPE_NEVER, + ), + cv.Optional("energy_delivered_tariff1"): sensor.sensor_schema( + "kWh", + ICON_EMPTY, + 3, + DEVICE_CLASS_ENERGY, + STATE_CLASS_MEASUREMENT, + LAST_RESET_TYPE_NEVER, + ), + cv.Optional("energy_delivered_tariff2"): sensor.sensor_schema( + "kWh", + ICON_EMPTY, + 3, + DEVICE_CLASS_ENERGY, + STATE_CLASS_MEASUREMENT, + LAST_RESET_TYPE_NEVER, + ), + cv.Optional("energy_returned_lux"): sensor.sensor_schema( + "kWh", + ICON_EMPTY, + 3, + DEVICE_CLASS_ENERGY, + STATE_CLASS_MEASUREMENT, + LAST_RESET_TYPE_NEVER, + ), + cv.Optional("energy_returned_tariff1"): sensor.sensor_schema( + "kWh", + ICON_EMPTY, + 3, + DEVICE_CLASS_ENERGY, + STATE_CLASS_MEASUREMENT, + LAST_RESET_TYPE_NEVER, + ), + cv.Optional("energy_returned_tariff2"): sensor.sensor_schema( + "kWh", + ICON_EMPTY, + 3, + DEVICE_CLASS_ENERGY, + STATE_CLASS_MEASUREMENT, + LAST_RESET_TYPE_NEVER, + ), + cv.Optional("total_imported_energy"): sensor.sensor_schema( + "kvarh", ICON_EMPTY, 3, DEVICE_CLASS_ENERGY, STATE_CLASS_NONE + ), + cv.Optional("total_exported_energy"): sensor.sensor_schema( + "kvarh", ICON_EMPTY, 3, DEVICE_CLASS_ENERGY, STATE_CLASS_NONE + ), + cv.Optional("power_delivered"): sensor.sensor_schema( + UNIT_WATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT + ), + cv.Optional("power_returned"): sensor.sensor_schema( + UNIT_WATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT + ), + cv.Optional("reactive_power_delivered"): sensor.sensor_schema( + "kvar", ICON_EMPTY, 3, DEVICE_CLASS_ENERGY, STATE_CLASS_NONE + ), + cv.Optional("reactive_power_returned"): sensor.sensor_schema( + "kvar", ICON_EMPTY, 3, DEVICE_CLASS_ENERGY, STATE_CLASS_MEASUREMENT + ), + cv.Optional("electricity_threshold"): sensor.sensor_schema( + UNIT_EMPTY, ICON_EMPTY, 3, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE + ), + cv.Optional("electricity_switch_position"): sensor.sensor_schema( + UNIT_EMPTY, ICON_EMPTY, 3, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE + ), + cv.Optional("electricity_failures"): sensor.sensor_schema( + UNIT_EMPTY, ICON_EMPTY, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE + ), + cv.Optional("electricity_long_failures"): sensor.sensor_schema( + UNIT_EMPTY, ICON_EMPTY, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE + ), + cv.Optional("electricity_sags_l1"): sensor.sensor_schema( + UNIT_EMPTY, ICON_EMPTY, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE + ), + cv.Optional("electricity_sags_l2"): sensor.sensor_schema( + UNIT_EMPTY, ICON_EMPTY, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_MEASUREMENT + ), + cv.Optional("electricity_sags_l3"): sensor.sensor_schema( + UNIT_EMPTY, ICON_EMPTY, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE + ), + cv.Optional("electricity_swells_l1"): sensor.sensor_schema( + UNIT_EMPTY, ICON_EMPTY, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_MEASUREMENT + ), + cv.Optional("electricity_swells_l2"): sensor.sensor_schema( + UNIT_EMPTY, ICON_EMPTY, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE + ), + cv.Optional("electricity_swells_l3"): sensor.sensor_schema( + UNIT_EMPTY, ICON_EMPTY, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE + ), + cv.Optional("current_l1"): sensor.sensor_schema( + UNIT_AMPERE, ICON_EMPTY, 1, DEVICE_CLASS_CURRENT, STATE_CLASS_MEASUREMENT + ), + cv.Optional("current_l2"): sensor.sensor_schema( + UNIT_AMPERE, ICON_EMPTY, 1, DEVICE_CLASS_CURRENT, STATE_CLASS_MEASUREMENT + ), + cv.Optional("current_l3"): sensor.sensor_schema( + UNIT_AMPERE, ICON_EMPTY, 1, DEVICE_CLASS_CURRENT, STATE_CLASS_MEASUREMENT + ), + cv.Optional("power_delivered_l1"): sensor.sensor_schema( + UNIT_WATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT + ), + cv.Optional("power_delivered_l2"): sensor.sensor_schema( + UNIT_WATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT + ), + cv.Optional("power_delivered_l3"): sensor.sensor_schema( + UNIT_WATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT + ), + cv.Optional("power_returned_l1"): sensor.sensor_schema( + UNIT_WATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT + ), + cv.Optional("power_returned_l2"): sensor.sensor_schema( + UNIT_WATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT + ), + cv.Optional("power_returned_l3"): sensor.sensor_schema( + UNIT_WATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT + ), + cv.Optional("reactive_power_delivered_l1"): sensor.sensor_schema( + UNIT_WATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT + ), + cv.Optional("reactive_power_delivered_l2"): sensor.sensor_schema( + UNIT_WATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT + ), + cv.Optional("reactive_power_delivered_l3"): sensor.sensor_schema( + UNIT_WATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT + ), + cv.Optional("reactive_power_returned_l1"): sensor.sensor_schema( + UNIT_WATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT + ), + cv.Optional("reactive_power_returned_l2"): sensor.sensor_schema( + UNIT_WATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT + ), + cv.Optional("reactive_power_returned_l3"): sensor.sensor_schema( + UNIT_WATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT + ), + cv.Optional("voltage_l1"): sensor.sensor_schema( + UNIT_VOLT, ICON_EMPTY, 1, DEVICE_CLASS_VOLTAGE, STATE_CLASS_NONE + ), + cv.Optional("voltage_l2"): sensor.sensor_schema( + UNIT_VOLT, ICON_EMPTY, 1, DEVICE_CLASS_VOLTAGE, STATE_CLASS_NONE + ), + cv.Optional("voltage_l3"): sensor.sensor_schema( + UNIT_VOLT, ICON_EMPTY, 1, DEVICE_CLASS_VOLTAGE, STATE_CLASS_NONE + ), + cv.Optional("gas_delivered"): sensor.sensor_schema( + "m³", + ICON_EMPTY, + 3, + DEVICE_CLASS_EMPTY, + STATE_CLASS_MEASUREMENT, + LAST_RESET_TYPE_NEVER, + ), + cv.Optional("gas_delivered_be"): sensor.sensor_schema( + "m³", + ICON_EMPTY, + 3, + DEVICE_CLASS_EMPTY, + STATE_CLASS_MEASUREMENT, + LAST_RESET_TYPE_NEVER, + ), + } +).extend(cv.COMPONENT_SCHEMA) + + +async def to_code(config): + hub = await cg.get_variable(config[CONF_DSMR_ID]) + + sensors = [] + for key, conf in config.items(): + if not isinstance(conf, dict): + continue + id = conf.get("id") + if id and id.type == sensor.Sensor: + s = await sensor.new_sensor(conf) + cg.add(getattr(hub, f"set_{key}")(s)) + sensors.append(f"F({key})") + + cg.add_define("DSMR_SENSOR_LIST(F, sep)", cg.RawExpression(" sep ".join(sensors))) diff --git a/esphome/components/dsmr/text_sensor.py b/esphome/components/dsmr/text_sensor.py new file mode 100644 index 0000000000..821b07dc6b --- /dev/null +++ b/esphome/components/dsmr/text_sensor.py @@ -0,0 +1,94 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import text_sensor +from esphome.const import ( + CONF_ID, +) +from . import Dsmr, CONF_DSMR_ID + +AUTO_LOAD = ["dsmr"] + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(CONF_DSMR_ID): cv.use_id(Dsmr), + cv.Optional("identification"): text_sensor.TEXT_SENSOR_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(text_sensor.TextSensor), + } + ), + cv.Optional("p1_version"): text_sensor.TEXT_SENSOR_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(text_sensor.TextSensor), + } + ), + cv.Optional("p1_version_be"): text_sensor.TEXT_SENSOR_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(text_sensor.TextSensor), + } + ), + cv.Optional("timestamp"): text_sensor.TEXT_SENSOR_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(text_sensor.TextSensor), + } + ), + cv.Optional("electricity_tariff"): text_sensor.TEXT_SENSOR_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(text_sensor.TextSensor), + } + ), + cv.Optional("electricity_failure_log"): text_sensor.TEXT_SENSOR_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(text_sensor.TextSensor), + } + ), + cv.Optional("message_short"): text_sensor.TEXT_SENSOR_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(text_sensor.TextSensor), + } + ), + cv.Optional("message_long"): text_sensor.TEXT_SENSOR_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(text_sensor.TextSensor), + } + ), + cv.Optional("gas_equipment_id"): text_sensor.TEXT_SENSOR_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(text_sensor.TextSensor), + } + ), + cv.Optional("thermal_equipment_id"): text_sensor.TEXT_SENSOR_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(text_sensor.TextSensor), + } + ), + cv.Optional("water_equipment_id"): text_sensor.TEXT_SENSOR_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(text_sensor.TextSensor), + } + ), + cv.Optional("sub_equipment_id"): text_sensor.TEXT_SENSOR_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(text_sensor.TextSensor), + } + ), + } +).extend(cv.COMPONENT_SCHEMA) + + +async def to_code(config): + hub = await cg.get_variable(config[CONF_DSMR_ID]) + + text_sensors = [] + for key, conf in config.items(): + if not isinstance(conf, dict): + continue + id = conf.get("id") + if id and id.type == text_sensor.TextSensor: + var = cg.new_Pvariable(conf[CONF_ID]) + await text_sensor.register_text_sensor(var, conf) + cg.add(getattr(hub, f"set_{key}")(var)) + text_sensors.append(f"F({key})") + + cg.add_define( + "DSMR_TEXT_SENSOR_LIST(F, sep)", cg.RawExpression(" sep ".join(text_sensors)) + ) diff --git a/platformio.ini b/platformio.ini index 9f1366d9af..c280c54a21 100644 --- a/platformio.ini +++ b/platformio.ini @@ -34,6 +34,9 @@ lib_deps = 1655@1.0.2 ; TinyGPSPlus (has name conflict) 6865@1.0.0 ; TM1651 Battery Display 6306@1.0.3 ; HM3301 + glmnet/Dsmr@0.3 ; used by dsmr + rweather/Crypto@0.2.0 ; used by dsmr + build_flags = -DESPHOME_LOG_LEVEL=ESPHOME_LOG_LEVEL_VERY_VERBOSE src_filter = diff --git a/tests/test3.yaml b/tests/test3.yaml index 6402684c5d..e35c1e611c 100644 --- a/tests/test3.yaml +++ b/tests/test3.yaml @@ -594,6 +594,9 @@ sensor: name: 'Import Reactive Energy' export_reactive_energy: name: 'Export Reactive Energy' + - platform: dsmr + energy_delivered_tariff1: + name: dsmr_energy_delivered_tariff1 - platform: nextion id: testnumber @@ -735,6 +738,11 @@ text_sensor: id: text0 update_interval: 4s component_name: text0 + - platform: dsmr + identification: + name: "dsmr_identification" + p1_version: + name: "dsmr_p1_version" script: - id: my_script @@ -1242,3 +1250,7 @@ fingerprint_grow: data: finger_id: !lambda 'return finger_id;' uart_id: uart6 + +dsmr: + decryption_key: 00112233445566778899aabbccddeeff + uart_id: uart6