diff --git a/esphome/components/dsmr/__init__.py b/esphome/components/dsmr/__init__.py index 1cfc21a4ac..06b022c513 100644 --- a/esphome/components/dsmr/__init__.py +++ b/esphome/components/dsmr/__init__.py @@ -1,5 +1,6 @@ import esphome.codegen as cg import esphome.config_validation as cv +from esphome import pins from esphome.components import uart from esphome.const import ( CONF_ID, @@ -11,11 +12,13 @@ CODEOWNERS = ["@glmnet", "@zuidwijk"] DEPENDENCIES = ["uart"] AUTO_LOAD = ["sensor", "text_sensor"] -CONF_DSMR_ID = "dsmr_id" -CONF_DECRYPTION_KEY = "decryption_key" CONF_CRC_CHECK = "crc_check" +CONF_DECRYPTION_KEY = "decryption_key" +CONF_DSMR_ID = "dsmr_id" CONF_GAS_MBUS_ID = "gas_mbus_id" CONF_MAX_TELEGRAM_LENGTH = "max_telegram_length" +CONF_REQUEST_INTERVAL = "request_interval" +CONF_REQUEST_PIN = "request_pin" # Hack to prevent compile error due to ambiguity with lib namespace dsmr_ns = cg.esphome_ns.namespace("esphome::dsmr") @@ -48,6 +51,8 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_CRC_CHECK, default=True): cv.boolean, cv.Optional(CONF_GAS_MBUS_ID, default=1): cv.int_, cv.Optional(CONF_MAX_TELEGRAM_LENGTH, default=1500): cv.int_, + cv.Optional(CONF_REQUEST_PIN): pins.gpio_output_pin_schema, + cv.Optional(CONF_REQUEST_INTERVAL): cv.positive_time_period_milliseconds, } ).extend(uart.UART_DEVICE_SCHEMA), cv.only_with_arduino, @@ -62,6 +67,14 @@ async def to_code(config): cg.add(var.set_decryption_key(config[CONF_DECRYPTION_KEY])) await cg.register_component(var, config) + if CONF_REQUEST_PIN in config: + request_pin = await cg.gpio_pin_expression(config[CONF_REQUEST_PIN]) + cg.add(var.set_request_pin(request_pin)) + if CONF_REQUEST_INTERVAL in config: + cg.add( + var.set_request_interval(config[CONF_REQUEST_INTERVAL].total_milliseconds) + ) + cg.add_define("DSMR_GAS_MBUS_ID", config[CONF_GAS_MBUS_ID]) # DSMR Parser diff --git a/esphome/components/dsmr/dsmr.cpp b/esphome/components/dsmr/dsmr.cpp index 631b18a1f4..03e418662a 100644 --- a/esphome/components/dsmr/dsmr.cpp +++ b/esphome/components/dsmr/dsmr.cpp @@ -13,147 +13,217 @@ namespace dsmr { static const char *const TAG = "dsmr"; void Dsmr::setup() { - telegram_ = new char[max_telegram_len_]; // NOLINT + this->telegram_ = new char[this->max_telegram_len_]; // NOLINT + if (this->request_pin_ != nullptr) { + this->request_pin_->setup(); + } } void Dsmr::loop() { - if (decryption_key_.empty()) - receive_telegram_(); - else - receive_encrypted_(); + if (this->ready_to_request_data_()) { + if (this->decryption_key_.empty()) { + this->receive_telegram_(); + } else { + this->receive_encrypted_(); + } + } +} + +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::available_within_timeout_() { uint8_t tries = READ_TIMEOUT_MS / 5; while (tries--) { delay(5); - if (available()) { + if (this->available()) { return true; } } 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::receive_telegram_() { while (true) { - if (!available()) { - if (!header_found_ || !available_within_timeout_()) { + if (!this->available()) { + if (!this->header_found_ || !this->available_within_timeout_()) { return; } } - const char c = read(); + const char c = this->read(); // Find a new telegram header, i.e. forward slash. if (c == '/') { ESP_LOGV(TAG, "Header of telegram found"); - header_found_ = true; - footer_found_ = false; - telegram_len_ = 0; + this->header_found_ = true; + this->footer_found_ = false; + this->telegram_len_ = 0; } - if (!header_found_) + if (!this->header_found_) continue; // Check for buffer overflow. - if (telegram_len_ >= max_telegram_len_) { - header_found_ = false; - footer_found_ = false; - ESP_LOGE(TAG, "Error: telegram larger than buffer (%d bytes)", max_telegram_len_); + if (this->telegram_len_ >= this->max_telegram_len_) { + this->header_found_ = false; + this->footer_found_ = false; + 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 - while (c == '(' && (telegram_[telegram_len_ - 1] == '\n' || telegram_[telegram_len_ - 1] == '\r')) - telegram_len_--; + // 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->telegram_len_ - 1]; + if (previous_char == '\n' || previous_char == '\r') { + this->telegram_len_--; + } else { + break; + } + } + } // Store the byte in the buffer. - telegram_[telegram_len_] = c; - telegram_len_++; + this->telegram_[this->telegram_len_] = c; + this->telegram_len_++; // Check for a footer, i.e. exlamation mark, followed by a hex checksum. if (c == '!') { ESP_LOGV(TAG, "Footer of telegram found"); - footer_found_ = true; + this->footer_found_ = true; continue; } // Check for the end of the hex checksum, i.e. a newline. - if (footer_found_ && c == '\n') { + if (this->footer_found_ && c == '\n') { // Parse the telegram and publish sensor values. - parse_telegram(); + this->parse_telegram(); - header_found_ = false; + this->header_found_ = false; return; } } } void Dsmr::receive_encrypted_() { - encrypted_telegram_len_ = 0; + this->encrypted_telegram_len_ = 0; size_t packet_size = 0; while (true) { - if (!available()) { - if (!header_found_) { + if (!this->available()) { + if (!this->header_found_) { return; } - if (!available_within_timeout_()) { + if (!this->available_within_timeout_()) { ESP_LOGW(TAG, "Timeout while reading data for encrypted telegram"); return; } } - const char c = read(); + const char c = this->read(); // Find a new telegram start byte. - if (!header_found_) { + if (!this->header_found_) { if ((uint8_t) c != 0xDB) { continue; } ESP_LOGV(TAG, "Start byte 0xDB of encrypted telegram found"); - header_found_ = true; + this->header_found_ = true; } // Check for buffer overflow. - if (encrypted_telegram_len_ >= max_telegram_len_) { - header_found_ = false; - ESP_LOGE(TAG, "Error: encrypted telegram larger than buffer (%d bytes)", max_telegram_len_); + if (this->encrypted_telegram_len_ >= this->max_telegram_len_) { + this->header_found_ = false; + ESP_LOGE(TAG, "Error: encrypted telegram larger than buffer (%d bytes)", this->max_telegram_len_); return; } - encrypted_telegram_[encrypted_telegram_len_++] = c; + this->encrypted_telegram_[this->encrypted_telegram_len_++] = c; - if (packet_size == 0 && encrypted_telegram_len_ > 20) { + if (packet_size == 0 && this->encrypted_telegram_len_ > 20) { // Complete header + data bytes - packet_size = 13 + (encrypted_telegram_[11] << 8 | encrypted_telegram_[12]); + packet_size = 13 + (this->encrypted_telegram_[11] << 8 | this->encrypted_telegram_[12]); ESP_LOGV(TAG, "Encrypted telegram size: %d bytes", packet_size); } - if (encrypted_telegram_len_ == packet_size && packet_size > 0) { + if (this->encrypted_telegram_len_ == packet_size && packet_size > 0) { ESP_LOGV(TAG, "End of encrypted telegram found"); GCM *gcmaes128{new GCM()}; - gcmaes128->setKey(decryption_key_.data(), gcmaes128->keySize()); + 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++) - encrypted_telegram_[i] = encrypted_telegram_[i + 4]; + this->encrypted_telegram_[i] = this->encrypted_telegram_[i + 4]; constexpr uint16_t iv_size{12}; - gcmaes128->setIV(&encrypted_telegram_[2], iv_size); - gcmaes128->decrypt(reinterpret_cast(telegram_), + gcmaes128->setIV(&this->encrypted_telegram_[2], iv_size); + gcmaes128->decrypt(reinterpret_cast(this->telegram_), // the ciphertext start at byte 18 - &encrypted_telegram_[18], + &this->encrypted_telegram_[18], // cipher size - encrypted_telegram_len_ - 17); + this->encrypted_telegram_len_ - 17); delete gcmaes128; // NOLINT(cppcoreguidelines-owning-memory) - telegram_len_ = strnlen(telegram_, max_telegram_len_); - ESP_LOGV(TAG, "Decrypted telegram size: %d bytes", telegram_len_); - ESP_LOGVV(TAG, "Decrypted telegram: %s", telegram_); + this->telegram_len_ = strnlen(this->telegram_, this->max_telegram_len_); + ESP_LOGV(TAG, "Decrypted telegram size: %d bytes", this->telegram_len_); + ESP_LOGVV(TAG, "Decrypted telegram: %s", this->telegram_); - parse_telegram(); + this->parse_telegram(); - header_found_ = false; - telegram_len_ = 0; + this->header_found_ = false; + this->telegram_len_ = 0; return; } } @@ -162,24 +232,32 @@ void Dsmr::receive_encrypted_() { bool Dsmr::parse_telegram() { MyData data; ESP_LOGV(TAG, "Trying to parse telegram"); + this->stop_requesting_data_(); ::dsmr::ParseResult res = - ::dsmr::P1Parser::parse(&data, telegram_, telegram_len_, false, - crc_check_); // Parse telegram according to data definition. Ignore unknown values. + ::dsmr::P1Parser::parse(&data, this->telegram_, this->telegram_len_, 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(telegram_, telegram_ + telegram_len_); + auto err_str = res.fullError(this->telegram_, this->telegram_ + this->telegram_len_); ESP_LOGE(TAG, "%s", err_str.c_str()); return false; } else { this->status_clear_warning(); - publish_sensors(data); + this->publish_sensors(data); return true; } } void Dsmr::dump_config() { ESP_LOGCONFIG(TAG, "DSMR:"); - ESP_LOGCONFIG(TAG, " Max telegram length: %d", max_telegram_len_); + ESP_LOGCONFIG(TAG, " Max telegram length: %d", this->max_telegram_len_); + + 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, ) @@ -191,10 +269,10 @@ void Dsmr::dump_config() { void Dsmr::set_decryption_key(const std::string &decryption_key) { if (decryption_key.length() == 0) { ESP_LOGI(TAG, "Disabling decryption"); - decryption_key_.clear(); - if (encrypted_telegram_ != nullptr) { - delete[] encrypted_telegram_; - encrypted_telegram_ = nullptr; + this->decryption_key_.clear(); + if (this->encrypted_telegram_ != nullptr) { + delete[] this->encrypted_telegram_; + this->encrypted_telegram_ = nullptr; } return; } @@ -203,7 +281,7 @@ void Dsmr::set_decryption_key(const std::string &decryption_key) { ESP_LOGE(TAG, "Error, decryption key must be 32 character long"); return; } - decryption_key_.clear(); + this->decryption_key_.clear(); ESP_LOGI(TAG, "Decryption key is set"); // Verbose level prints decryption key @@ -212,11 +290,11 @@ void Dsmr::set_decryption_key(const std::string &decryption_key) { 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)); + this->decryption_key_.push_back(std::strtoul(temp, nullptr, 16)); } - if (encrypted_telegram_ == nullptr) { - encrypted_telegram_ = new uint8_t[max_telegram_len_]; // NOLINT + if (this->encrypted_telegram_ == nullptr) { + this->encrypted_telegram_ = new uint8_t[this->max_telegram_len_]; // NOLINT } } diff --git a/esphome/components/dsmr/dsmr.h b/esphome/components/dsmr/dsmr.h index 5943e4d47f..0430eb93ed 100644 --- a/esphome/components/dsmr/dsmr.h +++ b/esphome/components/dsmr/dsmr.h @@ -52,7 +52,6 @@ class Dsmr : public Component, public uart::UARTDevice { Dsmr(uart::UARTComponent *uart, bool crc_check) : uart::UARTDevice(uart), crc_check_(crc_check) {} void setup() override; - void loop() override; bool parse_telegram(); @@ -75,6 +74,9 @@ class Dsmr : public Component, public uart::UARTDevice { void set_max_telegram_length(size_t length); + void set_request_pin(GPIOPin *request_pin) { this->request_pin_ = request_pin; } + void set_request_interval(uint32_t interval) { this->request_interval_ = interval; } + // Sensor setters #define DSMR_SET_SENSOR(s) \ void set_##s(sensor::Sensor *sensor) { s_##s##_ = sensor; } @@ -99,6 +101,16 @@ class Dsmr : public Component, public uart::UARTDevice { /// lost in the process. bool available_within_timeout_(); + // Data request + GPIOPin *request_pin_{nullptr}; + uint32_t request_interval_{0}; + uint32_t last_request_time_{0}; + bool requesting_data_{false}; + bool ready_to_request_data_(); + bool request_interval_reached_(); + void start_requesting_data_(); + void stop_requesting_data_(); + // Telegram buffer size_t max_telegram_len_; char *telegram_{nullptr}; diff --git a/tests/test3.yaml b/tests/test3.yaml index 0b9297a0b8..27a0c148d9 100644 --- a/tests/test3.yaml +++ b/tests/test3.yaml @@ -1309,6 +1309,8 @@ dsmr: decryption_key: 00112233445566778899aabbccddeeff uart_id: uart6 max_telegram_length: 1000 + request_pin: D5 + request_interval: 20s daly_bms: update_interval: 20s