From a550a24a4c142b7a35c8b37915f6ab9d5cfe0f94 Mon Sep 17 00:00:00 2001 From: mhendriks Date: Sat, 13 Jul 2024 16:38:40 +0200 Subject: [PATCH] p1 out support dsmr sensor --- components/dsmr/__init__.py | 9 +- components/dsmr/dsmr.cpp | 327 --------------------------------- components/dsmr/dsmr.h | 8 + components/dsmr/sensor.py | 31 ++++ components/dsmr/text_sensor.py | 9 +- 5 files changed, 53 insertions(+), 331 deletions(-) delete mode 100644 components/dsmr/dsmr.cpp diff --git a/components/dsmr/__init__.py b/components/dsmr/__init__.py index 1f3a6ad..7be71a2 100644 --- a/components/dsmr/__init__.py +++ b/components/dsmr/__init__.py @@ -10,6 +10,8 @@ CODEOWNERS = ["@glmnet", "@zuidwijk"] +MULTI_CONF = True + DEPENDENCIES = ["uart"] AUTO_LOAD = ["sensor", "text_sensor"] @@ -17,6 +19,7 @@ CONF_DECRYPTION_KEY = "decryption_key" CONF_DSMR_ID = "dsmr_id" CONF_GAS_MBUS_ID = "gas_mbus_id" +CONF_WATER_MBUS_ID = "water_mbus_id" CONF_MAX_TELEGRAM_LENGTH = "max_telegram_length" CONF_REQUEST_INTERVAL = "request_interval" CONF_REQUEST_PIN = "request_pin" @@ -51,6 +54,7 @@ def _validate_key(value): cv.Optional(CONF_DECRYPTION_KEY): _validate_key, cv.Optional(CONF_CRC_CHECK, default=True): cv.boolean, cv.Optional(CONF_GAS_MBUS_ID, default=1): cv.int_, + cv.Optional(CONF_WATER_MBUS_ID, default=2): cv.int_, cv.Optional(CONF_MAX_TELEGRAM_LENGTH, default=1500): cv.int_, cv.Optional(CONF_REQUEST_PIN): pins.gpio_output_pin_schema, cv.Optional( @@ -79,10 +83,11 @@ async def to_code(config): cg.add(var.set_request_interval(config[CONF_REQUEST_INTERVAL].total_milliseconds)) cg.add(var.set_receive_timeout(config[CONF_RECEIVE_TIMEOUT].total_milliseconds)) - cg.add_define("DSMR_GAS_MBUS_ID", config[CONF_GAS_MBUS_ID]) + cg.add_build_flag("-DDSMR_GAS_MBUS_ID=" + str(config[CONF_GAS_MBUS_ID])) + cg.add_build_flag("-DDSMR_WATER_MBUS_ID=" + str(config[CONF_WATER_MBUS_ID])) # DSMR Parser - cg.add_library("glmnet/Dsmr", "0.5") + cg.add_library("glmnet/Dsmr", "0.9") # Crypto cg.add_library("rweather/Crypto", "0.4.0") diff --git a/components/dsmr/dsmr.cpp b/components/dsmr/dsmr.cpp deleted file mode 100644 index 7b339e5..0000000 --- a/components/dsmr/dsmr.cpp +++ /dev/null @@ -1,327 +0,0 @@ -#ifdef USE_ARDUINO - -#include "dsmr.h" -#include "esphome/core/log.h" - -#include -#include -#include - -namespace esphome { -namespace dsmr { - -static const char *const TAG = "dsmr"; - -void Dsmr::setup() { - this->telegram_ = new char[this->max_telegram_len_]; // NOLINT - if (this->request_pin_ != nullptr) { - this->request_pin_->setup(); - } -} - -void Dsmr::loop() { - if (this->ready_to_request_data_()) { - if (this->decryption_key_.empty()) { - this->receive_telegram_(); - } else { - this->receive_encrypted_telegram_(); - } - } -} - -bool Dsmr::ready_to_request_data_() { - // When using a request pin, then wait for the next request interval. - if (this->request_pin_ != nullptr) { - if (!this->requesting_data_ && this->request_interval_reached_()) { - this->start_requesting_data_(); - } - } - // Otherwise, sink serial data until next request interval. - else { - if (this->request_interval_reached_()) { - this->start_requesting_data_(); - } - if (!this->requesting_data_) { - while (this->available()) { - this->read(); - } - } - } - return this->requesting_data_; -} - -bool Dsmr::request_interval_reached_() { - if (this->last_request_time_ == 0) { - return true; - } - return millis() - this->last_request_time_ > this->request_interval_; -} - -bool Dsmr::receive_timeout_reached_() { return millis() - this->last_read_time_ > this->receive_timeout_; } - -bool Dsmr::available_within_timeout_() { - // Data are available for reading on the UART bus? - // Then we can start reading right away. - if (this->available()) { - this->last_read_time_ = millis(); - return true; - } - // When we're not in the process of reading a telegram, then there is - // no need to actively wait for new data to come in. - if (!header_found_) { - return false; - } - // A telegram is being read. The smart meter might not deliver a telegram - // in one go, but instead send it in chunks with small pauses in between. - // When the UART RX buffer cannot hold a full telegram, then make sure - // that the UART read buffer does not overflow while other components - // perform their work in their loop. Do this by not returning control to - // the main loop, until the read timeout is reached. - if (this->parent_->get_rx_buffer_size() < this->max_telegram_len_) { - while (!this->receive_timeout_reached_()) { - delay(5); - if (this->available()) { - this->last_read_time_ = millis(); - return true; - } - } - } - // No new data has come in during the read timeout? Then stop reading the - // telegram and start waiting for the next one to arrive. - if (this->receive_timeout_reached_()) { - ESP_LOGW(TAG, "Timeout while reading data for telegram"); - this->reset_telegram_(); - } - - return false; -} - -void Dsmr::start_requesting_data_() { - if (!this->requesting_data_) { - if (this->request_pin_ != nullptr) { - ESP_LOGV(TAG, "Start requesting data from P1 port"); - this->request_pin_->digital_write(true); - } else { - ESP_LOGV(TAG, "Start reading data from P1 port"); - } - this->requesting_data_ = true; - this->last_request_time_ = millis(); - } -} - -void Dsmr::stop_requesting_data_() { - if (this->requesting_data_) { - if (this->request_pin_ != nullptr) { - ESP_LOGV(TAG, "Stop requesting data from P1 port"); - this->request_pin_->digital_write(false); - } else { - ESP_LOGV(TAG, "Stop reading data from P1 port"); - } - while (this->available()) { - this->read(); - } - this->requesting_data_ = false; - } -} - -void Dsmr::reset_telegram_() { - this->header_found_ = false; - this->footer_found_ = false; - this->bytes_read_ = 0; - this->crypt_bytes_read_ = 0; - this->crypt_telegram_len_ = 0; - this->last_read_time_ = 0; -} - -void Dsmr::receive_telegram_() { - while (this->available_within_timeout_()) { - const char c = this->read(); - - // Find a new telegram header, i.e. forward slash. - if (c == '/') { - ESP_LOGV(TAG, "Header of telegram found"); - this->reset_telegram_(); - this->header_found_ = true; - } - if (!this->header_found_) - continue; - - // Check for buffer overflow. - if (this->bytes_read_ >= this->max_telegram_len_) { - this->reset_telegram_(); - ESP_LOGE(TAG, "Error: telegram larger than buffer (%d bytes)", this->max_telegram_len_); - return; - } - - // Some v2.2 or v3 meters will send a new value which starts with '(' - // in a new line, while the value belongs to the previous ObisId. For - // proper parsing, remove these new line characters. - if (c == '(') { - while (true) { - auto previous_char = this->telegram_[this->bytes_read_ - 1]; - if (previous_char == '\n' || previous_char == '\r') { - this->bytes_read_--; - } else { - break; - } - } - } - - // Store the byte in the buffer. - this->telegram_[this->bytes_read_] = c; - this->bytes_read_++; - - // Check for a footer, i.e. exlamation mark, followed by a hex checksum. - if (c == '!') { - ESP_LOGV(TAG, "Footer of telegram found"); - this->footer_found_ = true; - continue; - } - // Check for the end of the hex checksum, i.e. a newline. - if (this->footer_found_ && c == '\n') { - // Parse the telegram and publish sensor values. - this->parse_telegram(); - this->reset_telegram_(); - return; - } - } -} - -void Dsmr::receive_encrypted_telegram_() { - while (this->available_within_timeout_()) { - const char c = this->read(); - - // Find a new telegram start byte. - if (!this->header_found_) { - if ((uint8_t) c != 0xDB) { - continue; - } - ESP_LOGV(TAG, "Start byte 0xDB of encrypted telegram found"); - this->reset_telegram_(); - this->header_found_ = true; - } - - // Check for buffer overflow. - if (this->crypt_bytes_read_ >= this->max_telegram_len_) { - this->reset_telegram_(); - ESP_LOGE(TAG, "Error: encrypted telegram larger than buffer (%d bytes)", this->max_telegram_len_); - return; - } - - // Store the byte in the buffer. - this->crypt_telegram_[this->crypt_bytes_read_] = c; - this->crypt_bytes_read_++; - - // Read the length of the incoming encrypted telegram. - if (this->crypt_telegram_len_ == 0 && this->crypt_bytes_read_ > 20) { - // Complete header + data bytes - this->crypt_telegram_len_ = 13 + (this->crypt_telegram_[11] << 8 | this->crypt_telegram_[12]); - ESP_LOGV(TAG, "Encrypted telegram length: %d bytes", this->crypt_telegram_len_); - } - - // Check for the end of the encrypted telegram. - if (this->crypt_telegram_len_ == 0 || this->crypt_bytes_read_ != this->crypt_telegram_len_) { - continue; - } - ESP_LOGV(TAG, "End of encrypted telegram found"); - - // Decrypt the encrypted telegram. - 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++) - this->crypt_telegram_[i] = this->crypt_telegram_[i + 4]; - constexpr uint16_t iv_size{12}; - gcmaes128->setIV(&this->crypt_telegram_[2], iv_size); - gcmaes128->decrypt(reinterpret_cast(this->telegram_), - // the ciphertext start at byte 18 - &this->crypt_telegram_[18], - // cipher size - this->crypt_bytes_read_ - 17); - delete gcmaes128; // NOLINT(cppcoreguidelines-owning-memory) - - this->bytes_read_ = strnlen(this->telegram_, this->max_telegram_len_); - ESP_LOGV(TAG, "Decrypted telegram size: %d bytes", this->bytes_read_); - ESP_LOGVV(TAG, "Decrypted telegram: %s", this->telegram_); - - // Parse the decrypted telegram and publish sensor values. - this->parse_telegram(); - this->reset_telegram_(); - return; - } -} - -bool Dsmr::parse_telegram() { - MyData data; - ESP_LOGV(TAG, "Trying to parse telegram"); - this->stop_requesting_data_(); - ::dsmr::ParseResult res = - ::dsmr::P1Parser::parse(&data, this->telegram_, this->bytes_read_, false, - this->crc_check_); // Parse telegram according to data definition. Ignore unknown values. - if (res.err) { - // Parsing error, show it - auto err_str = res.fullError(this->telegram_, this->telegram_ + this->bytes_read_); - ESP_LOGE(TAG, "%s", err_str.c_str()); - return false; - } else { - this->status_clear_warning(); - this->publish_sensors(data); - return true; - } -} - -void Dsmr::dump_config() { - ESP_LOGCONFIG(TAG, "DSMR:"); - ESP_LOGCONFIG(TAG, " Max telegram length: %d", this->max_telegram_len_); - ESP_LOGCONFIG(TAG, " Receive timeout: %.1fs", this->receive_timeout_ / 1e3f); - if (this->request_pin_ != nullptr) { - LOG_PIN(" Request Pin: ", this->request_pin_); - } - if (this->request_interval_ > 0) { - ESP_LOGCONFIG(TAG, " Request Interval: %.1fs", this->request_interval_ / 1e3f); - } - -#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(); - if (this->crypt_telegram_ != nullptr) { - delete[] this->crypt_telegram_; - this->crypt_telegram_ = nullptr; - } - 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); - this->decryption_key_.push_back(std::strtoul(temp, nullptr, 16)); - } - - if (this->crypt_telegram_ == nullptr) { - this->crypt_telegram_ = new uint8_t[this->max_telegram_len_]; // NOLINT - } -} - -} // namespace dsmr -} // namespace esphome - -#endif // USE_ARDUINO diff --git a/components/dsmr/dsmr.h b/components/dsmr/dsmr.h index 76f79ee..7304737 100644 --- a/components/dsmr/dsmr.h +++ b/components/dsmr/dsmr.h @@ -13,6 +13,8 @@ #include #include +#include + namespace esphome { namespace dsmr { @@ -83,6 +85,9 @@ class Dsmr : public Component, public uart::UARTDevice { void set_##s(text_sensor::TextSensor *sensor) { s_##s##_ = sensor; } DSMR_TEXT_SENSOR_LIST(DSMR_SET_TEXT_SENSOR, ) + // handled outside dsmr + void set_telegram(text_sensor::TextSensor *sensor) { s_telegram_ = sensor; } + protected: void receive_telegram_(); void receive_encrypted_telegram_(); @@ -122,6 +127,9 @@ class Dsmr : public Component, public uart::UARTDevice { bool header_found_{false}; bool footer_found_{false}; + // handled outside dsmr + text_sensor::TextSensor *s_telegram_{nullptr}; + // Sensor member pointers #define DSMR_DECLARE_SENSOR(s) sensor::Sensor *s_##s##_{nullptr}; DSMR_SENSOR_LIST(DSMR_DECLARE_SENSOR, ) diff --git a/components/dsmr/sensor.py b/components/dsmr/sensor.py index 0b0439b..f2398d1 100644 --- a/components/dsmr/sensor.py +++ b/components/dsmr/sensor.py @@ -8,6 +8,7 @@ DEVICE_CLASS_GAS, DEVICE_CLASS_POWER, DEVICE_CLASS_VOLTAGE, + DEVICE_CLASS_WATER, STATE_CLASS_MEASUREMENT, STATE_CLASS_TOTAL_INCREASING, UNIT_AMPERE, @@ -236,6 +237,36 @@ device_class=DEVICE_CLASS_GAS, state_class=STATE_CLASS_TOTAL_INCREASING, ), + cv.Optional("water_delivered"): sensor.sensor_schema( + unit_of_measurement=UNIT_CUBIC_METER, + accuracy_decimals=3, + device_class=DEVICE_CLASS_WATER, + state_class=STATE_CLASS_TOTAL_INCREASING, + ), + cv.Optional( + "active_energy_import_current_average_demand" + ): sensor.sensor_schema( + unit_of_measurement=UNIT_KILOWATT, + accuracy_decimals=3, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional( + "active_energy_import_maximum_demand_running_month" + ): sensor.sensor_schema( + unit_of_measurement=UNIT_KILOWATT, + accuracy_decimals=3, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional( + "active_energy_import_maximum_demand_last_13_months" + ): sensor.sensor_schema( + unit_of_measurement=UNIT_KILOWATT, + accuracy_decimals=3, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, + ), } ).extend(cv.COMPONENT_SCHEMA) diff --git a/components/dsmr/text_sensor.py b/components/dsmr/text_sensor.py index 202cc07..7c13fe7 100644 --- a/components/dsmr/text_sensor.py +++ b/components/dsmr/text_sensor.py @@ -1,7 +1,7 @@ import esphome.codegen as cg import esphome.config_validation as cv from esphome.components import text_sensor - +from esphome.const import CONF_INTERNAL from . import Dsmr, CONF_DSMR_ID AUTO_LOAD = ["dsmr"] @@ -22,6 +22,9 @@ cv.Optional("water_equipment_id"): text_sensor.text_sensor_schema(), cv.Optional("sub_equipment_id"): text_sensor.text_sensor_schema(), cv.Optional("gas_delivered_text"): text_sensor.text_sensor_schema(), + cv.Optional("telegram"): text_sensor.text_sensor_schema().extend( + {cv.Optional(CONF_INTERNAL, default=True): cv.boolean} + ), } ).extend(cv.COMPONENT_SCHEMA) @@ -37,7 +40,9 @@ async def to_code(config): if id and id.type == text_sensor.TextSensor: var = await text_sensor.new_text_sensor(conf) cg.add(getattr(hub, f"set_{key}")(var)) - text_sensors.append(f"F({key})") + if key != "telegram": + # telegram is not handled by dsmr + text_sensors.append(f"F({key})") if text_sensors: cg.add_define(