mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-30 22:53:59 +00:00 
			
		
		
		
	Add support for P1 Data Request pin control (#2676)
This commit is contained in:
		| @@ -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 | ||||
|   | ||||
| @@ -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<AES128> *gcmaes128{new GCM<AES128>()}; | ||||
|       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<uint8_t *>(telegram_), | ||||
|       gcmaes128->setIV(&this->encrypted_telegram_[2], iv_size); | ||||
|       gcmaes128->decrypt(reinterpret_cast<uint8_t *>(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<void> 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 | ||||
|   } | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -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}; | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
		Reference in New Issue
	
	Block a user