├── .gitignore ├── Examples ├── README.md ├── dutch-dsmr-2.2.yaml └── dutch-dsmr-4-and-5.yaml ├── README.md ├── components ├── dsmr │ ├── __init__.py │ ├── crc16.h │ ├── dsmr.cpp │ ├── dsmr.h │ ├── fields.cpp │ ├── fields.h │ ├── parser.h │ ├── sensor.py │ ├── text_sensor.py │ └── util.h └── empty.txt ├── dashboard_import.yaml ├── pre-compiled ├── slimmelezer-v2022.10.1.bin ├── slimmelezer-v2022.11.3.bin ├── slimmelezer-v2022.11.4.bin ├── slimmelezer-v2022.11.5.bin ├── slimmelezer-v2022.12.1.bin ├── slimmelezer-v2022.12.3.bin └── slimmelezer-v2022.12.8.bin ├── sl32plus.yaml ├── slimmelezer-be.yaml ├── slimmelezer-test.yaml ├── slimmelezer.old.yaml └── slimmelezer.yaml /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.py[cod] 3 | secrets.yaml 4 | -------------------------------------------------------------------------------- /Examples/README.md: -------------------------------------------------------------------------------- 1 | # Example configs 2 | 3 | Here I want to collect different samples for different kind of meters and different countries 4 | -------------------------------------------------------------------------------- /Examples/dutch-dsmr-2.2.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | substitutions: 3 | device_name: slimmelezer 4 | device_description: "DIY P1 module to read your smart meter" 5 | 6 | esphome: 7 | name: ${device_name} 8 | comment: "${device_description}" 9 | platform: ESP8266 10 | esp8266_restore_from_flash: true 11 | board: d1_mini 12 | name_add_mac_suffix: false 13 | project: 14 | name: zuidwijk.slimmelezer 15 | version: "1.0" 16 | 17 | wifi: 18 | networks: 19 | - ssid: "JouwSSID" 20 | password: "JouwWiFiWachtwoord" 21 | 22 | # Enable fallback hotspot (captive portal) in case wifi connection fails 23 | ap: 24 | ssid: ${device_name} 25 | ap_timeout: 15s 26 | 27 | captive_portal: 28 | 29 | # Enable logging 30 | logger: 31 | baud_rate: 0 32 | 33 | # Enable Home Assistant API 34 | api: 35 | 36 | ota: 37 | 38 | web_server: 39 | port: 80 40 | 41 | uart: 42 | rx_pin: D7 43 | baud_rate: 9600 44 | data_bits: 7 45 | parity: EVEN 46 | stop_bits: 1 47 | 48 | dsmr: 49 | gas_mbus_id: 1 50 | crc_check: false 51 | 52 | sensor: 53 | - platform: dsmr 54 | energy_delivered_tariff1: 55 | name: "Energy Consumed Tariff 1" 56 | energy_delivered_tariff2: 57 | name: "Energy Consumed Tariff 2" 58 | energy_returned_tariff1: 59 | name: "Energy Produced Tariff 1" 60 | energy_returned_tariff2: 61 | name: "Energy Produced Tariff 2" 62 | power_delivered: 63 | name: "Power Consumed" 64 | accuracy_decimals: 3 65 | power_returned: 66 | name: "Power Produced" 67 | accuracy_decimals: 3 68 | electricity_failures: 69 | name: "Electricity Failures" 70 | icon: mdi:alert 71 | electricity_long_failures: 72 | name: "Long Electricity Failures" 73 | icon: mdi:alert 74 | voltage_l1: 75 | name: "Voltage Phase 1" 76 | voltage_l2: 77 | name: "Voltage Phase 2" 78 | voltage_l3: 79 | name: "Voltage Phase 3" 80 | current_l1: 81 | name: "Current Phase 1" 82 | current_l2: 83 | name: "Current Phase 2" 84 | current_l3: 85 | name: "Current Phase 3" 86 | power_delivered_l1: 87 | name: "Power Consumed Phase 1" 88 | accuracy_decimals: 3 89 | power_delivered_l2: 90 | name: "Power Consumed Phase 2" 91 | accuracy_decimals: 3 92 | power_delivered_l3: 93 | name: "Power Consumed Phase 3" 94 | accuracy_decimals: 3 95 | power_returned_l1: 96 | name: "Power Produced Phase 1" 97 | accuracy_decimals: 3 98 | power_returned_l2: 99 | name: "Power Produced Phase 2" 100 | accuracy_decimals: 3 101 | power_returned_l3: 102 | name: "Power Produced Phase 3" 103 | accuracy_decimals: 3 104 | gas_delivered: 105 | name: "Gas Consumed" 106 | - platform: uptime 107 | name: "SlimmeLezer Uptime" 108 | - platform: wifi_signal 109 | name: "SlimmeLezer Wi-Fi Signal" 110 | update_interval: 60s 111 | 112 | text_sensor: 113 | - platform: dsmr 114 | identification: 115 | name: "DSMR Identification" 116 | p1_version: 117 | name: "DSMR Version" 118 | - platform: wifi_info 119 | ip_address: 120 | name: "SlimmeLezer IP Address" 121 | ssid: 122 | name: "SlimmeLezer Wi-Fi SSID" 123 | bssid: 124 | name: "SlimmeLezer Wi-Fi BSSID" 125 | - platform: version 126 | name: "ESPHome Version" 127 | hide_timestamp: true 128 | -------------------------------------------------------------------------------- /Examples/dutch-dsmr-4-and-5.yaml: -------------------------------------------------------------------------------- 1 | substitutions: 2 | device_name: slimmelezer 3 | device_description: "DIY P1 module to read your smart meter" 4 | 5 | esphome: 6 | name: ${device_name} 7 | comment: "${device_description}" 8 | platform: ESP8266 9 | esp8266_restore_from_flash: true 10 | board: d1_mini 11 | name_add_mac_suffix: false 12 | project: 13 | name: zuidwijk.slimmelezer 14 | version: "2.0" 15 | 16 | wifi: 17 | networks: 18 | - ssid: "JouwSSID" 19 | password: "JouwWiFiWachtwoord" 20 | 21 | # Enable fallback hotspot (captive portal) in case wifi connection fails 22 | ap: 23 | ssid: ${device_name} 24 | 25 | captive_portal: 26 | 27 | # Enable logging 28 | logger: 29 | baud_rate: 0 30 | 31 | # Enable Home Assistant API 32 | api: 33 | 34 | ota: 35 | 36 | web_server: 37 | port: 80 38 | 39 | uart: 40 | baud_rate: 115200 41 | rx_pin: D7 42 | rx_buffer_size: 1700 43 | 44 | dsmr: 45 | max_telegram_length: 1700 46 | 47 | sensor: 48 | - platform: dsmr 49 | energy_delivered_tariff1: 50 | name: "Energy Consumed Tariff 1" 51 | energy_delivered_tariff2: 52 | name: "Energy Consumed Tariff 2" 53 | energy_returned_tariff1: 54 | name: "Energy Produced Tariff 1" 55 | energy_returned_tariff2: 56 | name: "Energy Produced Tariff 2" 57 | power_delivered: 58 | name: "Power Consumed" 59 | accuracy_decimals: 3 60 | power_returned: 61 | name: "Power Produced" 62 | accuracy_decimals: 3 63 | electricity_failures: 64 | name: "Electricity Failures" 65 | icon: mdi:alert 66 | electricity_long_failures: 67 | name: "Long Electricity Failures" 68 | icon: mdi:alert 69 | voltage_l1: 70 | name: "Voltage Phase 1" 71 | voltage_l2: 72 | name: "Voltage Phase 2" 73 | voltage_l3: 74 | name: "Voltage Phase 3" 75 | current_l1: 76 | name: "Current Phase 1" 77 | current_l2: 78 | name: "Current Phase 2" 79 | current_l3: 80 | name: "Current Phase 3" 81 | power_delivered_l1: 82 | name: "Power Consumed Phase 1" 83 | accuracy_decimals: 3 84 | power_delivered_l2: 85 | name: "Power Consumed Phase 2" 86 | accuracy_decimals: 3 87 | power_delivered_l3: 88 | name: "Power Consumed Phase 3" 89 | accuracy_decimals: 3 90 | power_returned_l1: 91 | name: "Power Produced Phase 1" 92 | accuracy_decimals: 3 93 | power_returned_l2: 94 | name: "Power Produced Phase 2" 95 | accuracy_decimals: 3 96 | power_returned_l3: 97 | name: "Power Produced Phase 3" 98 | accuracy_decimals: 3 99 | gas_delivered: 100 | name: "Gas Consumed" 101 | - platform: uptime 102 | name: "SlimmeLezer Uptime" 103 | - platform: wifi_signal 104 | name: "SlimmeLezer Wi-Fi Signal" 105 | update_interval: 60s 106 | 107 | text_sensor: 108 | - platform: dsmr 109 | identification: 110 | name: "DSMR Identification" 111 | p1_version: 112 | name: "DSMR Version" 113 | - platform: wifi_info 114 | ip_address: 115 | name: "SlimmeLezer IP Address" 116 | ssid: 117 | name: "SlimmeLezer Wi-Fi SSID" 118 | bssid: 119 | name: "SlimmeLezer Wi-Fi BSSID" 120 | - platform: version 121 | name: "ESPHome Version" 122 | hide_timestamp: false 123 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Buy Me A Coffee ![button_buy-slimmelezer](https://user-images.githubusercontent.com/10123063/127783836-900027f9-e7ea-4084-89e8-89e1cc5f486e.png) 2 | # Please read!! 3 | Since ESPHome version 2021.8.0 the DSMR component is natively in ESPHome. Please use this in your code, as all maintenance and changes are published there and **not** in my code 4 | 5 | All documentation is on ESPHome self: https://esphome.io/components/sensor/dsmr.html 6 | 7 | A big shout out to @glmnet @klaasnicolaas @frenck and off course @home-assistant & @esphome for improving the code and place it within ESPHome natively! 8 | 9 | # DSMR component for ESPHome 10 | The [SlimmeLezer](https://www.zuidwijk.com/product/slimmelezer/) is a compact build easy to use module to read data via the P1 port on a Smart Meter. Based on an ESP8266 (Wemos D1), the SlimmeLezer is perfect to use with [ESPHome](https://esphome.io) and integrates seamless into [Home Assistant](https://www.home-assistant.io). 11 | 12 | ![IMG_2886](https://user-images.githubusercontent.com/10123063/127781811-f3a67082-32f3-4633-803a-d320bc6af3e4.jpeg) 13 | ![IMG_2887](https://user-images.githubusercontent.com/10123063/127781814-8bbe0781-5bdb-4e65-97ac-509afdb0b72d.jpeg) 14 | 15 | ## DSMR component 16 | The main goal is to create one universal component, which can be used in every country. Though the DSMR (Dutch Smart Meter Requirements) is [specified](https://www.netbeheernederland.nl/_upload/Files/Slimme_meter_15_a727fce1f1.pdf) with pre specified OBIS code, not every country has exact the same code. Some examples: 17 | 18 | **Version information for P1 output** 19 | - Default OBIS: 1-3:0.2.8.255 20 | - Belgium OBIS: 0-0:96.1.4.255 21 | 22 | **Meter Reading electricity delivered to client (Tariff 1) in 0,001 kWh** 23 | - Default OBIS: 1-0:1.8.1.255 24 | - Luxembourg OBIS: 1-0:1.8.0.255 25 | 26 | **Meter Reading electricity delivered by dient (Tariff 1) in 0,001 kWh** 27 | - Default OBIS: 1-0:2.8.1.255 28 | - Luxembourg OBIS: 1-0:2.8.0.255 29 | 30 | Some countries like Luxembourg, Sweden and Hungary, uses kvar next to kW. Therefor all deviant OBIS code is added as extra fields. This gives more sensors than needed, yet it can be used in every country where DSMR based Smart Meters is being used. 31 | 32 | ### Decryption data for Luxembourg 33 | Smart Meters used in Luxembourg are using encryption. Decryption for Luxembourg is build in the code. This can be defined in the code: 34 | ```YAML 35 | dsmr: 36 | id: dsmr_instance 37 | decryption_key: '00112233445566778899AABBCCDDEEFF' 38 | ``` 39 | 40 | When the key is not set in the code, or when the key changes, it can be set/changes via a Service within Home Assistant, created via below api: 41 | ```YAML 42 | # Enable Home Assistant API 43 | api: 44 | services: 45 | service: set_dsmr_key 46 | variables: 47 | private_key: string 48 | then: 49 | - logger.log: 50 | format: Setting private key %s. Set to empty string to disable 51 | args: [private_key.c_str()] 52 | - globals.set: 53 | id: has_key 54 | value: !lambda "return private_key.length() == 32;" 55 | - lambda: |- 56 | if (private_key.length() == 32) 57 | private_key.copy(id(stored_decryption_key), 32); 58 | id(dsmr_instance).set_decryption_key(private_key); 59 | ``` 60 | 61 | In Home Assistant go to Services and select the service ESPHome: {name}_set_dsmr_key. There fill in the code received from the provider: 62 | ![SlimmeLezer_set_key](https://user-images.githubusercontent.com/10123063/127783141-52d3ae77-e02b-4296-a1fb-78ab3bbe5ff3.jpg) 63 | 64 | 65 | ### Different uart 66 | The SlimmeLezer is built with a logic inverter on the pcb. Connecting that directly to the Rx of the Wemos, causes that it can't be flashed via USB as it constanly pulls the Rx either high or low. Therefor I'm using the 2nd uart, on pin D7. That's why the uart is specified on pin D7 in the code: 67 | ```YAML 68 | uart: 69 | baud_rate: 115200 70 | rx_pin: D7 71 | ``` 72 | -------------------------------------------------------------------------------- /components/dsmr/__init__.py: -------------------------------------------------------------------------------- 1 | import esphome.codegen as cg 2 | import esphome.config_validation as cv 3 | from esphome.components import uart 4 | from esphome.const import ( 5 | CONF_ID, 6 | CONF_UART_ID, 7 | ) 8 | 9 | DEPENDENCIES = ["uart"] 10 | AUTO_LOAD = ["sensor", "text_sensor"] 11 | 12 | CONF_DSMR_ID = "dsmr_id" 13 | CONF_DECRYPTION_KEY = "decryption_key" 14 | 15 | dsmr_ns = cg.esphome_ns.namespace("dsmr_") 16 | DSMR = dsmr_ns.class_("Dsmr", cg.Component, uart.UARTDevice) 17 | 18 | 19 | def _validate_key(value): 20 | value = cv.string_strict(value) 21 | parts = [value[i : i + 2] for i in range(0, len(value), 2)] 22 | if len(parts) != 16: 23 | raise cv.Invalid("Decryption key must consist of 16 hexadecimal numbers") 24 | parts_int = [] 25 | if any(len(part) != 2 for part in parts): 26 | raise cv.Invalid("Decryption key must be format XX") 27 | for part in parts: 28 | try: 29 | parts_int.append(int(part, 16)) 30 | except ValueError: 31 | # pylint: disable=raise-missing-from 32 | raise cv.Invalid("Decryption key must be hex values from 00 to FF") 33 | 34 | return "".join(f"{part:02X}" for part in parts_int) 35 | 36 | 37 | CONFIG_SCHEMA = cv.Schema( 38 | { 39 | cv.GenerateID(): cv.declare_id(DSMR), 40 | cv.Optional(CONF_DECRYPTION_KEY): _validate_key, 41 | } 42 | ).extend(uart.UART_DEVICE_SCHEMA) 43 | 44 | 45 | def to_code(config): 46 | uart_component = yield cg.get_variable(config[CONF_UART_ID]) 47 | var = cg.new_Pvariable(config[CONF_ID], uart_component) 48 | if CONF_DECRYPTION_KEY in config: 49 | cg.add(var.set_decryption_key(config[CONF_DECRYPTION_KEY])) 50 | yield cg.register_component(var, config) 51 | 52 | # Crypto 53 | cg.add_library("1168", "0.2.0") 54 | -------------------------------------------------------------------------------- /components/dsmr/crc16.h: -------------------------------------------------------------------------------- 1 | /* CRC compatibility, adapted from the Teensy 3 core at: 2 | https://github.com/PaulStoffregen/cores/tree/master/teensy3 3 | which was in turn adapted by Paul Stoffregen from the C-only comments here: 4 | http://svn.savannah.nongnu.org/viewvc/trunk/avr-libc/include/util/crc16.h?revision=933&root=avr-libc&view=markup */ 5 | 6 | /* Copyright (c) 2002, 2003, 2004 Marek Michalkiewicz 7 | All rights reserved. 8 | 9 | Redistribution and use in source and binary forms, with or without 10 | modification, are permitted provided that the following conditions are met: 11 | 12 | * Redistributions of source code must retain the above copyright 13 | notice, this list of conditions and the following disclaimer. 14 | 15 | * Redistributions in binary form must reproduce the above copyright 16 | notice, this list of conditions and the following disclaimer in 17 | the documentation and/or other materials provided with the 18 | distribution. 19 | 20 | * Neither the name of the copyright holders nor the names of 21 | contributors may be used to endorse or promote products derived 22 | from this software without specific prior written permission. 23 | 24 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 25 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 26 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 27 | ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE 28 | LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 29 | CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 30 | SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 31 | INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 32 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 33 | ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 34 | POSSIBILITY OF SUCH DAMAGE. */ 35 | 36 | #ifndef _UTIL_CRC16_H_ 37 | #ifdef ARDUINO_ARCH_AVR 38 | #include 39 | #else 40 | #define _UTIL_CRC16_H_ 41 | #include 42 | 43 | static inline uint16_t _crc16_update(uint16_t crc, uint8_t data) __attribute__((always_inline, unused)); 44 | static inline uint16_t _crc16_update(uint16_t crc, uint8_t data) 45 | { 46 | unsigned int i; 47 | 48 | crc ^= data; 49 | for (i = 0; i < 8; ++i) { 50 | if (crc & 1) { 51 | crc = (crc >> 1) ^ 0xA001; 52 | } else { 53 | crc = (crc >> 1); 54 | } 55 | } 56 | return crc; 57 | } 58 | 59 | static inline uint16_t _crc_xmodem_update(uint16_t crc, uint8_t data) __attribute__((always_inline, unused)); 60 | static inline uint16_t _crc_xmodem_update(uint16_t crc, uint8_t data) 61 | { 62 | unsigned int i; 63 | 64 | crc = crc ^ ((uint16_t)data << 8); 65 | for (i=0; i<8; i++) { 66 | if (crc & 0x8000) { 67 | crc = (crc << 1) ^ 0x1021; 68 | } else { 69 | crc <<= 1; 70 | } 71 | } 72 | return crc; 73 | } 74 | 75 | static inline uint16_t _crc_ccitt_update (uint16_t crc, uint8_t data) __attribute__((always_inline, unused)); 76 | static inline uint16_t _crc_ccitt_update (uint16_t crc, uint8_t data) 77 | { 78 | data ^= (crc & 255); 79 | data ^= data << 4; 80 | 81 | return ((((uint16_t)data << 8) | (crc >> 8)) ^ (uint8_t)(data >> 4) 82 | ^ ((uint16_t)data << 3)); 83 | } 84 | 85 | static inline uint8_t _crc_ibutton_update(uint8_t crc, uint8_t data) __attribute__((always_inline, unused)); 86 | static inline uint8_t _crc_ibutton_update(uint8_t crc, uint8_t data) 87 | { 88 | unsigned int i; 89 | 90 | crc = crc ^ data; 91 | for (i = 0; i < 8; i++) { 92 | if (crc & 0x01) { 93 | crc = (crc >> 1) ^ 0x8C; 94 | } else { 95 | crc >>= 1; 96 | } 97 | } 98 | return crc; 99 | } 100 | 101 | #endif 102 | #endif 103 | -------------------------------------------------------------------------------- /components/dsmr/dsmr.cpp: -------------------------------------------------------------------------------- 1 | #include "dsmr.h" 2 | #include "esphome/core/log.h" 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | namespace esphome { 9 | namespace dsmr_ { 10 | 11 | static const char *TAG = "dsmr"; 12 | 13 | void Dsmr::loop() { 14 | if (this->decryption_key_.size() == 0) 15 | this->receive_telegram(); 16 | else 17 | this->receive_encrypted(); 18 | } 19 | 20 | void Dsmr::receive_telegram() { 21 | while (available()) { 22 | const char c = read(); 23 | 24 | if (c == '/') { // header: forward slash 25 | ESP_LOGV(TAG, "Header found"); 26 | header_found_ = true; 27 | footer_found_ = false; 28 | telegram_len_ = 0; 29 | } 30 | 31 | if (!header_found_) 32 | continue; 33 | if (telegram_len_ >= MAX_TELEGRAM_LENGTH) { // Buffer overflow 34 | header_found_ = false; 35 | footer_found_ = false; 36 | ESP_LOGE(TAG, "Error: Message larger than buffer"); 37 | } 38 | 39 | telegram_[telegram_len_] = c; 40 | telegram_len_++; 41 | if (c == '!') { // footer: exclamation mark 42 | ESP_LOGV(TAG, "Footer found"); 43 | footer_found_ = true; 44 | } else { 45 | if (footer_found_ && c == 10) { // last \n after footer 46 | header_found_ = false; 47 | // Parse message 48 | if (parse_telegram()) 49 | return; 50 | } 51 | } 52 | } 53 | } 54 | 55 | void Dsmr::receive_encrypted() { 56 | // Encrypted buffer 57 | uint8_t buffer[MAX_TELEGRAM_LENGTH]; 58 | size_t buffer_length = 0; 59 | 60 | size_t packet_size = 0; 61 | while (available()) { 62 | const char c = read(); 63 | 64 | if (!header_found_) { 65 | if (c == 0xdb) { 66 | ESP_LOGV(TAG, "Start byte 0xDB found"); 67 | header_found_ = true; 68 | } 69 | } 70 | 71 | // Sanity check 72 | if (!header_found_ || buffer_length >= MAX_TELEGRAM_LENGTH) { 73 | if (buffer_length == 0) { 74 | ESP_LOGE(TAG, "First byte of encrypted telegram should be 0xDB, aborting."); 75 | } else { 76 | ESP_LOGW(TAG, "Unexpected data"); 77 | } 78 | this->status_momentary_warning("unexpected_data"); 79 | this->flush(); 80 | while (available()) 81 | read(); 82 | return; 83 | } 84 | 85 | buffer[buffer_length++] = c; 86 | 87 | if (packet_size == 0 && buffer_length > 20) // Complete header + a few bytes of data 88 | { 89 | packet_size = buffer[11] << 8 | buffer[12]; 90 | } 91 | if (buffer_length == packet_size + 13 && packet_size > 0) { 92 | ESP_LOGV(TAG, "Encrypted data: %d bytes", buffer_length); 93 | 94 | GCM *gcmaes128{new GCM()}; 95 | gcmaes128->setKey(this->decryption_key_.data(), gcmaes128->keySize()); 96 | // the iv is 8 bytes of the system title + 4 bytes frame counter 97 | // system title is at byte 2 and frame counter at byte 15 98 | for (int i = 10; i < 14; i++) 99 | buffer[i] = buffer[i + 4]; 100 | constexpr uint16_t iv_size{12}; 101 | gcmaes128->setIV(&buffer[2], iv_size); 102 | gcmaes128->decrypt(static_cast(static_cast(this->telegram_)), 103 | // the cypher text start at byte 18 104 | &buffer[18], 105 | // cypher data size 106 | buffer_length - 17); 107 | delete gcmaes128; 108 | 109 | telegram_len_ = strlen(this->telegram_); 110 | ESP_LOGV(TAG, "Decrypted data length: %d", telegram_len_); 111 | ESP_LOGVV(TAG, "Decrypted data %s", this->telegram_); 112 | 113 | parse_telegram(); 114 | telegram_len_ = 0; 115 | return; 116 | } 117 | 118 | if (!available()) { 119 | // baud rate is 115200 for encrypted data, this means a few byte should arrive every time 120 | // program runs faster than buffer loading then available() might return false in the middle 121 | delay(4); // Wait for data 122 | } 123 | } 124 | if (buffer_length > 0) 125 | ESP_LOGW(TAG, "Timeout while waiting for encrypted data or invalid data received."); 126 | } 127 | 128 | bool Dsmr::parse_telegram() { 129 | MyData data; 130 | ESP_LOGV(TAG, "Trying to parse"); 131 | ::dsmr::ParseResult res = 132 | ::dsmr::P1Parser::parse(&data, telegram_, telegram_len_, 133 | false); // Parse telegram according to data definition. Ignore unknown values. 134 | if (res.err) { 135 | // Parsing error, show it 136 | auto err_str = res.fullError(telegram_, telegram_ + telegram_len_); 137 | ESP_LOGE(TAG, "%s", err_str.c_str()); 138 | return false; 139 | } else { 140 | this->status_clear_warning(); 141 | publish_sensors(data); 142 | return true; 143 | } 144 | } 145 | 146 | void Dsmr::dump_config() { 147 | ESP_LOGCONFIG(TAG, "dsmr:"); 148 | 149 | #define DSMR_LOG_SENSOR(s) LOG_SENSOR(" ", #s, this->s_##s##_); 150 | DSMR_SENSOR_LIST(DSMR_LOG_SENSOR, ) 151 | 152 | #define DSMR_LOG_TEXT_SENSOR(s) LOG_TEXT_SENSOR(" ", #s, this->s_##s##_); 153 | DSMR_TEXT_SENSOR_LIST(DSMR_LOG_TEXT_SENSOR, ) 154 | } 155 | 156 | void Dsmr::set_decryption_key(const std::string &decryption_key) { 157 | if (decryption_key.length() == 0) { 158 | ESP_LOGI(TAG, "Disabling decryption"); 159 | this->decryption_key_.clear(); 160 | return; 161 | } 162 | 163 | if (decryption_key.length() != 32) { 164 | ESP_LOGE(TAG, "Error, decryption key must be 32 character long."); 165 | return; 166 | } 167 | this->decryption_key_.clear(); 168 | 169 | ESP_LOGI(TAG, "Decryption key is set."); 170 | // Verbose level prints decryption key 171 | ESP_LOGV(TAG, "Using decryption key: %s", decryption_key.c_str()); 172 | 173 | char temp[3] = {0}; 174 | for (int i = 0; i < 16; i++) { 175 | strncpy(temp, &(decryption_key.c_str()[i * 2]), 2); 176 | decryption_key_.push_back(std::strtoul(temp, NULL, 16)); 177 | } 178 | } 179 | 180 | } // namespace dsmr_ 181 | } // namespace esphome 182 | -------------------------------------------------------------------------------- /components/dsmr/dsmr.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "esphome/core/component.h" 4 | #include "esphome/components/sensor/sensor.h" 5 | #include "esphome/components/text_sensor/text_sensor.h" 6 | #include "esphome/components/uart/uart.h" 7 | #include "esphome/core/log.h" 8 | #include "esphome/core/defines.h" 9 | 10 | #include "parser.h" 11 | #include "fields.h" 12 | 13 | namespace esphome { 14 | namespace dsmr_ { 15 | 16 | static constexpr uint32_t MAX_TELEGRAM_LENGTH = 1500; 17 | static constexpr uint32_t POLL_TIMEOUT = 1000; 18 | 19 | using namespace dsmr::fields; 20 | 21 | // DSMR_**_LIST generated by ESPHome and written in esphome/core/defines 22 | 23 | #if defined(DSMR_SENSOR_LIST) && defined(DSMR_TEXT_SENSOR_LIST) 24 | #define DSMR_BOTH , 25 | #else 26 | #define DSMR_BOTH 27 | #endif 28 | 29 | #ifndef DSMR_SENSOR_LIST 30 | #define DSMR_SENSOR_LIST(F, SEP) 31 | #endif 32 | 33 | #ifndef DSMR_TEXT_SENSOR_LIST 34 | #define DSMR_TEXT_SENSOR_LIST(F, SEP) 35 | #endif 36 | 37 | #define DSMR_DATA_SENSOR(s) s 38 | #define COMMA , 39 | 40 | using MyData = dsmr::ParsedData; 42 | 43 | class Dsmr : public Component, public uart::UARTDevice { 44 | public: 45 | Dsmr(uart::UARTComponent* uart) : uart::UARTDevice(uart) {} 46 | 47 | void loop() override; 48 | 49 | bool parse_telegram(); 50 | 51 | void publish_sensors(MyData data) { 52 | #define DSMR_PUBLISH_SENSOR(s) \ 53 | if (data.s##_present && this->s_##s##_ != nullptr) \ 54 | s_##s##_->publish_state(data.s); 55 | DSMR_SENSOR_LIST(DSMR_PUBLISH_SENSOR, ) 56 | 57 | #define DSMR_PUBLISH_TEXT_SENSOR(s) \ 58 | if (data.s##_present && this->s_##s##_ != nullptr) \ 59 | s_##s##_->publish_state(data.s.c_str()); 60 | DSMR_TEXT_SENSOR_LIST(DSMR_PUBLISH_TEXT_SENSOR, ) 61 | }; 62 | 63 | void dump_config() override; 64 | 65 | void set_decryption_key(const std::string& decryption_key); 66 | 67 | // Sensor setters 68 | #define DSMR_SET_SENSOR(s) \ 69 | void set_##s(sensor::Sensor* sensor) { s_##s##_ = sensor; } 70 | DSMR_SENSOR_LIST(DSMR_SET_SENSOR, ) 71 | 72 | #define DSMR_SET_TEXT_SENSOR(s) \ 73 | void set_##s(text_sensor::TextSensor* sensor) { s_##s##_ = sensor; } 74 | DSMR_TEXT_SENSOR_LIST(DSMR_SET_TEXT_SENSOR, ) 75 | 76 | protected: 77 | void receive_telegram(); 78 | void receive_encrypted(); 79 | 80 | // Telegram buffer 81 | char telegram_[MAX_TELEGRAM_LENGTH]; 82 | int telegram_len_{0}; 83 | 84 | // Serial parser 85 | bool header_found_{false}; 86 | bool footer_found_{false}; 87 | 88 | // Sensor member pointers 89 | #define DSMR_DECLARE_SENSOR(s) sensor::Sensor* s_##s##_{nullptr}; 90 | DSMR_SENSOR_LIST(DSMR_DECLARE_SENSOR, ) 91 | 92 | #define DSMR_DECLARE_TEXT_SENSOR(s) text_sensor::TextSensor* s_##s##_{nullptr}; 93 | DSMR_TEXT_SENSOR_LIST(DSMR_DECLARE_TEXT_SENSOR, ) 94 | 95 | std::vector decryption_key_{}; 96 | }; 97 | } // namespace dsmr_ 98 | } // namespace esphome 99 | -------------------------------------------------------------------------------- /components/dsmr/fields.cpp: -------------------------------------------------------------------------------- 1 | /** 2 | * Arduino DSMR parser. 3 | * 4 | * This software is licensed under the MIT License. 5 | * 6 | * Copyright (c) 2015 Matthijs Kooijman 7 | * 8 | * Permission is hereby granted, free of charge, to any person obtaining 9 | * a copy of this software and associated documentation files (the 10 | * "Software"), to deal in the Software without restriction, including 11 | * without limitation the rights to use, copy, modify, merge, publish, 12 | * distribute, sublicense, and/or sell copies of the Software, and to 13 | * permit persons to whom the Software is furnished to do so, subject to 14 | * the following conditions: 15 | * 16 | * The above copyright notice and this permission notice shall be 17 | * included in all copies or substantial portions of the Software. 18 | * 19 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 20 | * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 21 | * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 22 | * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS 23 | * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN 24 | * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 25 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 26 | * SOFTWARE. 27 | * 28 | * Field parsing functions 29 | */ 30 | 31 | #include "fields.h" 32 | 33 | using namespace dsmr; 34 | using namespace dsmr::fields; 35 | 36 | // Since C++11 it is possible to define the initial values for static 37 | // const members in the class declaration, but if their address is 38 | // taken, they still need a normal definition somewhere (to allocate 39 | // storage). 40 | constexpr char units::none[]; 41 | constexpr char units::kWh[]; 42 | constexpr char units::Wh[]; 43 | constexpr char units::kW[]; 44 | constexpr char units::W[]; 45 | constexpr char units::V[]; 46 | constexpr char units::mV[]; 47 | constexpr char units::A[]; 48 | constexpr char units::mA[]; 49 | constexpr char units::m3[]; 50 | constexpr char units::dm3[]; 51 | constexpr char units::GJ[]; 52 | constexpr char units::MJ[]; 53 | constexpr char units::kvar[]; 54 | constexpr char units::kvarh[]; 55 | 56 | constexpr ObisId identification::id; 57 | constexpr char identification::name_progmem[]; 58 | constexpr const __FlashStringHelper *identification::name; 59 | 60 | constexpr ObisId p1_version::id; 61 | constexpr char p1_version::name_progmem[]; 62 | constexpr const __FlashStringHelper *p1_version::name; 63 | 64 | /* extra field for Belgium */ 65 | constexpr ObisId p1_version_be::id; 66 | constexpr char p1_version_be::name_progmem[]; 67 | constexpr const __FlashStringHelper *p1_version_be::name; 68 | 69 | constexpr ObisId timestamp::id; 70 | constexpr char timestamp::name_progmem[]; 71 | constexpr const __FlashStringHelper *timestamp::name; 72 | 73 | constexpr ObisId equipment_id::id; 74 | constexpr char equipment_id::name_progmem[]; 75 | constexpr const __FlashStringHelper *equipment_id::name; 76 | 77 | /* extra for Lux */ 78 | constexpr ObisId energy_delivered_lux::id; 79 | constexpr char energy_delivered_lux::name_progmem[]; 80 | constexpr const __FlashStringHelper *energy_delivered_lux::name; 81 | 82 | constexpr ObisId energy_delivered_tariff1::id; 83 | constexpr char energy_delivered_tariff1::name_progmem[]; 84 | constexpr const __FlashStringHelper *energy_delivered_tariff1::name; 85 | 86 | constexpr ObisId energy_delivered_tariff2::id; 87 | constexpr char energy_delivered_tariff2::name_progmem[]; 88 | constexpr const __FlashStringHelper *energy_delivered_tariff2::name; 89 | 90 | /* extra for Lux */ 91 | constexpr ObisId energy_returned_lux::id; 92 | constexpr char energy_returned_lux::name_progmem[]; 93 | constexpr const __FlashStringHelper *energy_returned_lux::name; 94 | 95 | constexpr ObisId energy_returned_tariff1::id; 96 | constexpr char energy_returned_tariff1::name_progmem[]; 97 | constexpr const __FlashStringHelper *energy_returned_tariff1::name; 98 | 99 | constexpr ObisId energy_returned_tariff2::id; 100 | constexpr char energy_returned_tariff2::name_progmem[]; 101 | constexpr const __FlashStringHelper *energy_returned_tariff2::name; 102 | 103 | /* extra for Lux */ 104 | constexpr ObisId total_imported_energy::id; 105 | constexpr char total_imported_energy::name_progmem[]; 106 | constexpr const __FlashStringHelper *total_imported_energy::name; 107 | 108 | /* extra for Lux */ 109 | constexpr ObisId total_exported_energy::id; 110 | constexpr char total_exported_energy::name_progmem[]; 111 | constexpr const __FlashStringHelper *total_exported_energy::name; 112 | 113 | /* extra for Lux */ 114 | constexpr ObisId reactive_power_delivered::id; 115 | constexpr char reactive_power_delivered::name_progmem[]; 116 | constexpr const __FlashStringHelper *reactive_power_delivered::name; 117 | 118 | /* extra for Lux */ 119 | constexpr ObisId reactive_power_returned::id; 120 | constexpr char reactive_power_returned::name_progmem[]; 121 | constexpr const __FlashStringHelper *reactive_power_returned::name; 122 | 123 | constexpr ObisId electricity_tariff::id; 124 | constexpr char electricity_tariff::name_progmem[]; 125 | constexpr const __FlashStringHelper *electricity_tariff::name; 126 | 127 | constexpr ObisId power_delivered::id; 128 | constexpr char power_delivered::name_progmem[]; 129 | constexpr const __FlashStringHelper *power_delivered::name; 130 | 131 | constexpr ObisId power_returned::id; 132 | constexpr char power_returned::name_progmem[]; 133 | constexpr const __FlashStringHelper *power_returned::name; 134 | 135 | constexpr ObisId electricity_threshold::id; 136 | constexpr char electricity_threshold::name_progmem[]; 137 | constexpr const __FlashStringHelper *electricity_threshold::name; 138 | 139 | constexpr ObisId electricity_switch_position::id; 140 | constexpr char electricity_switch_position::name_progmem[]; 141 | constexpr const __FlashStringHelper *electricity_switch_position::name; 142 | 143 | constexpr ObisId electricity_failures::id; 144 | constexpr char electricity_failures::name_progmem[]; 145 | constexpr const __FlashStringHelper *electricity_failures::name; 146 | 147 | constexpr ObisId electricity_long_failures::id; 148 | constexpr char electricity_long_failures::name_progmem[]; 149 | constexpr const __FlashStringHelper *electricity_long_failures::name; 150 | 151 | constexpr ObisId electricity_failure_log::id; 152 | constexpr char electricity_failure_log::name_progmem[]; 153 | constexpr const __FlashStringHelper *electricity_failure_log::name; 154 | 155 | constexpr ObisId electricity_sags_l1::id; 156 | constexpr char electricity_sags_l1::name_progmem[]; 157 | constexpr const __FlashStringHelper *electricity_sags_l1::name; 158 | 159 | constexpr ObisId electricity_sags_l2::id; 160 | constexpr char electricity_sags_l2::name_progmem[]; 161 | constexpr const __FlashStringHelper *electricity_sags_l2::name; 162 | 163 | constexpr ObisId electricity_sags_l3::id; 164 | constexpr char electricity_sags_l3::name_progmem[]; 165 | constexpr const __FlashStringHelper *electricity_sags_l3::name; 166 | 167 | constexpr ObisId electricity_swells_l1::id; 168 | constexpr char electricity_swells_l1::name_progmem[]; 169 | constexpr const __FlashStringHelper *electricity_swells_l1::name; 170 | 171 | constexpr ObisId electricity_swells_l2::id; 172 | constexpr char electricity_swells_l2::name_progmem[]; 173 | constexpr const __FlashStringHelper *electricity_swells_l2::name; 174 | 175 | constexpr ObisId electricity_swells_l3::id; 176 | constexpr char electricity_swells_l3::name_progmem[]; 177 | constexpr const __FlashStringHelper *electricity_swells_l3::name; 178 | 179 | constexpr ObisId message_short::id; 180 | constexpr char message_short::name_progmem[]; 181 | constexpr const __FlashStringHelper *message_short::name; 182 | 183 | constexpr ObisId message_long::id; 184 | constexpr char message_long::name_progmem[]; 185 | constexpr const __FlashStringHelper *message_long::name; 186 | 187 | constexpr ObisId voltage_l1::id; 188 | constexpr char voltage_l1::name_progmem[]; 189 | constexpr const __FlashStringHelper *voltage_l1::name; 190 | 191 | constexpr ObisId voltage_l2::id; 192 | constexpr char voltage_l2::name_progmem[]; 193 | constexpr const __FlashStringHelper *voltage_l2::name; 194 | 195 | constexpr ObisId voltage_l3::id; 196 | constexpr char voltage_l3::name_progmem[]; 197 | constexpr const __FlashStringHelper *voltage_l3::name; 198 | 199 | constexpr ObisId current_l1::id; 200 | constexpr char current_l1::name_progmem[]; 201 | constexpr const __FlashStringHelper *current_l1::name; 202 | 203 | constexpr ObisId current_l2::id; 204 | constexpr char current_l2::name_progmem[]; 205 | constexpr const __FlashStringHelper *current_l2::name; 206 | 207 | constexpr ObisId current_l3::id; 208 | constexpr char current_l3::name_progmem[]; 209 | constexpr const __FlashStringHelper *current_l3::name; 210 | 211 | constexpr ObisId power_delivered_l1::id; 212 | constexpr char power_delivered_l1::name_progmem[]; 213 | constexpr const __FlashStringHelper *power_delivered_l1::name; 214 | 215 | constexpr ObisId power_delivered_l2::id; 216 | constexpr char power_delivered_l2::name_progmem[]; 217 | constexpr const __FlashStringHelper *power_delivered_l2::name; 218 | 219 | constexpr ObisId power_delivered_l3::id; 220 | constexpr char power_delivered_l3::name_progmem[]; 221 | constexpr const __FlashStringHelper *power_delivered_l3::name; 222 | 223 | constexpr ObisId power_returned_l1::id; 224 | constexpr char power_returned_l1::name_progmem[]; 225 | constexpr const __FlashStringHelper *power_returned_l1::name; 226 | 227 | constexpr ObisId power_returned_l2::id; 228 | constexpr char power_returned_l2::name_progmem[]; 229 | constexpr const __FlashStringHelper *power_returned_l2::name; 230 | 231 | constexpr ObisId power_returned_l3::id; 232 | constexpr char power_returned_l3::name_progmem[]; 233 | constexpr const __FlashStringHelper *power_returned_l3::name; 234 | 235 | /* LUX */ 236 | constexpr ObisId reactive_power_delivered_l1::id; 237 | constexpr char reactive_power_delivered_l1::name_progmem[]; 238 | constexpr const __FlashStringHelper *reactive_power_delivered_l1::name; 239 | 240 | /* LUX */ 241 | constexpr ObisId reactive_power_delivered_l2::id; 242 | constexpr char reactive_power_delivered_l2::name_progmem[]; 243 | constexpr const __FlashStringHelper *reactive_power_delivered_l2::name; 244 | 245 | /* LUX */ 246 | constexpr ObisId reactive_power_delivered_l3::id; 247 | constexpr char reactive_power_delivered_l3::name_progmem[]; 248 | constexpr const __FlashStringHelper *reactive_power_delivered_l3::name; 249 | 250 | /* LUX */ 251 | constexpr ObisId reactive_power_returned_l1::id; 252 | constexpr char reactive_power_returned_l1::name_progmem[]; 253 | constexpr const __FlashStringHelper *reactive_power_returned_l1::name; 254 | 255 | /* LUX */ 256 | constexpr ObisId reactive_power_returned_l2::id; 257 | constexpr char reactive_power_returned_l2::name_progmem[]; 258 | constexpr const __FlashStringHelper *reactive_power_returned_l2::name; 259 | 260 | /* LUX */ 261 | constexpr ObisId reactive_power_returned_l3::id; 262 | constexpr char reactive_power_returned_l3::name_progmem[]; 263 | constexpr const __FlashStringHelper *reactive_power_returned_l3::name; 264 | 265 | constexpr ObisId gas_device_type::id; 266 | constexpr char gas_device_type::name_progmem[]; 267 | constexpr const __FlashStringHelper *gas_device_type::name; 268 | 269 | constexpr ObisId gas_equipment_id::id; 270 | constexpr char gas_equipment_id::name_progmem[]; 271 | constexpr const __FlashStringHelper *gas_equipment_id::name; 272 | 273 | constexpr ObisId gas_valve_position::id; 274 | constexpr char gas_valve_position::name_progmem[]; 275 | constexpr const __FlashStringHelper *gas_valve_position::name; 276 | 277 | /* _NL */ 278 | constexpr ObisId gas_delivered::id; 279 | constexpr char gas_delivered::name_progmem[]; 280 | constexpr const __FlashStringHelper *gas_delivered::name; 281 | 282 | /* _BE */ 283 | constexpr ObisId gas_delivered_be::id; 284 | constexpr char gas_delivered_be::name_progmem[]; 285 | constexpr const __FlashStringHelper *gas_delivered_be::name; 286 | 287 | constexpr ObisId thermal_device_type::id; 288 | constexpr char thermal_device_type::name_progmem[]; 289 | constexpr const __FlashStringHelper *thermal_device_type::name; 290 | 291 | constexpr ObisId thermal_equipment_id::id; 292 | constexpr char thermal_equipment_id::name_progmem[]; 293 | constexpr const __FlashStringHelper *thermal_equipment_id::name; 294 | 295 | constexpr ObisId thermal_valve_position::id; 296 | constexpr char thermal_valve_position::name_progmem[]; 297 | constexpr const __FlashStringHelper *thermal_valve_position::name; 298 | 299 | constexpr ObisId thermal_delivered::id; 300 | constexpr char thermal_delivered::name_progmem[]; 301 | constexpr const __FlashStringHelper *thermal_delivered::name; 302 | 303 | constexpr ObisId water_device_type::id; 304 | constexpr char water_device_type::name_progmem[]; 305 | constexpr const __FlashStringHelper *water_device_type::name; 306 | 307 | constexpr ObisId water_equipment_id::id; 308 | constexpr char water_equipment_id::name_progmem[]; 309 | constexpr const __FlashStringHelper *water_equipment_id::name; 310 | 311 | constexpr ObisId water_valve_position::id; 312 | constexpr char water_valve_position::name_progmem[]; 313 | constexpr const __FlashStringHelper *water_valve_position::name; 314 | 315 | constexpr ObisId water_delivered::id; 316 | constexpr char water_delivered::name_progmem[]; 317 | constexpr const __FlashStringHelper *water_delivered::name; 318 | 319 | constexpr ObisId slave_device_type::id; 320 | constexpr char slave_device_type::name_progmem[]; 321 | constexpr const __FlashStringHelper *slave_device_type::name; 322 | 323 | constexpr ObisId slave_equipment_id::id; 324 | constexpr char slave_equipment_id::name_progmem[]; 325 | constexpr const __FlashStringHelper *slave_equipment_id::name; 326 | 327 | constexpr ObisId slave_valve_position::id; 328 | constexpr char slave_valve_position::name_progmem[]; 329 | constexpr const __FlashStringHelper *slave_valve_position::name; 330 | 331 | constexpr ObisId slave_delivered::id; 332 | constexpr char slave_delivered::name_progmem[]; 333 | constexpr const __FlashStringHelper *slave_delivered::name; 334 | 335 | -------------------------------------------------------------------------------- /components/dsmr/fields.h: -------------------------------------------------------------------------------- 1 | /** 2 | * Arduino DSMR parser. 3 | * 4 | * This software is licensed under the MIT License. 5 | * 6 | * Copyright (c) 2015 Matthijs Kooijman 7 | * 8 | * Permission is hereby granted, free of charge, to any person obtaining 9 | * a copy of this software and associated documentation files (the 10 | * "Software"), to deal in the Software without restriction, including 11 | * without limitation the rights to use, copy, modify, merge, publish, 12 | * distribute, sublicense, and/or sell copies of the Software, and to 13 | * permit persons to whom the Software is furnished to do so, subject to 14 | * the following conditions: 15 | * 16 | * The above copyright notice and this permission notice shall be 17 | * included in all copies or substantial portions of the Software. 18 | * 19 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 20 | * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 21 | * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 22 | * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS 23 | * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN 24 | * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 25 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 26 | * SOFTWARE. 27 | * 28 | * Field parsing functions 29 | */ 30 | 31 | #ifndef DSMR_INCLUDE_FIELDS_H 32 | #define DSMR_INCLUDE_FIELDS_H 33 | 34 | #include "util.h" 35 | #include "parser.h" 36 | 37 | namespace dsmr { 38 | 39 | /** 40 | * Superclass for data items in a P1 message. 41 | */ 42 | template 43 | struct ParsedField { 44 | template 45 | void apply(F& f) { 46 | f.apply(*static_cast(this)); 47 | } 48 | // By defaults, fields have no unit 49 | static const char *unit() { return ""; } 50 | }; 51 | 52 | template 53 | struct StringField : ParsedField { 54 | ParseResult parse(const char *str, const char *end) { 55 | ParseResult res = StringParser::parse_string(minlen, maxlen, str, end); 56 | if (!res.err) 57 | static_cast(this)->val() = res.result; 58 | return res; 59 | } 60 | }; 61 | 62 | // A timestamp is essentially a string using YYMMDDhhmmssX format (where 63 | // X is W or S for wintertime or summertime). Parsing this into a proper 64 | // (UNIX) timestamp is hard to do generically. Parsing it into a 65 | // single integer needs > 4 bytes top fit and isn't very useful (you 66 | // cannot really do any calculation with those values). So we just parse 67 | // into a string for now. 68 | template 69 | struct TimestampField : StringField { }; 70 | 71 | // Value that is parsed as a three-decimal float, but stored as an 72 | // integer (by multiplying by 1000). Supports val() (or implicit cast to 73 | // float) to get the original value, and int_val() to get the more 74 | // efficient integer value. The unit() and int_unit() methods on 75 | // FixedField return the corresponding units for these values. 76 | struct FixedValue { 77 | operator float() { return val();} 78 | float val() { return _value / 1000.0;} 79 | uint32_t int_val() { return _value; } 80 | 81 | uint32_t _value; 82 | }; 83 | 84 | // Floating point numbers in the message never have more than 3 decimal 85 | // digits. To prevent inefficient floating point operations, we store 86 | // them as a fixed-point number: an integer that stores the value in 87 | // thousands. For example, a value of 1.234 kWh is stored as 1234. This 88 | // effectively means that the integer value is het value in Wh. To allow 89 | // automatic printing of these values, both the original unit and the 90 | // integer unit is passed as a template argument. 91 | template 92 | struct FixedField : ParsedField { 93 | ParseResult parse(const char *str, const char *end) { 94 | ParseResult res = NumParser::parse(3, _unit, str, end); 95 | if (!res.err) 96 | static_cast(this)->val()._value = res.result; 97 | return res; 98 | } 99 | 100 | static const char *unit() { return _unit; } 101 | static const char *int_unit() { return _int_unit; } 102 | }; 103 | 104 | struct TimestampedFixedValue : public FixedValue { 105 | String timestamp; 106 | }; 107 | 108 | // Some numerical values are prefixed with a timestamp. This is simply 109 | // both of them concatenated, e.g. 0-1:24.2.1(150117180000W)(00473.789*m3) 110 | template 111 | struct TimestampedFixedField : public FixedField { 112 | ParseResult parse(const char *str, const char *end) { 113 | // First, parse timestamp 114 | ParseResult res = StringParser::parse_string(13, 13, str, end); 115 | if (res.err) 116 | return res; 117 | 118 | static_cast(this)->val().timestamp = res.result; 119 | 120 | // Which is immediately followed by the numerical value 121 | return FixedField::parse(res.next, end); 122 | } 123 | }; 124 | 125 | // A integer number is just represented as an integer. 126 | template 127 | struct IntField : ParsedField { 128 | ParseResult parse(const char *str, const char *end) { 129 | ParseResult res = NumParser::parse(0, _unit, str, end); 130 | if (!res.err) 131 | static_cast(this)->val() = res.result; 132 | return res; 133 | } 134 | 135 | static const char *unit() { return _unit; } 136 | }; 137 | 138 | // A RawField is not parsed, the entire value (including any 139 | // parenthesis around it) is returned as a string. 140 | template 141 | struct RawField : ParsedField { 142 | ParseResult parse(const char *str, const char *end) { 143 | // Just copy the string verbatim value without any parsing 144 | concat_hack(static_cast(this)->val(), str, end - str); 145 | return ParseResult().until(end); 146 | } 147 | }; 148 | 149 | namespace fields { 150 | 151 | struct units { 152 | // These variables are inside a struct, since that allows us to make 153 | // them constexpr and define their values here, but define the storage 154 | // in a cpp file. Global const(expr) variables have implicitly 155 | // internal linkage, meaning each cpp file that includes us will have 156 | // its own copy of the variable. Since we take the address of these 157 | // variables (passing it as a template argument), this would cause a 158 | // compiler warning. By putting these in a struct, this is prevented. 159 | static constexpr char none[] = ""; 160 | static constexpr char kWh[] = "kWh"; 161 | static constexpr char Wh[] = "Wh"; 162 | static constexpr char kW[] = "kW"; 163 | static constexpr char W[] = "W"; 164 | static constexpr char V[] = "V"; 165 | static constexpr char mV[] = "mV"; 166 | static constexpr char A[] = "A"; 167 | static constexpr char mA[] = "mA"; 168 | static constexpr char m3[] = "m3"; 169 | static constexpr char dm3[] = "dm3"; 170 | static constexpr char GJ[] = "GJ"; 171 | static constexpr char MJ[] = "MJ"; 172 | static constexpr char kvar[] = "kvar"; 173 | static constexpr char kvarh[] = "kvarh"; 174 | }; 175 | 176 | const uint8_t GAS_MBUS_ID = 1; 177 | const uint8_t WATER_MBUS_ID = 2; 178 | const uint8_t THERMAL_MBUS_ID = 3; 179 | const uint8_t SLAVE_MBUS_ID = 4; 180 | 181 | #define DEFINE_FIELD(fieldname, value_t, obis, field_t, field_args...) \ 182 | struct fieldname : field_t { \ 183 | value_t fieldname; \ 184 | bool fieldname ## _present = false; \ 185 | static constexpr ObisId id = obis; \ 186 | static constexpr char name_progmem[] DSMR_PROGMEM = #fieldname; \ 187 | static constexpr const __FlashStringHelper *name = reinterpret_cast(&name_progmem); \ 188 | value_t& val() { return fieldname; } \ 189 | bool& present() { return fieldname ## _present; } \ 190 | } 191 | 192 | /* Meter identification. This is not a normal field, but a 193 | * specially-formatted first line of the message */ 194 | DEFINE_FIELD(identification, String, ObisId(255, 255, 255, 255, 255, 255), RawField); 195 | 196 | /* Version information for P1 output */ 197 | DEFINE_FIELD(p1_version, String, ObisId(1, 3, 0, 2, 8), StringField, 2, 2); 198 | DEFINE_FIELD(p1_version_be, String, ObisId(0, 0, 96, 1, 4), StringField, 2, 5); 199 | 200 | /* Date-time stamp of the P1 message */ 201 | DEFINE_FIELD(timestamp, String, ObisId(0, 0, 1, 0, 0), TimestampField); 202 | 203 | /* Equipment identifier */ 204 | DEFINE_FIELD(equipment_id, String, ObisId(0, 0, 96, 1, 1), StringField, 0, 96); 205 | 206 | /* Meter Reading electricity delivered to client (Special for Lux) in 0,001 kWh */ 207 | DEFINE_FIELD(energy_delivered_lux, FixedValue, ObisId(1, 0, 1, 8, 0), FixedField, units::kWh, units::Wh); 208 | /* Meter Reading electricity delivered to client (Tariff 1) in 0,001 kWh */ 209 | DEFINE_FIELD(energy_delivered_tariff1, FixedValue, ObisId(1, 0, 1, 8, 1), FixedField, units::kWh, units::Wh); 210 | /* Meter Reading electricity delivered to client (Tariff 2) in 0,001 kWh */ 211 | DEFINE_FIELD(energy_delivered_tariff2, FixedValue, ObisId(1, 0, 1, 8, 2), FixedField, units::kWh, units::Wh); 212 | /* Meter Reading electricity delivered by client (Special for Lux) in 0,001 kWh */ 213 | DEFINE_FIELD(energy_returned_lux, FixedValue, ObisId(1, 0, 2, 8, 0), FixedField, units::kWh, units::Wh); 214 | /* Meter Reading electricity delivered by client (Tariff 1) in 0,001 kWh */ 215 | DEFINE_FIELD(energy_returned_tariff1, FixedValue, ObisId(1, 0, 2, 8, 1), FixedField, units::kWh, units::Wh); 216 | /* Meter Reading electricity delivered by client (Tariff 2) in 0,001 kWh */ 217 | DEFINE_FIELD(energy_returned_tariff2, FixedValue, ObisId(1, 0, 2, 8, 2), FixedField, units::kWh, units::Wh); 218 | 219 | /* 220 | * Extra fields used for Luxembourg 221 | */ 222 | DEFINE_FIELD(total_imported_energy, FixedValue, ObisId(1, 0, 3, 8, 0), FixedField, units::kvarh, units::kvarh); 223 | DEFINE_FIELD(total_exported_energy, FixedValue, ObisId(1, 0, 4, 8, 0), FixedField, units::kvarh, units::kvarh); 224 | 225 | /* Tariff indicator electricity. The tariff indicator can also be used 226 | * to switch tariff dependent loads e.g boilers. This is the 227 | * responsibility of the P1 user */ 228 | DEFINE_FIELD(electricity_tariff, String, ObisId(0, 0, 96, 14, 0), StringField, 4, 4); 229 | 230 | /* Actual electricity power delivered (+P) in 1 Watt resolution */ 231 | DEFINE_FIELD(power_delivered, FixedValue, ObisId(1, 0, 1, 7, 0), FixedField, units::kW, units::W); 232 | /* Actual electricity power received (-P) in 1 Watt resolution */ 233 | DEFINE_FIELD(power_returned, FixedValue, ObisId(1, 0, 2, 7, 0), FixedField, units::kW, units::W); 234 | 235 | /* 236 | * Extra fields used for Luxembourg 237 | */ 238 | DEFINE_FIELD(reactive_power_delivered, FixedValue, ObisId(1, 0, 3, 7, 0), FixedField, units::kvar, units::kvar); 239 | DEFINE_FIELD(reactive_power_returned, FixedValue, ObisId(1, 0, 4, 7, 0), FixedField, units::kvar, units::kvar); 240 | 241 | /* The actual threshold Electricity in kW. Removed in 4.0.7 / 4.2.2 / 5.0 */ 242 | DEFINE_FIELD(electricity_threshold, FixedValue, ObisId(0, 0, 17, 0, 0), FixedField, units::kW, units::W); 243 | 244 | /* Switch position Electricity (in/out/enabled). Removed in 4.0.7 / 4.2.2 / 5.0 */ 245 | DEFINE_FIELD(electricity_switch_position, uint8_t, ObisId(0, 0, 96, 3, 10), IntField, units::none); 246 | 247 | /* Number of power failures in any phase */ 248 | DEFINE_FIELD(electricity_failures, uint32_t, ObisId(0, 0, 96, 7, 21), IntField, units::none); 249 | /* Number of long power failures in any phase */ 250 | DEFINE_FIELD(electricity_long_failures, uint32_t, ObisId(0, 0, 96, 7, 9), IntField, units::none); 251 | 252 | /* Power Failure Event Log (long power failures) */ 253 | DEFINE_FIELD(electricity_failure_log, String, ObisId(1, 0, 99, 97, 0), RawField); 254 | 255 | /* Number of voltage sags in phase L1 */ 256 | DEFINE_FIELD(electricity_sags_l1, uint32_t, ObisId(1, 0, 32, 32, 0), IntField, units::none); 257 | /* Number of voltage sags in phase L2 (polyphase meters only) */ 258 | DEFINE_FIELD(electricity_sags_l2, uint32_t, ObisId(1, 0, 52, 32, 0), IntField, units::none); 259 | /* Number of voltage sags in phase L3 (polyphase meters only) */ 260 | DEFINE_FIELD(electricity_sags_l3, uint32_t, ObisId(1, 0, 72, 32, 0), IntField, units::none); 261 | 262 | /* Number of voltage swells in phase L1 */ 263 | DEFINE_FIELD(electricity_swells_l1, uint32_t, ObisId(1, 0, 32, 36, 0), IntField, units::none); 264 | /* Number of voltage swells in phase L2 (polyphase meters only) */ 265 | DEFINE_FIELD(electricity_swells_l2, uint32_t, ObisId(1, 0, 52, 36, 0), IntField, units::none); 266 | /* Number of voltage swells in phase L3 (polyphase meters only) */ 267 | DEFINE_FIELD(electricity_swells_l3, uint32_t, ObisId(1, 0, 72, 36, 0), IntField, units::none); 268 | 269 | /* Text message codes: numeric 8 digits (Note: Missing from 5.0 spec) 270 | * */ 271 | DEFINE_FIELD(message_short, String, ObisId(0, 0, 96, 13, 1), StringField, 0, 16); 272 | /* Text message max 2048 characters (Note: Spec says 1024 in comment and 273 | * 2048 in format spec, so we stick to 2048). */ 274 | DEFINE_FIELD(message_long, String, ObisId(0, 0, 96, 13, 0), StringField, 0, 2048); 275 | 276 | /* Instantaneous voltage L1 in 0.1V resolution (Note: Spec says V 277 | * resolution in comment, but 0.1V resolution in format spec. Added in 278 | * 5.0) */ 279 | DEFINE_FIELD(voltage_l1, FixedValue, ObisId(1, 0, 32, 7, 0), FixedField, units::V, units::mV); 280 | /* Instantaneous voltage L2 in 0.1V resolution (Note: Spec says V 281 | * resolution in comment, but 0.1V resolution in format spec. Added in 282 | * 5.0) */ 283 | DEFINE_FIELD(voltage_l2, FixedValue, ObisId(1, 0, 52, 7, 0), FixedField, units::V, units::mV); 284 | /* Instantaneous voltage L3 in 0.1V resolution (Note: Spec says V 285 | * resolution in comment, but 0.1V resolution in format spec. Added in 286 | * 5.0) */ 287 | DEFINE_FIELD(voltage_l3, FixedValue, ObisId(1, 0, 72, 7, 0), FixedField, units::V, units::mV); 288 | 289 | /* Instantaneous current L1 in A resolution */ 290 | //DEFINE_FIELD(current_l1, uint16_t, ObisId(1, 0, 31, 7, 0), IntField, units::A); 291 | DEFINE_FIELD(current_l1, FixedValue, ObisId(1, 0, 31, 7, 0), FixedField, units::A, units::mA); 292 | /* Instantaneous current L2 in A resolution */ 293 | //DEFINE_FIELD(current_l2, uint16_t, ObisId(1, 0, 51, 7, 0), IntField, units::A); 294 | DEFINE_FIELD(current_l2, FixedValue, ObisId(1, 0, 51, 7, 0), FixedField, units::A, units::mA); 295 | /* Instantaneous current L3 in A resolution */ 296 | //DEFINE_FIELD(current_l3, uint16_t, ObisId(1, 0, 71, 7, 0), IntField, units::A); 297 | DEFINE_FIELD(current_l3, FixedValue, ObisId(1, 0, 71, 7, 0), FixedField, units::A, units::mA); 298 | 299 | /* Instantaneous active power L1 (+P) in W resolution */ 300 | DEFINE_FIELD(power_delivered_l1, FixedValue, ObisId(1, 0, 21, 7, 0), FixedField, units::kW, units::W); 301 | /* Instantaneous active power L2 (+P) in W resolution */ 302 | DEFINE_FIELD(power_delivered_l2, FixedValue, ObisId(1, 0, 41, 7, 0), FixedField, units::kW, units::W); 303 | /* Instantaneous active power L3 (+P) in W resolution */ 304 | DEFINE_FIELD(power_delivered_l3, FixedValue, ObisId(1, 0, 61, 7, 0), FixedField, units::kW, units::W); 305 | 306 | /* Instantaneous active power L1 (-P) in W resolution */ 307 | DEFINE_FIELD(power_returned_l1, FixedValue, ObisId(1, 0, 22, 7, 0), FixedField, units::kW, units::W); 308 | /* Instantaneous active power L2 (-P) in W resolution */ 309 | DEFINE_FIELD(power_returned_l2, FixedValue, ObisId(1, 0, 42, 7, 0), FixedField, units::kW, units::W); 310 | /* Instantaneous active power L3 (-P) in W resolution */ 311 | DEFINE_FIELD(power_returned_l3, FixedValue, ObisId(1, 0, 62, 7, 0), FixedField, units::kW, units::W); 312 | 313 | /* 314 | * LUX 315 | */ 316 | /* Instantaneous reactive power L1 (+Q) in W resolution */ 317 | DEFINE_FIELD(reactive_power_delivered_l1, FixedValue, ObisId(1, 0, 23, 7, 0), FixedField, units::none, units::none); 318 | /* Instantaneous reactive power L2 (+Q) in W resolution */ 319 | DEFINE_FIELD(reactive_power_delivered_l2, FixedValue, ObisId(1, 0, 43, 7, 0), FixedField, units::none, units::none); 320 | /* Instantaneous reactive power L3 (+Q) in W resolution */ 321 | DEFINE_FIELD(reactive_power_delivered_l3, FixedValue, ObisId(1, 0, 63, 7, 0), FixedField, units::none, units::none); 322 | 323 | /* 324 | * LUX 325 | */ 326 | /* Instantaneous reactive power L1 (-Q) in W resolution */ 327 | DEFINE_FIELD(reactive_power_returned_l1, FixedValue, ObisId(1, 0, 24, 7, 0), FixedField, units::none, units::none); 328 | /* Instantaneous reactive power L2 (-Q) in W resolution */ 329 | DEFINE_FIELD(reactive_power_returned_l2, FixedValue, ObisId(1, 0, 44, 7, 0), FixedField, units::none, units::none); 330 | /* Instantaneous reactive power L3 (-Q) in W resolution */ 331 | DEFINE_FIELD(reactive_power_returned_l3, FixedValue, ObisId(1, 0, 64, 7, 0), FixedField, units::none, units::none); 332 | 333 | /* Device-Type */ 334 | DEFINE_FIELD(gas_device_type, uint16_t, ObisId(0, GAS_MBUS_ID, 24, 1, 0), IntField, units::none); 335 | 336 | /* Equipment identifier (Gas) */ 337 | DEFINE_FIELD(gas_equipment_id, String, ObisId(0, GAS_MBUS_ID, 96, 1, 0), StringField, 0, 96); 338 | /* Equipment identifier (Gas) BE */ 339 | DEFINE_FIELD(gas_equipment_id_be, String, ObisId(0, GAS_MBUS_ID, 96, 1, 1), StringField, 0, 96); 340 | 341 | /* Valve position Gas (on/off/released) (Note: Removed in 4.0.7 / 4.2.2 / 5.0). */ 342 | DEFINE_FIELD(gas_valve_position, uint8_t, ObisId(0, GAS_MBUS_ID, 24, 4, 0), IntField, units::none); 343 | 344 | /* Last 5-minute value (temperature converted), gas delivered to client 345 | * in m3, including decimal values and capture time (Note: 4.x spec has 346 | * "hourly value") */ 347 | DEFINE_FIELD(gas_delivered, TimestampedFixedValue, ObisId(0, GAS_MBUS_ID, 24, 2, 1), TimestampedFixedField, units::m3, units::dm3); 348 | /* _BE */ 349 | DEFINE_FIELD(gas_delivered_be, TimestampedFixedValue, ObisId(0, GAS_MBUS_ID, 24, 2, 3), TimestampedFixedField, units::m3, units::dm3); 350 | 351 | 352 | /* Device-Type */ 353 | DEFINE_FIELD(thermal_device_type, uint16_t, ObisId(0, THERMAL_MBUS_ID, 24, 1, 0), IntField, units::none); 354 | 355 | /* Equipment identifier (Thermal: heat or cold) */ 356 | DEFINE_FIELD(thermal_equipment_id, String, ObisId(0, THERMAL_MBUS_ID, 96, 1, 0), StringField, 0, 96); 357 | 358 | /* Valve position (on/off/released) (Note: Removed in 4.0.7 / 4.2.2 / 5.0). */ 359 | DEFINE_FIELD(thermal_valve_position, uint8_t, ObisId(0, THERMAL_MBUS_ID, 24, 4, 0), IntField, units::none); 360 | 361 | /* Last 5-minute Meter reading Heat or Cold in 0,01 GJ and capture time 362 | * (Note: 4.x spec has "hourly meter reading") */ 363 | DEFINE_FIELD(thermal_delivered, TimestampedFixedValue, ObisId(0, THERMAL_MBUS_ID, 24, 2, 1), TimestampedFixedField, units::GJ, units::MJ); 364 | 365 | 366 | /* Device-Type */ 367 | DEFINE_FIELD(water_device_type, uint16_t, ObisId(0, WATER_MBUS_ID, 24, 1, 0), IntField, units::none); 368 | 369 | /* Equipment identifier (Thermal: heat or cold) */ 370 | DEFINE_FIELD(water_equipment_id, String, ObisId(0, WATER_MBUS_ID, 96, 1, 0), StringField, 0, 96); 371 | 372 | /* Valve position (on/off/released) (Note: Removed in 4.0.7 / 4.2.2 / 5.0). */ 373 | DEFINE_FIELD(water_valve_position, uint8_t, ObisId(0, WATER_MBUS_ID, 24, 4, 0), IntField, units::none); 374 | 375 | /* Last 5-minute Meter reading in 0,001 m3 and capture time 376 | * (Note: 4.x spec has "hourly meter reading") */ 377 | DEFINE_FIELD(water_delivered, TimestampedFixedValue, ObisId(0, WATER_MBUS_ID, 24, 2, 1), TimestampedFixedField, units::m3, units::dm3); 378 | 379 | 380 | /* Device-Type */ 381 | DEFINE_FIELD(slave_device_type, uint16_t, ObisId(0, SLAVE_MBUS_ID, 24, 1, 0), IntField, units::none); 382 | 383 | /* Equipment identifier (Thermal: heat or cold) */ 384 | DEFINE_FIELD(slave_equipment_id, String, ObisId(0, SLAVE_MBUS_ID, 96, 1, 0), StringField, 0, 96); 385 | 386 | /* Valve position (on/off/released) (Note: Removed in 4.0.7 / 4.2.2 / 5.0). */ 387 | DEFINE_FIELD(slave_valve_position, uint8_t, ObisId(0, SLAVE_MBUS_ID, 24, 4, 0), IntField, units::none); 388 | 389 | /* Last 5-minute Meter reading Heat or Cold and capture time (e.g. slave 390 | * E meter) (Note: 4.x spec has "hourly meter reading") */ 391 | DEFINE_FIELD(slave_delivered, TimestampedFixedValue, ObisId(0, SLAVE_MBUS_ID, 24, 2, 1), TimestampedFixedField, units::m3, units::dm3); 392 | 393 | } // namespace fields 394 | 395 | } // namespace dsmr 396 | 397 | #endif // DSMR_INCLUDE_FIELDS_H 398 | -------------------------------------------------------------------------------- /components/dsmr/parser.h: -------------------------------------------------------------------------------- 1 | /** 2 | * Arduino DSMR parser. 3 | * 4 | * This software is licensed under the MIT License. 5 | * 6 | * Copyright (c) 2015 Matthijs Kooijman 7 | * 8 | * Permission is hereby granted, free of charge, to any person obtaining 9 | * a copy of this software and associated documentation files (the 10 | * "Software"), to deal in the Software without restriction, including 11 | * without limitation the rights to use, copy, modify, merge, publish, 12 | * distribute, sublicense, and/or sell copies of the Software, and to 13 | * permit persons to whom the Software is furnished to do so, subject to 14 | * the following conditions: 15 | * 16 | * The above copyright notice and this permission notice shall be 17 | * included in all copies or substantial portions of the Software. 18 | * 19 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 20 | * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 21 | * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 22 | * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS 23 | * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN 24 | * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 25 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 26 | * SOFTWARE. 27 | * 28 | * Message parsing core 29 | */ 30 | 31 | #ifndef DSMR_INCLUDE_PARSER_H 32 | #define DSMR_INCLUDE_PARSER_H 33 | 34 | #include "crc16.h" 35 | #include "util.h" 36 | 37 | namespace dsmr { 38 | 39 | /** 40 | * ParsedData is a template for the result of parsing a Dsmr P1 message. 41 | * You pass the fields you want to add to it as template arguments. 42 | * 43 | * This template will then generate a class that extends all the fields 44 | * passed (the fields really are classes themselves). Since each field 45 | * class has a single member variable, with the same name as the field 46 | * class, all of these fields will be available on the generated class. 47 | * 48 | * In other words, if I have: 49 | * 50 | * using MyData = ParsedData< 51 | * identification, 52 | * equipment_id 53 | * >; 54 | * 55 | * MyData data; 56 | * 57 | * then I can refer to the fields like data.identification and 58 | * data.equipment_id normally. 59 | * 60 | * Furthermore, this class offers some helper methods that can be used 61 | * to loop over all the fields inside it. 62 | */ 63 | template struct ParsedData; 64 | 65 | /** 66 | * Base case: No fields present. 67 | */ 68 | template<> struct ParsedData<> { 69 | ParseResult __attribute__((__always_inline__)) 70 | parse_line_inlined(const ObisId & /* id */, const char *str, const char * /* end */) { 71 | // Parsing succeeded, but found no matching handler (so return 72 | // set the next pointer to show nothing was parsed). 73 | return ParseResult().until(str); 74 | } 75 | 76 | template void __attribute__((__always_inline__)) applyEach_inlined(F && /* f */) { 77 | // Nothing to do 78 | } 79 | 80 | bool all_present_inlined() { return true; } 81 | }; 82 | 83 | // Do not use F() for multiply-used strings (including strings used from 84 | // multiple template instantiations), that would result in multiple 85 | // instances of the string in the binary 86 | static constexpr char DUPLICATE_FIELD[] DSMR_PROGMEM = "Duplicate field"; 87 | 88 | /** 89 | * General case: At least one typename is passed. 90 | */ 91 | template struct ParsedData : public T, ParsedData { 92 | /** 93 | * This method is used by the parser to parse a single line. The 94 | * OBIS id of the line is passed, and this method recursively finds a 95 | * field with a matching id. If any, it calls it's parse method, which 96 | * parses the value and stores it in the field. 97 | */ 98 | ParseResult parse_line(const ObisId &id, const char *str, const char *end) { 99 | return parse_line_inlined(id, str, end); 100 | } 101 | 102 | /** 103 | * always_inline version of parse_line. This is a separate method, to 104 | * allow recursively inlining all calls, but still have a non-inlined 105 | * top-level parse_line method. 106 | */ 107 | ParseResult __attribute__((__always_inline__)) 108 | parse_line_inlined(const ObisId &id, const char *str, const char *end) { 109 | if (id == T::id) { 110 | if (T::present()) 111 | return ParseResult().fail((const __FlashStringHelper *) DUPLICATE_FIELD, str); 112 | T::present() = true; 113 | return T::parse(str, end); 114 | } 115 | return ParsedData::parse_line_inlined(id, str, end); 116 | } 117 | 118 | template void applyEach(F &&f) { applyEach_inlined(f); } 119 | 120 | template void __attribute__((__always_inline__)) applyEach_inlined(F &&f) { 121 | T::apply(f); 122 | return ParsedData::applyEach_inlined(f); 123 | } 124 | 125 | /** 126 | * Returns true when all defined fields are present. 127 | */ 128 | bool all_present() { return all_present_inlined(); } 129 | 130 | bool all_present_inlined() { return T::present() && ParsedData::all_present_inlined(); } 131 | }; 132 | 133 | struct StringParser { 134 | static ParseResult parse_string(size_t min, size_t max, const char *str, const char *end) { 135 | ParseResult res; 136 | if (str >= end || *str != '(') 137 | return res.fail(F("Missing ("), str); 138 | 139 | const char *str_start = str + 1; // Skip ( 140 | const char *str_end = str_start; 141 | 142 | while (str_end < end && *str_end != ')') 143 | ++str_end; 144 | 145 | if (str_end == end) 146 | return res.fail(F("Missing )"), str_end); 147 | 148 | size_t len = str_end - str_start; 149 | if (len < min || len > max) 150 | return res.fail(F("Invalid string length"), str_start); 151 | 152 | concat_hack(res.result, str_start, len); 153 | 154 | return res.until(str_end + 1); // Skip ) 155 | } 156 | }; 157 | 158 | // Do not use F() for multiply-used strings (including strings used from 159 | // multiple template instantiations), that would result in multiple 160 | // instances of the string in the binary 161 | static constexpr char INVALID_NUMBER[] DSMR_PROGMEM = "Invalid number"; 162 | static constexpr char INVALID_UNIT[] DSMR_PROGMEM = "Invalid unit"; 163 | 164 | struct NumParser { 165 | static ParseResult parse(size_t max_decimals, const char *unit, const char *str, const char *end) { 166 | ParseResult res; 167 | if (str >= end || *str != '(') 168 | return res.fail(F("Missing ("), str); 169 | 170 | const char *num_start = str + 1; // Skip ( 171 | const char *num_end = num_start; 172 | 173 | uint32_t value = 0; 174 | 175 | // Parse integer part 176 | while (num_end < end && !strchr("*.)", *num_end)) { 177 | if (*num_end < '0' || *num_end > '9') 178 | return res.fail((const __FlashStringHelper *) INVALID_NUMBER, num_end); 179 | value *= 10; 180 | value += *num_end - '0'; 181 | ++num_end; 182 | } 183 | 184 | // Parse decimal part, if any 185 | if (max_decimals && num_end < end && *num_end == '.') { 186 | ++num_end; 187 | 188 | while (num_end < end && !strchr("*)", *num_end) && max_decimals--) { 189 | if (*num_end < '0' || *num_end > '9') 190 | return res.fail((const __FlashStringHelper *) INVALID_NUMBER, num_end); 191 | value *= 10; 192 | value += *num_end - '0'; 193 | ++num_end; 194 | } 195 | } 196 | 197 | // Fill in missing decimals with zeroes 198 | while (max_decimals--) 199 | value *= 10; 200 | 201 | if (unit && *unit) { 202 | if (num_end >= end || *num_end != '*') 203 | return res.fail(F("Missing unit"), num_end); 204 | const char *unit_start = ++num_end; // skip * 205 | while (num_end < end && *num_end != ')' && *unit) { 206 | if (*num_end++ != *unit++) 207 | return res.fail((const __FlashStringHelper *) INVALID_UNIT, unit_start); 208 | } 209 | if (*unit) 210 | return res.fail((const __FlashStringHelper *) INVALID_UNIT, unit_start); 211 | } 212 | 213 | if (num_end >= end || *num_end != ')') 214 | return res.fail(F("Extra data"), num_end); 215 | 216 | return res.succeed(value).until(num_end + 1); // Skip ) 217 | } 218 | }; 219 | 220 | struct ObisIdParser { 221 | static ParseResult parse(const char *str, const char *end) { 222 | // Parse a Obis ID of the form 1-2:3.4.5.6 223 | // Stops parsing on the first unrecognized character. Any unparsed 224 | // parts are set to 255. 225 | ParseResult res; 226 | ObisId &id = res.result; 227 | res.next = str; 228 | uint8_t part = 0; 229 | while (res.next < end) { 230 | char c = *res.next; 231 | 232 | if (c >= '0' && c <= '9') { 233 | uint8_t digit = c - '0'; 234 | if (id.v[part] > 25 || (id.v[part] == 25 && digit > 5)) 235 | return res.fail(F("Obis ID has number over 255"), res.next); 236 | id.v[part] = id.v[part] * 10 + digit; 237 | } else if (part == 0 && c == '-') { 238 | part++; 239 | } else if (part == 1 && c == ':') { 240 | part++; 241 | } else if (part > 1 && part < 5 && c == '.') { 242 | part++; 243 | } else { 244 | break; 245 | } 246 | ++res.next; 247 | } 248 | 249 | if (res.next == str) 250 | return res.fail(F("OBIS id Empty"), str); 251 | 252 | for (++part; part < 6; ++part) 253 | id.v[part] = 255; 254 | 255 | return res; 256 | } 257 | }; 258 | 259 | struct CrcParser { 260 | static const size_t CRC_LEN = 4; 261 | 262 | // Parse a crc value. str must point to the first of the four hex 263 | // bytes in the CRC. 264 | static ParseResult parse(const char *str, const char *end) { 265 | ParseResult res; 266 | // This should never happen with the code in this library, but 267 | // check anyway 268 | if (str + CRC_LEN > end) 269 | return res.fail(F("No checksum found"), str); 270 | 271 | // A bit of a messy way to parse the checksum, but all 272 | // integer-parse functions assume nul-termination 273 | char buf[CRC_LEN + 1]; 274 | memcpy(buf, str, CRC_LEN); 275 | buf[CRC_LEN] = '\0'; 276 | char *endp; 277 | uint16_t check = strtoul(buf, &endp, 16); 278 | 279 | // See if all four bytes formed a valid number 280 | if (endp != buf + CRC_LEN) 281 | return res.fail(F("Incomplete or malformed checksum"), str); 282 | 283 | res.next = str + CRC_LEN; 284 | return res.succeed(check); 285 | } 286 | }; 287 | 288 | struct P1Parser { 289 | /** 290 | * Parse a complete P1 telegram. The string passed should start 291 | * with '/' and run up to and including the ! and the following 292 | * four byte checksum. It's ok if the string is longer, the .next 293 | * pointer in the result will indicate the next unprocessed byte. 294 | */ 295 | template 296 | static ParseResult parse(ParsedData *data, const char *str, size_t n, bool unknown_error = false) { 297 | ParseResult res; 298 | if (!n || str[0] != '/') 299 | return res.fail(F("Data should start with /"), str); 300 | 301 | // Skip / 302 | const char *data_start = str + 1; 303 | 304 | // Look for ! that terminates the data 305 | const char *data_end = data_start; 306 | uint16_t crc = _crc16_update(0, *str); // Include the / in CRC 307 | while (data_end < str + n && *data_end != '!') { 308 | crc = _crc16_update(crc, *data_end); 309 | ++data_end; 310 | } 311 | 312 | if (data_end >= str + n) 313 | return res.fail(F("No checksum found"), data_end); 314 | 315 | crc = _crc16_update(crc, *data_end); // Include the ! in CRC 316 | 317 | ParseResult check_res = CrcParser::parse(data_end + 1, str + n); 318 | if (check_res.err) 319 | return check_res; 320 | 321 | // Check CRC 322 | if (check_res.result != crc) { 323 | Serial.println(check_res.result); 324 | Serial.println(crc); 325 | 326 | return res.fail(F("Checksum mismatch"), data_end + 1); 327 | } 328 | 329 | res = parse_data(data, data_start, data_end, unknown_error); 330 | res.next = check_res.next; 331 | return res; 332 | } 333 | 334 | /** 335 | * Parse the data part of a message. Str should point to the first 336 | * character after the leading /, end should point to the ! before the 337 | * checksum. Does not verify the checksum. 338 | */ 339 | template 340 | static ParseResult parse_data(ParsedData *data, const char *str, const char *end, 341 | bool unknown_error = false) { 342 | ParseResult res; 343 | // Split into lines and parse those 344 | const char *line_end = str, *line_start = str; 345 | 346 | // Parse ID line 347 | while (line_end < end) { 348 | if (*line_end == '\r' || *line_end == '\n') { 349 | // The first identification line looks like: 350 | // XXX5 351 | // The DSMR spec is vague on details, but in 62056-21, the X's 352 | // are a three-leter (registerd) manufacturer ID, the id 353 | // string is up to 16 chars of arbitrary characters and the 354 | // '5' is a baud rate indication. 5 apparently means 9600, 355 | // which DSMR 3.x and below used. It seems that DSMR 2.x 356 | // passed '3' here (which is mandatory for "mode D" 357 | // communication according to 62956-21), so we also allow 358 | // that. 359 | if (line_start + 3 >= line_end || (line_start[3] != '5' && line_start[3] != '3')) 360 | return res.fail(F("Invalid identification string"), line_start); 361 | // Offer it for processing using the all-ones Obis ID, which 362 | // is not otherwise valid. 363 | ParseResult tmp = data->parse_line(ObisId(255, 255, 255, 255, 255, 255), line_start, line_end); 364 | if (tmp.err) 365 | return tmp; 366 | line_start = ++line_end; 367 | break; 368 | } 369 | ++line_end; 370 | } 371 | 372 | // Parse data lines 373 | while (line_end < end) { 374 | if (*line_end == '\r' || *line_end == '\n') { 375 | ParseResult tmp = parse_line(data, line_start, line_end, unknown_error); 376 | if (tmp.err) 377 | return tmp; 378 | line_start = line_end + 1; 379 | } 380 | line_end++; 381 | } 382 | 383 | if (line_end != line_start) 384 | return res.fail(F("Last dataline not CRLF terminated"), line_end); 385 | 386 | return res; 387 | } 388 | 389 | template 390 | static ParseResult parse_line(Data *data, const char *line, const char *end, bool unknown_error) { 391 | ParseResult res; 392 | if (line == end) 393 | return res; 394 | 395 | ParseResult idres = ObisIdParser::parse(line, end); 396 | if (idres.err) 397 | return idres; 398 | 399 | ParseResult datares = data->parse_line(idres.result, idres.next, end); 400 | if (datares.err) 401 | return datares; 402 | 403 | // If datares.next didn't move at all, there was no parser for 404 | // this field, that's ok. But if it did move, but not all the way 405 | // to the end, that's an error. 406 | if (datares.next != idres.next && datares.next != end) 407 | return res.fail(F("Trailing characters on data line"), datares.next); 408 | else if (datares.next == idres.next && unknown_error) 409 | return res.fail(F("Unknown field"), line); 410 | 411 | return res.until(end); 412 | } 413 | }; 414 | 415 | } // namespace dsmr 416 | 417 | #endif // DSMR_INCLUDE_PARSER_H 418 | -------------------------------------------------------------------------------- /components/dsmr/sensor.py: -------------------------------------------------------------------------------- 1 | import esphome.codegen as cg 2 | import esphome.config_validation as cv 3 | from esphome.components import sensor 4 | from esphome.const import ( 5 | DEVICE_CLASS_CURRENT, 6 | DEVICE_CLASS_EMPTY, 7 | DEVICE_CLASS_ENERGY, 8 | DEVICE_CLASS_POWER, 9 | DEVICE_CLASS_VOLTAGE, 10 | ICON_EMPTY, 11 | LAST_RESET_TYPE_NEVER, 12 | STATE_CLASS_MEASUREMENT, 13 | STATE_CLASS_NONE, 14 | UNIT_AMPERE, 15 | UNIT_EMPTY, 16 | UNIT_VOLT, 17 | UNIT_WATT_HOURS, 18 | UNIT_WATT, 19 | ) 20 | from . import DSMR, CONF_DSMR_ID 21 | 22 | AUTO_LOAD = ["dsmr"] 23 | 24 | 25 | CONFIG_SCHEMA = cv.Schema( 26 | { 27 | cv.GenerateID(CONF_DSMR_ID): cv.use_id(DSMR), 28 | cv.Optional("energy_delivered_lux"): sensor.sensor_schema( 29 | "kWh", 30 | ICON_EMPTY, 31 | 3, 32 | DEVICE_CLASS_ENERGY, 33 | STATE_CLASS_MEASUREMENT, 34 | LAST_RESET_TYPE_NEVER, 35 | ), 36 | cv.Optional("energy_delivered_tariff1"): sensor.sensor_schema( 37 | "kWh", 38 | ICON_EMPTY, 39 | 3, 40 | DEVICE_CLASS_ENERGY, 41 | STATE_CLASS_MEASUREMENT, 42 | LAST_RESET_TYPE_NEVER 43 | ), 44 | cv.Optional("energy_delivered_tariff2"): sensor.sensor_schema( 45 | "kWh", 46 | ICON_EMPTY, 47 | 3, 48 | DEVICE_CLASS_ENERGY, 49 | STATE_CLASS_MEASUREMENT, 50 | LAST_RESET_TYPE_NEVER, 51 | ), 52 | cv.Optional("energy_returned_lux"): sensor.sensor_schema( 53 | "kWh", 54 | ICON_EMPTY, 55 | 3, 56 | DEVICE_CLASS_ENERGY, 57 | STATE_CLASS_MEASUREMENT, 58 | LAST_RESET_TYPE_NEVER, 59 | ), 60 | cv.Optional("energy_returned_tariff1"): sensor.sensor_schema( 61 | "kWh", 62 | ICON_EMPTY, 63 | 3, 64 | DEVICE_CLASS_ENERGY, 65 | STATE_CLASS_MEASUREMENT, 66 | LAST_RESET_TYPE_NEVER, 67 | ), 68 | cv.Optional("energy_returned_tariff2"): sensor.sensor_schema( 69 | "kWh", 70 | ICON_EMPTY, 71 | 3, 72 | DEVICE_CLASS_ENERGY, 73 | STATE_CLASS_MEASUREMENT, 74 | LAST_RESET_TYPE_NEVER, 75 | ), 76 | cv.Optional("total_imported_energy"): sensor.sensor_schema( 77 | "kvarh", ICON_EMPTY, 3, DEVICE_CLASS_ENERGY, STATE_CLASS_NONE 78 | ), 79 | cv.Optional("total_exported_energy"): sensor.sensor_schema( 80 | "kvarh", ICON_EMPTY, 3, DEVICE_CLASS_ENERGY, STATE_CLASS_NONE 81 | ), 82 | cv.Optional("power_delivered"): sensor.sensor_schema( 83 | UNIT_WATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT 84 | ), 85 | cv.Optional("power_returned"): sensor.sensor_schema( 86 | UNIT_WATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT 87 | ), 88 | cv.Optional("reactive_power_delivered"): sensor.sensor_schema( 89 | "kvar", ICON_EMPTY, 3, DEVICE_CLASS_ENERGY, STATE_CLASS_NONE 90 | ), 91 | cv.Optional("reactive_power_returned"): sensor.sensor_schema( 92 | "kvar", ICON_EMPTY, 3, DEVICE_CLASS_ENERGY, STATE_CLASS_MEASUREMENT 93 | ), 94 | cv.Optional("electricity_threshold"): sensor.sensor_schema( 95 | UNIT_EMPTY, ICON_EMPTY, 3, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE 96 | ), 97 | cv.Optional("electricity_switch_position"): sensor.sensor_schema( 98 | UNIT_EMPTY, ICON_EMPTY, 3, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE 99 | ), 100 | cv.Optional("electricity_failures"): sensor.sensor_schema( 101 | UNIT_EMPTY, ICON_EMPTY, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE 102 | ), 103 | cv.Optional("electricity_long_failures"): sensor.sensor_schema( 104 | UNIT_EMPTY, ICON_EMPTY, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE 105 | ), 106 | cv.Optional("electricity_sags_l1"): sensor.sensor_schema( 107 | UNIT_EMPTY, ICON_EMPTY, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE 108 | ), 109 | cv.Optional("electricity_sags_l2"): sensor.sensor_schema( 110 | UNIT_EMPTY, ICON_EMPTY, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_MEASUREMENT 111 | ), 112 | cv.Optional("electricity_sags_l3"): sensor.sensor_schema( 113 | UNIT_EMPTY, ICON_EMPTY, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE 114 | ), 115 | cv.Optional("electricity_swells_l1"): sensor.sensor_schema( 116 | UNIT_EMPTY, ICON_EMPTY, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_MEASUREMENT 117 | ), 118 | cv.Optional("electricity_swells_l2"): sensor.sensor_schema( 119 | UNIT_EMPTY, ICON_EMPTY, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE 120 | ), 121 | cv.Optional("electricity_swells_l3"): sensor.sensor_schema( 122 | UNIT_EMPTY, ICON_EMPTY, 0, DEVICE_CLASS_EMPTY, STATE_CLASS_NONE 123 | ), 124 | cv.Optional("current_l1"): sensor.sensor_schema( 125 | UNIT_AMPERE, ICON_EMPTY, 1, DEVICE_CLASS_CURRENT, STATE_CLASS_MEASUREMENT 126 | ), 127 | cv.Optional("current_l2"): sensor.sensor_schema( 128 | UNIT_AMPERE, ICON_EMPTY, 1, DEVICE_CLASS_CURRENT, STATE_CLASS_MEASUREMENT 129 | ), 130 | cv.Optional("current_l3"): sensor.sensor_schema( 131 | UNIT_AMPERE, ICON_EMPTY, 1, DEVICE_CLASS_CURRENT, STATE_CLASS_MEASUREMENT 132 | ), 133 | cv.Optional("power_delivered_l1"): sensor.sensor_schema( 134 | UNIT_WATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT 135 | ), 136 | cv.Optional("power_delivered_l2"): sensor.sensor_schema( 137 | UNIT_WATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT 138 | ), 139 | cv.Optional("power_delivered_l3"): sensor.sensor_schema( 140 | UNIT_WATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT 141 | ), 142 | cv.Optional("power_returned_l1"): sensor.sensor_schema( 143 | UNIT_WATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT 144 | ), 145 | cv.Optional("power_returned_l2"): sensor.sensor_schema( 146 | UNIT_WATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT 147 | ), 148 | cv.Optional("power_returned_l3"): sensor.sensor_schema( 149 | UNIT_WATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT 150 | ), 151 | cv.Optional("reactive_power_delivered_l1"): sensor.sensor_schema( 152 | UNIT_WATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT 153 | ), 154 | cv.Optional("reactive_power_delivered_l2"): sensor.sensor_schema( 155 | UNIT_WATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT 156 | ), 157 | cv.Optional("reactive_power_delivered_l3"): sensor.sensor_schema( 158 | UNIT_WATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT 159 | ), 160 | cv.Optional("reactive_power_returned_l1"): sensor.sensor_schema( 161 | UNIT_WATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT 162 | ), 163 | cv.Optional("reactive_power_returned_l2"): sensor.sensor_schema( 164 | UNIT_WATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT 165 | ), 166 | cv.Optional("reactive_power_returned_l3"): sensor.sensor_schema( 167 | UNIT_WATT, ICON_EMPTY, 3, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT 168 | ), 169 | cv.Optional("voltage_l1"): sensor.sensor_schema( 170 | UNIT_VOLT, ICON_EMPTY, 1, DEVICE_CLASS_VOLTAGE, STATE_CLASS_NONE 171 | ), 172 | cv.Optional("voltage_l2"): sensor.sensor_schema( 173 | UNIT_VOLT, ICON_EMPTY, 1, DEVICE_CLASS_VOLTAGE, STATE_CLASS_NONE 174 | ), 175 | cv.Optional("voltage_l3"): sensor.sensor_schema( 176 | UNIT_VOLT, ICON_EMPTY, 1, DEVICE_CLASS_VOLTAGE, STATE_CLASS_NONE 177 | ), 178 | cv.Optional("gas_delivered"): sensor.sensor_schema( 179 | "m³", 180 | ICON_EMPTY, 181 | 3, 182 | DEVICE_CLASS_EMPTY, 183 | STATE_CLASS_MEASUREMENT, 184 | LAST_RESET_TYPE_NEVER, 185 | ), 186 | cv.Optional("gas_delivered_be"): sensor.sensor_schema( 187 | "m³", 188 | ICON_EMPTY, 189 | 3, 190 | DEVICE_CLASS_EMPTY, 191 | STATE_CLASS_MEASUREMENT, 192 | LAST_RESET_TYPE_NEVER 193 | ), 194 | } 195 | ).extend(cv.COMPONENT_SCHEMA) 196 | 197 | 198 | def to_code(config): 199 | hub = yield cg.get_variable(config[CONF_DSMR_ID]) 200 | 201 | sensors = [] 202 | for key, conf in config.items(): 203 | if not isinstance(conf, dict): 204 | continue 205 | id = conf.get("id") 206 | if id and id.type == sensor.Sensor: 207 | s = yield sensor.new_sensor(conf) 208 | cg.add(getattr(hub, f"set_{key}")(s)) 209 | sensors.append(f"F({key})") 210 | 211 | cg.add_define("DSMR_SENSOR_LIST(F, sep)", cg.RawExpression(" sep ".join(sensors))) 212 | -------------------------------------------------------------------------------- /components/dsmr/text_sensor.py: -------------------------------------------------------------------------------- 1 | import esphome.codegen as cg 2 | import esphome.config_validation as cv 3 | from esphome.components import text_sensor 4 | from esphome.const import ( 5 | CONF_ID, 6 | CONF_SENSOR, 7 | ICON_EMPTY, 8 | UNIT_WATT_HOURS, 9 | ) 10 | from . import DSMR, CONF_DSMR_ID 11 | 12 | AUTO_LOAD = ["dsmr"] 13 | 14 | CONFIG_SCHEMA = cv.Schema( 15 | { 16 | cv.GenerateID(CONF_DSMR_ID): cv.use_id(DSMR), 17 | cv.Optional("identification"): text_sensor.TEXT_SENSOR_SCHEMA.extend( 18 | { 19 | cv.GenerateID(): cv.declare_id(text_sensor.TextSensor), 20 | } 21 | ), 22 | cv.Optional("p1_version"): text_sensor.TEXT_SENSOR_SCHEMA.extend( 23 | { 24 | cv.GenerateID(): cv.declare_id(text_sensor.TextSensor), 25 | } 26 | ), 27 | cv.Optional("p1_version_be"): text_sensor.TEXT_SENSOR_SCHEMA.extend( 28 | { 29 | cv.GenerateID(): cv.declare_id(text_sensor.TextSensor), 30 | } 31 | ), 32 | cv.Optional("timestamp"): text_sensor.TEXT_SENSOR_SCHEMA.extend( 33 | { 34 | cv.GenerateID(): cv.declare_id(text_sensor.TextSensor), 35 | } 36 | ), 37 | cv.Optional("electricity_tariff"): text_sensor.TEXT_SENSOR_SCHEMA.extend( 38 | { 39 | cv.GenerateID(): cv.declare_id(text_sensor.TextSensor), 40 | } 41 | ), 42 | cv.Optional("electricity_failure_log"): text_sensor.TEXT_SENSOR_SCHEMA.extend( 43 | { 44 | cv.GenerateID(): cv.declare_id(text_sensor.TextSensor), 45 | } 46 | ), 47 | cv.Optional("message_short"): text_sensor.TEXT_SENSOR_SCHEMA.extend( 48 | { 49 | cv.GenerateID(): cv.declare_id(text_sensor.TextSensor), 50 | } 51 | ), 52 | cv.Optional("message_long"): text_sensor.TEXT_SENSOR_SCHEMA.extend( 53 | { 54 | cv.GenerateID(): cv.declare_id(text_sensor.TextSensor), 55 | } 56 | ), 57 | cv.Optional("gas_equipment_id"): text_sensor.TEXT_SENSOR_SCHEMA.extend( 58 | { 59 | cv.GenerateID(): cv.declare_id(text_sensor.TextSensor), 60 | } 61 | ), 62 | cv.Optional("thermal_equipment_id"): text_sensor.TEXT_SENSOR_SCHEMA.extend( 63 | { 64 | cv.GenerateID(): cv.declare_id(text_sensor.TextSensor), 65 | } 66 | ), 67 | cv.Optional("water_equipment_id"): text_sensor.TEXT_SENSOR_SCHEMA.extend( 68 | { 69 | cv.GenerateID(): cv.declare_id(text_sensor.TextSensor), 70 | } 71 | ), 72 | cv.Optional("slave_equipment_id"): text_sensor.TEXT_SENSOR_SCHEMA.extend( 73 | { 74 | cv.GenerateID(): cv.declare_id(text_sensor.TextSensor), 75 | } 76 | ), 77 | } 78 | ).extend(cv.COMPONENT_SCHEMA) 79 | 80 | 81 | def to_code(config): 82 | hub = yield cg.get_variable(config[CONF_DSMR_ID]) 83 | 84 | text_sensors = [] 85 | for key, conf in config.items(): 86 | if not isinstance(conf, dict): 87 | continue 88 | id = conf.get("id") 89 | if id and id.type == text_sensor.TextSensor: 90 | var = cg.new_Pvariable(conf[CONF_ID]) 91 | yield text_sensor.register_text_sensor(var, conf) 92 | cg.add(getattr(hub, f"set_{key}")(var)) 93 | text_sensors.append(f"F({key})") 94 | 95 | cg.add_define( 96 | "DSMR_TEXT_SENSOR_LIST(F, sep)", cg.RawExpression(" sep ".join(text_sensors)) 97 | ) 98 | -------------------------------------------------------------------------------- /components/dsmr/util.h: -------------------------------------------------------------------------------- 1 | /** 2 | * Arduino DSMR parser. 3 | * 4 | * This software is licensed under the MIT License. 5 | * 6 | * Copyright (c) 2015 Matthijs Kooijman 7 | * 8 | * Permission is hereby granted, free of charge, to any person obtaining 9 | * a copy of this software and associated documentation files (the 10 | * "Software"), to deal in the Software without restriction, including 11 | * without limitation the rights to use, copy, modify, merge, publish, 12 | * distribute, sublicense, and/or sell copies of the Software, and to 13 | * permit persons to whom the Software is furnished to do so, subject to 14 | * the following conditions: 15 | * 16 | * The above copyright notice and this permission notice shall be 17 | * included in all copies or substantial portions of the Software. 18 | * 19 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 20 | * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 21 | * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 22 | * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS 23 | * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN 24 | * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 25 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 26 | * SOFTWARE. 27 | * 28 | * Various utility functions 29 | */ 30 | 31 | #ifndef DSMR_INCLUDE_UTIL_H 32 | #define DSMR_INCLUDE_UTIL_H 33 | 34 | #ifdef ARDUINO_ARCH_ESP8266 35 | #define DSMR_PROGMEM 36 | #else 37 | #define DSMR_PROGMEM PROGMEM 38 | #endif 39 | 40 | #include 41 | 42 | namespace dsmr { 43 | 44 | /** 45 | * Small utility to get the length of an array at compiletime. 46 | */ 47 | template 48 | inline unsigned int lengthof(const T (&)[sz]) { return sz; } 49 | 50 | // Hack until https://github.com/arduino/Arduino/pull/1936 is merged. 51 | // This appends the given number of bytes from the given C string to the 52 | // given Arduino string, without requiring a trailing NUL. 53 | // Requires that there _is_ room for nul-termination 54 | static void concat_hack(String& s, const char *append, size_t n) { 55 | // Add null termination. Inefficient, but it works... 56 | char buf[n + 1]; 57 | memcpy(buf, append, n); 58 | buf[n] = 0; 59 | s.concat(buf); 60 | } 61 | 62 | /** 63 | * The ParseResult class wraps the result of a parse function. The type 64 | * of the result is passed as a template parameter and can be void to 65 | * not return any result. 66 | * 67 | * A ParseResult can either: 68 | * - Return an error. In this case, err is set to an error message, ctx 69 | * is optionally set to where the error occurred. The result (if any) 70 | * and the next pointer are meaningless. 71 | * - Return succesfully. In this case, err and ctx are NULL, result 72 | * contains the result (if any) and next points one past the last 73 | * byte processed by the parser. 74 | * 75 | * The ParseResult class has some convenience functions: 76 | * - succeed(result): sets the result to the given value and returns 77 | * the ParseResult again. 78 | * - fail(err): Set the err member to the error message passed, 79 | * optionally sets the ctx and return the ParseResult again. 80 | * - until(next): Set the next member and return the ParseResult again. 81 | * 82 | * Furthermore, ParseResults can be implicitely converted to other 83 | * types. In this case, the error message, context and and next pointer are 84 | * conserved, the return value is reset to the default value for the 85 | * target type. 86 | * 87 | * Note that ctx points into the string being parsed, so it does not 88 | * need to be freed, lives as long as the original string and is 89 | * probably way longer that needed. 90 | */ 91 | 92 | // Superclass for ParseResult so we can specialize for void without 93 | // having to duplicate all content 94 | template 95 | struct _ParseResult { 96 | T result; 97 | 98 | P& succeed(T& result) { 99 | this->result = result; return *static_cast(this); 100 | } 101 | P& succeed(T&& result) { 102 | this->result = result; 103 | return *static_cast(this); 104 | } 105 | }; 106 | 107 | // partial specialization for void result 108 | template 109 | struct _ParseResult { 110 | }; 111 | 112 | // Actual ParseResult class 113 | template 114 | struct ParseResult : public _ParseResult, T> { 115 | const char *next = NULL; 116 | const __FlashStringHelper *err = NULL; 117 | const char *ctx = NULL; 118 | 119 | ParseResult& fail(const __FlashStringHelper *err, const char* ctx = NULL) { 120 | this->err = err; 121 | this->ctx = ctx; 122 | return *this; 123 | } 124 | ParseResult& until(const char *next) { 125 | this->next = next; 126 | return *this; 127 | } 128 | ParseResult() = default; 129 | ParseResult(const ParseResult& other) = default; 130 | 131 | template 132 | ParseResult(const ParseResult& other): next(other.next), err(other.err), ctx(other.ctx) { } 133 | 134 | /** 135 | * Returns the error, including context in a fancy multi-line format. 136 | * The start and end passed are the first and one-past-the-end 137 | * characters in the total parsed string. These are needed to properly 138 | * limit the context output. 139 | */ 140 | String fullError(const char* start, const char* end) const { 141 | String res; 142 | if (this->ctx && start && end) { 143 | // Find the entire line surrounding the context 144 | const char *line_end = this->ctx; 145 | while(line_end < end && line_end[0] != '\r' && line_end[0] != '\n') ++line_end; 146 | const char *line_start = this->ctx; 147 | while(line_start > start && line_start[-1] != '\r' && line_start[-1] != '\n') --line_start; 148 | 149 | // We can now predict the context string length, so let String allocate 150 | // memory in advance 151 | res.reserve((line_end - line_start) + 2 + (this->ctx - line_start) + 1 + 2); 152 | 153 | // Write the line 154 | concat_hack(res, line_start, line_end - line_start); 155 | res += "\r\n"; 156 | 157 | // Write a marker to point out ctx 158 | while (line_start++ < this->ctx) 159 | res += ' '; 160 | res += '^'; 161 | res += "\r\n"; 162 | } 163 | res += this->err; 164 | return res; 165 | } 166 | }; 167 | 168 | /** 169 | * An OBIS id is 6 bytes, usually noted as a-b:c.d.e.f. Here we put them 170 | * in an array for easy parsing. 171 | */ 172 | struct ObisId { 173 | uint8_t v[6]; 174 | 175 | constexpr ObisId(uint8_t a, uint8_t b = 255, uint8_t c = 255, uint8_t d = 255, uint8_t e = 255, uint8_t f = 255) 176 | : v{a, b, c, d, e, f} { }; 177 | constexpr ObisId() : v() {} // Zeroes 178 | 179 | bool operator==(const ObisId &other) const { 180 | return memcmp(&v, &other.v, sizeof(v)) == 0; 181 | } 182 | }; 183 | 184 | } // namespace dsmr 185 | 186 | #endif // DSMR_INCLUDE_UTIL_H 187 | -------------------------------------------------------------------------------- /components/empty.txt: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /dashboard_import.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | substitutions: 3 | device_name: slimmelezer 4 | device_description: "DIY P1 module to read your smart meter" 5 | 6 | esphome: 7 | name: ${device_name} 8 | comment: "${device_description}" 9 | name_add_mac_suffix: false 10 | project: 11 | name: zuidwijk.slimmelezer 12 | version: "2.0" 13 | on_boot: 14 | then: 15 | - if: 16 | condition: 17 | lambda: return id(has_key); 18 | then: 19 | - lambda: |- 20 | std::string key(id(stored_decryption_key), 32); 21 | id(dsmr_instance).set_decryption_key(key); 22 | else: 23 | - logger.log: 24 | level: info 25 | format: "Not using decryption key. If you need to set a key use Home Assistant service 'ESPHome: ${device_name}_set_dsmr_key'" 26 | 27 | esp8266: 28 | restore_from_flash: true 29 | board: d1_mini 30 | 31 | # To be able to get logs from the device via serial and api. 32 | logger: 33 | baud_rate: 0 34 | 35 | # API is a requirement of the dashboard import. 36 | api: 37 | services: 38 | service: set_dsmr_key 39 | variables: 40 | private_key: string 41 | then: 42 | - logger.log: 43 | format: Setting private key %s. Set to empty string to disable 44 | args: [private_key.c_str()] 45 | - globals.set: 46 | id: has_key 47 | value: !lambda "return private_key.length() == 32;" 48 | - lambda: |- 49 | if (private_key.length() == 32) 50 | private_key.copy(id(stored_decryption_key), 32); 51 | id(dsmr_instance).set_decryption_key(private_key); 52 | 53 | # OTA is required for Over-the-Air updating 54 | ota: 55 | 56 | # This should point to the public location of this yaml file. 57 | dashboard_import: 58 | package_import_url: github://zuidwijk/dsmr/dashboard_import.yaml 59 | 60 | wifi: 61 | # ssid: !secret wifi_ssid 62 | # password: !secret wifi_password 63 | 64 | # Enable fallback hotspot (captive portal) in case wifi connection fails 65 | ap: 66 | ssid: ${device_name} 67 | 68 | captive_portal: 69 | 70 | web_server: 71 | port: 80 72 | 73 | uart: 74 | baud_rate: 115200 75 | rx_pin: D7 76 | rx_buffer_size: 1500 77 | 78 | globals: 79 | - id: has_key 80 | type: bool 81 | restore_value: yes 82 | initial_value: "false" 83 | - id: stored_decryption_key 84 | type: char[32] 85 | restore_value: yes 86 | 87 | dsmr: 88 | id: dsmr_instance 89 | # For Luxembourg users set here your decryption key 90 | #decryption_key: !secret decryption_key // enable this when using decryption for Luxembourg; key like '00112233445566778899AABBCCDDEEFF' 91 | 92 | sensor: 93 | - platform: dsmr 94 | energy_delivered_lux: 95 | name: "Energy Consumed Luxembourg" 96 | energy_delivered_tariff1: 97 | name: "Energy Consumed Tariff 1" 98 | energy_delivered_tariff2: 99 | name: "Energy Consumed Tariff 2" 100 | energy_returned_lux: 101 | name: "Energy Produced Luxembourg" 102 | energy_returned_tariff1: 103 | name: "Energy Produced Tariff 1" 104 | energy_returned_tariff2: 105 | name: "Energy Produced Tariff 2" 106 | power_delivered: 107 | name: "Power Consumed" 108 | accuracy_decimals: 3 109 | power_returned: 110 | name: "Power Produced" 111 | accuracy_decimals: 3 112 | electricity_failures: 113 | name: "Electricity Failures" 114 | icon: mdi:alert 115 | electricity_long_failures: 116 | name: "Long Electricity Failures" 117 | icon: mdi:alert 118 | voltage_l1: 119 | name: "Voltage Phase 1" 120 | voltage_l2: 121 | name: "Voltage Phase 2" 122 | voltage_l3: 123 | name: "Voltage Phase 3" 124 | current_l1: 125 | name: "Current Phase 1" 126 | current_l2: 127 | name: "Current Phase 2" 128 | current_l3: 129 | name: "Current Phase 3" 130 | power_delivered_l1: 131 | name: "Power Consumed Phase 1" 132 | accuracy_decimals: 3 133 | power_delivered_l2: 134 | name: "Power Consumed Phase 2" 135 | accuracy_decimals: 3 136 | power_delivered_l3: 137 | name: "Power Consumed Phase 3" 138 | accuracy_decimals: 3 139 | power_returned_l1: 140 | name: "Power Produced Phase 1" 141 | accuracy_decimals: 3 142 | power_returned_l2: 143 | name: "Power Produced Phase 2" 144 | accuracy_decimals: 3 145 | power_returned_l3: 146 | name: "Power Produced Phase 3" 147 | accuracy_decimals: 3 148 | gas_delivered: 149 | name: "Gas Consumed" 150 | gas_delivered_be: 151 | name: "Gas Consumed Belgium" 152 | - platform: uptime 153 | name: "SlimmeLezer Uptime" 154 | - platform: wifi_signal 155 | name: "SlimmeLezer Wi-Fi Signal" 156 | update_interval: 60s 157 | 158 | text_sensor: 159 | - platform: dsmr 160 | identification: 161 | name: "DSMR Identification" 162 | p1_version: 163 | name: "DSMR Version" 164 | p1_version_be: 165 | name: "DSMR Version Belgium" 166 | - platform: wifi_info 167 | ip_address: 168 | name: "SlimmeLezer IP Address" 169 | ssid: 170 | name: "SlimmeLezer Wi-Fi SSID" 171 | bssid: 172 | name: "SlimmeLezer Wi-Fi BSSID" 173 | - platform: version 174 | name: "ESPHome Version" 175 | hide_timestamp: true 176 | -------------------------------------------------------------------------------- /pre-compiled/slimmelezer-v2022.10.1.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zuidwijk/dsmr/33accb0fbb4d52ad8ea1bbde2332ac561a276285/pre-compiled/slimmelezer-v2022.10.1.bin -------------------------------------------------------------------------------- /pre-compiled/slimmelezer-v2022.11.3.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zuidwijk/dsmr/33accb0fbb4d52ad8ea1bbde2332ac561a276285/pre-compiled/slimmelezer-v2022.11.3.bin -------------------------------------------------------------------------------- /pre-compiled/slimmelezer-v2022.11.4.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zuidwijk/dsmr/33accb0fbb4d52ad8ea1bbde2332ac561a276285/pre-compiled/slimmelezer-v2022.11.4.bin -------------------------------------------------------------------------------- /pre-compiled/slimmelezer-v2022.11.5.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zuidwijk/dsmr/33accb0fbb4d52ad8ea1bbde2332ac561a276285/pre-compiled/slimmelezer-v2022.11.5.bin -------------------------------------------------------------------------------- /pre-compiled/slimmelezer-v2022.12.1.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zuidwijk/dsmr/33accb0fbb4d52ad8ea1bbde2332ac561a276285/pre-compiled/slimmelezer-v2022.12.1.bin -------------------------------------------------------------------------------- /pre-compiled/slimmelezer-v2022.12.3.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zuidwijk/dsmr/33accb0fbb4d52ad8ea1bbde2332ac561a276285/pre-compiled/slimmelezer-v2022.12.3.bin -------------------------------------------------------------------------------- /pre-compiled/slimmelezer-v2022.12.8.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zuidwijk/dsmr/33accb0fbb4d52ad8ea1bbde2332ac561a276285/pre-compiled/slimmelezer-v2022.12.8.bin -------------------------------------------------------------------------------- /sl32plus.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | substitutions: 3 | device_name: sl32plus 4 | device_description: "DIY P1 module to read your smart meter" 5 | 6 | esp32: 7 | board: esp32dev 8 | 9 | esphome: 10 | name: ${device_name} 11 | comment: "${device_description}" 12 | name_add_mac_suffix: false 13 | project: 14 | name: zuidwijk.slimmelezer 15 | version: "2.0" 16 | on_boot: 17 | then: 18 | - if: 19 | condition: 20 | lambda: return id(has_key); 21 | then: 22 | - lambda: |- 23 | std::string key(id(stored_decryption_key), 32); 24 | id(dsmr_instance).set_decryption_key(key); 25 | else: 26 | - logger.log: 27 | level: info 28 | format: "Not using decryption key. If you need to set a key use Home Assistant service 'ESPHome: ${device_name}_set_dsmr_key'" 29 | 30 | wifi: 31 | ap: 32 | ssid: ${device_name} 33 | 34 | captive_portal: 35 | 36 | esp32_ble_tracker: 37 | scan_parameters: 38 | interval: 1100ms 39 | window: 1100ms 40 | active: true 41 | 42 | bluetooth_proxy: 43 | active: true 44 | 45 | improv_serial: 46 | 47 | # Enable logging 48 | logger: 49 | baud_rate: 115200 50 | 51 | # Enable Home Assistant API 52 | api: 53 | services: 54 | service: set_dsmr_key 55 | variables: 56 | private_key: string 57 | then: 58 | - logger.log: 59 | format: Setting private key %s. Set to empty string to disable 60 | args: [private_key.c_str()] 61 | - globals.set: 62 | id: has_key 63 | value: !lambda "return private_key.length() == 32;" 64 | - lambda: |- 65 | if (private_key.length() == 32) 66 | private_key.copy(id(stored_decryption_key), 32); 67 | id(dsmr_instance).set_decryption_key(private_key); 68 | 69 | ota: 70 | platform: esphome 71 | 72 | dashboard_import: 73 | package_import_url: github://zuidwijk/dsmr/sl32plus.yaml@main 74 | 75 | web_server: 76 | port: 80 77 | 78 | uart: 79 | id: uart_dsmr 80 | baud_rate: 115200 81 | rx_pin: GPIO16 82 | rx_buffer_size: 1700 83 | 84 | globals: 85 | - id: has_key 86 | type: bool 87 | restore_value: yes 88 | initial_value: "false" 89 | - id: stored_decryption_key 90 | type: char[32] 91 | restore_value: yes 92 | 93 | dsmr: 94 | uart_id: uart_dsmr 95 | id: dsmr_instance 96 | max_telegram_length: 1700 97 | # For Luxembourg users set here your decryption key 98 | #decryption_key: !secret decryption_key // enable this when using decryption for Luxembourg; key like '00112233445566778899AABBCCDDEEFF' 99 | 100 | sensor: 101 | - platform: dsmr 102 | energy_delivered_lux: 103 | name: "Energy Consumed Luxembourg" 104 | energy_delivered_tariff1: 105 | name: "Energy Consumed Tariff 1" 106 | energy_delivered_tariff2: 107 | name: "Energy Consumed Tariff 2" 108 | energy_returned_lux: 109 | name: "Energy Produced Luxembourg" 110 | energy_returned_tariff1: 111 | name: "Energy Produced Tariff 1" 112 | energy_returned_tariff2: 113 | name: "Energy Produced Tariff 2" 114 | power_delivered: 115 | name: "Power Consumed" 116 | accuracy_decimals: 3 117 | power_returned: 118 | name: "Power Produced" 119 | accuracy_decimals: 3 120 | electricity_failures: 121 | name: "Electricity Failures" 122 | icon: mdi:alert 123 | electricity_long_failures: 124 | name: "Long Electricity Failures" 125 | icon: mdi:alert 126 | voltage_l1: 127 | name: "Voltage Phase 1" 128 | voltage_l2: 129 | name: "Voltage Phase 2" 130 | voltage_l3: 131 | name: "Voltage Phase 3" 132 | current_l1: 133 | name: "Current Phase 1" 134 | current_l2: 135 | name: "Current Phase 2" 136 | current_l3: 137 | name: "Current Phase 3" 138 | power_delivered_l1: 139 | name: "Power Consumed Phase 1" 140 | accuracy_decimals: 3 141 | power_delivered_l2: 142 | name: "Power Consumed Phase 2" 143 | accuracy_decimals: 3 144 | power_delivered_l3: 145 | name: "Power Consumed Phase 3" 146 | accuracy_decimals: 3 147 | power_returned_l1: 148 | name: "Power Produced Phase 1" 149 | accuracy_decimals: 3 150 | power_returned_l2: 151 | name: "Power Produced Phase 2" 152 | accuracy_decimals: 3 153 | power_returned_l3: 154 | name: "Power Produced Phase 3" 155 | accuracy_decimals: 3 156 | gas_delivered: 157 | name: "Gas Consumed" 158 | gas_delivered_be: 159 | name: "Gas Consumed Belgium" 160 | - platform: uptime 161 | name: "SlimmeLezer Uptime" 162 | - platform: wifi_signal 163 | name: "SlimmeLezer Wi-Fi Signal" 164 | update_interval: 60s 165 | 166 | text_sensor: 167 | - platform: dsmr 168 | identification: 169 | name: "DSMR Identification" 170 | p1_version: 171 | name: "DSMR Version" 172 | p1_version_be: 173 | name: "DSMR Version Belgium" 174 | - platform: wifi_info 175 | ip_address: 176 | name: "SlimmeLezer IP Address" 177 | ssid: 178 | name: "SlimmeLezer Wi-Fi SSID" 179 | bssid: 180 | name: "SlimmeLezer Wi-Fi BSSID" 181 | - platform: version 182 | name: "ESPHome Version" 183 | hide_timestamp: true 184 | -------------------------------------------------------------------------------- /slimmelezer-be.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | substitutions: 3 | device_name: slimmelezer 4 | 5 | esphome: 6 | name: ${device_name} 7 | platform: ESP8266 8 | esp8266_restore_from_flash: true 9 | board: d1_mini 10 | name_add_mac_suffix: false 11 | project: 12 | name: zuidwijk.slimmelezer 13 | version: "1.2" 14 | 15 | wifi: 16 | # remove leading '#' and fill in your wifi details 17 | # ssid: !secret wifi_ssid 18 | # password: !secret wifi_password 19 | 20 | # Enable fallback hotspot (captive portal) in case wifi connection fails 21 | ap: 22 | ssid: ${device_name} 23 | 24 | captive_portal: 25 | 26 | # Enable logging 27 | logger: 28 | baud_rate: 0 29 | 30 | # Enable Home Assistant API 31 | api: 32 | 33 | ota: 34 | 35 | dashboard_import: 36 | package_import_url: github://zuidwijk/dsmr/slimmelezer-be.yaml@main 37 | import_full_config: true 38 | 39 | web_server: 40 | port: 80 41 | 42 | uart: 43 | baud_rate: 115200 44 | rx_pin: D7 45 | rx_buffer_size: 1700 46 | 47 | dsmr: 48 | id: dsmr_instance 49 | max_telegram_length: 1700 50 | 51 | sensor: 52 | - platform: dsmr 53 | energy_delivered_tariff1: 54 | name: "Energy Consumed Tariff 1" 55 | energy_delivered_tariff2: 56 | name: "Energy Consumed Tariff 2" 57 | energy_returned_tariff1: 58 | name: "Energy Produced Tariff 1" 59 | energy_returned_tariff2: 60 | name: "Energy Produced Tariff 2" 61 | power_delivered: 62 | name: "Power Consumed" 63 | accuracy_decimals: 3 64 | power_returned: 65 | name: "Power Produced" 66 | accuracy_decimals: 3 67 | voltage_l1: 68 | name: "Voltage Phase 1" 69 | voltage_l2: 70 | name: "Voltage Phase 2" 71 | voltage_l3: 72 | name: "Voltage Phase 3" 73 | current_l1: 74 | name: "Current Phase 1" 75 | current_l2: 76 | name: "Current Phase 2" 77 | current_l3: 78 | name: "Current Phase 3" 79 | power_delivered_l1: 80 | name: "Power Consumed Phase 1" 81 | accuracy_decimals: 3 82 | power_delivered_l2: 83 | name: "Power Consumed Phase 2" 84 | accuracy_decimals: 3 85 | power_delivered_l3: 86 | name: "Power Consumed Phase 3" 87 | accuracy_decimals: 3 88 | power_returned_l1: 89 | name: "Power Produced Phase 1" 90 | accuracy_decimals: 3 91 | power_returned_l2: 92 | name: "Power Produced Phase 2" 93 | accuracy_decimals: 3 94 | power_returned_l3: 95 | name: "Power Produced Phase 3" 96 | accuracy_decimals: 3 97 | gas_delivered_be: 98 | name: "Gas Consumed Belgium" 99 | active_energy_import_current_average_demand: 100 | name: "Peak Current Average Quarterly Demand" 101 | icon: mdi:chart-sankey 102 | active_energy_import_maximum_demand_running_month: 103 | name: "Peak Month Maximum Quarterly Demand" 104 | icon: mdi:chart-sankey 105 | active_energy_import_maximum_demand_last_13_months: 106 | name: "Peak 13 Month Maximum Quarterly Demand" 107 | icon: mdi:chart-sankey 108 | - platform: uptime 109 | name: "${device_name} Uptime" 110 | - platform: wifi_signal 111 | name: "${device_name} Wi-Fi Signal" 112 | update_interval: 60s 113 | 114 | text_sensor: 115 | - platform: dsmr 116 | identification: 117 | name: "DSMR Identification" 118 | p1_version_be: 119 | name: "DSMR Version Belgium" 120 | electricity_tariff: 121 | name: "DSMR Tariff" 122 | message_long: 123 | name: "DSMR Message Long" 124 | icon: mdi:message-text-outline 125 | - platform: wifi_info 126 | ip_address: 127 | name: "SlimmeLezer IP Address" 128 | ssid: 129 | name: "SlimmeLezer Wi-Fi SSID" 130 | bssid: 131 | name: "SlimmeLezer Wi-Fi BSSID" 132 | - platform: version 133 | name: "ESPHome Version" 134 | hide_timestamp: true 135 | -------------------------------------------------------------------------------- /slimmelezer-test.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | substitutions: 3 | device_name: slimmelezer-test 4 | 5 | esphome: 6 | name: ${device_name} 7 | platform: ESP8266 8 | esp8266_restore_from_flash: true 9 | board: d1_mini 10 | name_add_mac_suffix: false 11 | project: 12 | name: zuidwijk.slimmelezer 13 | version: "2.0" 14 | on_boot: 15 | then: 16 | - if: 17 | condition: 18 | lambda: return id(has_key); 19 | then: 20 | - lambda: |- 21 | std::string key(id(stored_decryption_key), 32); 22 | id(dsmr_instance).set_decryption_key(key); 23 | else: 24 | - logger.log: 25 | level: info 26 | format: "Not using decryption key. If you need to set a key use Home Assistant service 'ESPHome: ${device_name}_set_dsmr_key'" 27 | 28 | wifi: 29 | ap: 30 | ssid: ${device_name} 31 | 32 | captive_portal: 33 | 34 | # Enable logging 35 | logger: 36 | baud_rate: 0 37 | 38 | # Enable Home Assistant API 39 | api: 40 | services: 41 | service: set_dsmr_key 42 | variables: 43 | private_key: string 44 | then: 45 | - logger.log: 46 | format: Setting private key %s. Set to empty string to disable 47 | args: [private_key.c_str()] 48 | - globals.set: 49 | id: has_key 50 | value: !lambda "return private_key.length() == 32;" 51 | - lambda: |- 52 | if (private_key.length() == 32) 53 | private_key.copy(id(stored_decryption_key), 32); 54 | id(dsmr_instance).set_decryption_key(private_key); 55 | 56 | ota: 57 | 58 | dashboard_import: 59 | package_import_url: github://zuidwijk/dsmr/slimmelezer-test.yaml@main 60 | 61 | web_server: 62 | port: 80 63 | 64 | uart: 65 | baud_rate: 115200 66 | rx_pin: D7 67 | rx_buffer_size: 1700 68 | 69 | globals: 70 | - id: has_key 71 | type: bool 72 | restore_value: yes 73 | initial_value: "false" 74 | - id: stored_decryption_key 75 | type: char[32] 76 | restore_value: yes 77 | 78 | dsmr: 79 | id: dsmr_instance 80 | max_telegram_length: 1700 81 | # For Luxembourg users set here your decryption key 82 | #decryption_key: !secret decryption_key // enable this when using decryption for Luxembourg; key like '00112233445566778899AABBCCDDEEFF' 83 | 84 | sensor: 85 | - platform: dsmr 86 | energy_delivered_lux: 87 | name: "Energy Consumed Luxembourg" 88 | energy_delivered_tariff1: 89 | name: "Energy Consumed Tariff 1" 90 | energy_delivered_tariff2: 91 | name: "Energy Consumed Tariff 2" 92 | energy_returned_lux: 93 | name: "Energy Produced Luxembourg" 94 | energy_returned_tariff1: 95 | name: "Energy Produced Tariff 1" 96 | energy_returned_tariff2: 97 | name: "Energy Produced Tariff 2" 98 | power_delivered: 99 | name: "Power Consumed" 100 | accuracy_decimals: 3 101 | power_returned: 102 | name: "Power Produced" 103 | accuracy_decimals: 3 104 | electricity_failures: 105 | name: "Electricity Failures" 106 | icon: mdi:alert 107 | electricity_long_failures: 108 | name: "Long Electricity Failures" 109 | icon: mdi:alert 110 | voltage_l1: 111 | name: "Voltage Phase 1" 112 | voltage_l2: 113 | name: "Voltage Phase 2" 114 | voltage_l3: 115 | name: "Voltage Phase 3" 116 | current_l1: 117 | name: "Current Phase 1" 118 | current_l2: 119 | name: "Current Phase 2" 120 | current_l3: 121 | name: "Current Phase 3" 122 | power_delivered_l1: 123 | name: "Power Consumed Phase 1" 124 | accuracy_decimals: 3 125 | power_delivered_l2: 126 | name: "Power Consumed Phase 2" 127 | accuracy_decimals: 3 128 | power_delivered_l3: 129 | name: "Power Consumed Phase 3" 130 | accuracy_decimals: 3 131 | power_returned_l1: 132 | name: "Power Produced Phase 1" 133 | accuracy_decimals: 3 134 | power_returned_l2: 135 | name: "Power Produced Phase 2" 136 | accuracy_decimals: 3 137 | power_returned_l3: 138 | name: "Power Produced Phase 3" 139 | accuracy_decimals: 3 140 | gas_delivered: 141 | name: "Gas Consumed" 142 | gas_delivered_be: 143 | name: "Gas Consumed Belgium" 144 | - platform: uptime 145 | name: "SlimmeLezer Uptime" 146 | - platform: wifi_signal 147 | name: "SlimmeLezer Wi-Fi Signal" 148 | update_interval: 60s 149 | 150 | text_sensor: 151 | - platform: dsmr 152 | identification: 153 | name: "DSMR Identification" 154 | p1_version: 155 | name: "DSMR Version" 156 | p1_version_be: 157 | name: "DSMR Version Belgium" 158 | - platform: wifi_info 159 | ip_address: 160 | name: "SlimmeLezer IP Address" 161 | ssid: 162 | name: "SlimmeLezer Wi-Fi SSID" 163 | bssid: 164 | name: "SlimmeLezer Wi-Fi BSSID" 165 | - platform: version 166 | name: "ESPHome Version" 167 | hide_timestamp: true 168 | -------------------------------------------------------------------------------- /slimmelezer.old.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | substitutions: 3 | device_name: slimmelezer 4 | 5 | esphome: 6 | name: ${device_name} 7 | platform: ESP8266 8 | esp8266_restore_from_flash: true 9 | board: d1_mini 10 | name_add_mac_suffix: false 11 | project: 12 | name: zuidwijk.slimmelezer 13 | version: "1.2" 14 | on_boot: 15 | then: 16 | - if: 17 | condition: 18 | lambda: return id(has_key); 19 | then: 20 | - lambda: |- 21 | std::string key(id(stored_decryption_key), 32); 22 | id(dsmr_instance).set_decryption_key(key); 23 | else: 24 | - logger.log: 25 | level: info 26 | format: "Not using decryption key. If you need to set a key use Home Assistant service 'ESPHome: ${device_name}_set_dsmr_key'" 27 | 28 | wifi: 29 | # remove leading '#' and fill in your wifi details 30 | # ssid: !secret wifi_ssid 31 | # password: !secret wifi_password 32 | 33 | # Enable fallback hotspot (captive portal) in case wifi connection fails 34 | ap: 35 | ssid: ${device_name} 36 | 37 | captive_portal: 38 | 39 | # Enable logging 40 | logger: 41 | baud_rate: 0 42 | 43 | # Enable Home Assistant API 44 | api: 45 | services: 46 | service: set_dsmr_key 47 | variables: 48 | private_key: string 49 | then: 50 | - logger.log: 51 | format: Setting private key %s. Set to empty string to disable 52 | args: [private_key.c_str()] 53 | - globals.set: 54 | id: has_key 55 | value: !lambda "return private_key.length() == 32;" 56 | - lambda: |- 57 | if (private_key.length() == 32) 58 | private_key.copy(id(stored_decryption_key), 32); 59 | id(dsmr_instance).set_decryption_key(private_key); 60 | 61 | ota: 62 | platform: esphome 63 | 64 | dashboard_import: 65 | package_import_url: github://zuidwijk/dsmr/slimmelezer.yaml@main 66 | import_full_config: true 67 | 68 | web_server: 69 | port: 80 70 | 71 | uart: 72 | baud_rate: 115200 73 | rx_pin: D7 74 | rx_buffer_size: 1700 75 | 76 | globals: 77 | - id: has_key 78 | type: bool 79 | restore_value: yes 80 | initial_value: "false" 81 | - id: stored_decryption_key 82 | type: char[32] 83 | restore_value: yes 84 | 85 | dsmr: 86 | id: dsmr_instance 87 | max_telegram_length: 1700 88 | # For Luxembourg users set here your decryption key 89 | #decryption_key: !secret decryption_key // enable this when using decryption for Luxembourg; key like '00112233445566778899AABBCCDDEEFF' 90 | 91 | sensor: 92 | - platform: dsmr 93 | energy_delivered_lux: 94 | name: "Energy Consumed Luxembourg" 95 | energy_delivered_tariff1: 96 | name: "Energy Consumed Tariff 1" 97 | energy_delivered_tariff2: 98 | name: "Energy Consumed Tariff 2" 99 | energy_returned_lux: 100 | name: "Energy Produced Luxembourg" 101 | energy_returned_tariff1: 102 | name: "Energy Produced Tariff 1" 103 | energy_returned_tariff2: 104 | name: "Energy Produced Tariff 2" 105 | power_delivered: 106 | name: "Power Consumed" 107 | accuracy_decimals: 3 108 | power_returned: 109 | name: "Power Produced" 110 | accuracy_decimals: 3 111 | electricity_failures: 112 | name: "Electricity Failures" 113 | icon: mdi:alert 114 | electricity_long_failures: 115 | name: "Long Electricity Failures" 116 | icon: mdi:alert 117 | voltage_l1: 118 | name: "Voltage Phase 1" 119 | voltage_l2: 120 | name: "Voltage Phase 2" 121 | voltage_l3: 122 | name: "Voltage Phase 3" 123 | current_l1: 124 | name: "Current Phase 1" 125 | current_l2: 126 | name: "Current Phase 2" 127 | current_l3: 128 | name: "Current Phase 3" 129 | power_delivered_l1: 130 | name: "Power Consumed Phase 1" 131 | accuracy_decimals: 3 132 | power_delivered_l2: 133 | name: "Power Consumed Phase 2" 134 | accuracy_decimals: 3 135 | power_delivered_l3: 136 | name: "Power Consumed Phase 3" 137 | accuracy_decimals: 3 138 | power_returned_l1: 139 | name: "Power Produced Phase 1" 140 | accuracy_decimals: 3 141 | power_returned_l2: 142 | name: "Power Produced Phase 2" 143 | accuracy_decimals: 3 144 | power_returned_l3: 145 | name: "Power Produced Phase 3" 146 | accuracy_decimals: 3 147 | gas_delivered: 148 | name: "Gas Consumed" 149 | gas_delivered_be: 150 | name: "Gas Consumed Belgium" 151 | - platform: uptime 152 | name: "SlimmeLezer Uptime" 153 | - platform: wifi_signal 154 | name: "SlimmeLezer Wi-Fi Signal" 155 | update_interval: 60s 156 | 157 | text_sensor: 158 | - platform: dsmr 159 | identification: 160 | name: "DSMR Identification" 161 | p1_version: 162 | name: "DSMR Version" 163 | p1_version_be: 164 | name: "DSMR Version Belgium" 165 | - platform: wifi_info 166 | ip_address: 167 | name: "SlimmeLezer IP Address" 168 | ssid: 169 | name: "SlimmeLezer Wi-Fi SSID" 170 | bssid: 171 | name: "SlimmeLezer Wi-Fi BSSID" 172 | - platform: version 173 | name: "ESPHome Version" 174 | hide_timestamp: true 175 | -------------------------------------------------------------------------------- /slimmelezer.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | substitutions: 3 | device_name: slimmelezer 4 | 5 | esphome: 6 | name: ${device_name} 7 | name_add_mac_suffix: false 8 | project: 9 | name: zuidwijk.slimmelezer 10 | version: "2.0" 11 | on_boot: 12 | then: 13 | - if: 14 | condition: 15 | lambda: return id(has_key); 16 | then: 17 | - lambda: |- 18 | std::string key(id(stored_decryption_key), 32); 19 | id(dsmr_instance).set_decryption_key(key); 20 | else: 21 | - logger.log: 22 | level: info 23 | format: "Not using decryption key. If you need to set a key use Home Assistant service 'ESPHome: ${device_name}_set_dsmr_key'" 24 | 25 | esp8266: 26 | restore_from_flash: true 27 | board: d1_mini 28 | 29 | wifi: 30 | # remove leading '#' and fill in your wifi details !! 31 | # ssid: !secret wifi_ssid 32 | # password: !secret wifi_password 33 | 34 | # Enable fallback hotspot (captive portal) in case wifi connection fails 35 | ap: 36 | ssid: ${device_name} 37 | 38 | # Powersaving for brownout due to 250mA restriction P1 39 | output_power: 14dB 40 | 41 | captive_portal: 42 | 43 | # Enable logging 44 | logger: 45 | baud_rate: 0 46 | # logs: 47 | # component: ERROR 48 | 49 | # Enable Home Assistant API 50 | api: 51 | services: 52 | service: set_dsmr_key 53 | variables: 54 | private_key: string 55 | then: 56 | - logger.log: 57 | format: Setting private key %s. Set to empty string to disable 58 | args: [private_key.c_str()] 59 | - globals.set: 60 | id: has_key 61 | value: !lambda "return private_key.length() == 32;" 62 | - lambda: |- 63 | if (private_key.length() == 32) 64 | private_key.copy(id(stored_decryption_key), 32); 65 | id(dsmr_instance).set_decryption_key(private_key); 66 | 67 | ota: 68 | platform: esphome 69 | 70 | dashboard_import: 71 | package_import_url: github://zuidwijk/dsmr/slimmelezer.yaml@main 72 | import_full_config: true 73 | 74 | web_server: 75 | port: 80 76 | 77 | uart: 78 | baud_rate: 115200 79 | rx_pin: D7 80 | rx_buffer_size: 1700 81 | 82 | globals: 83 | - id: has_key 84 | type: bool 85 | restore_value: yes 86 | initial_value: "false" 87 | - id: stored_decryption_key 88 | type: char[32] 89 | restore_value: yes 90 | 91 | dsmr: 92 | id: dsmr_instance 93 | max_telegram_length: 1700 94 | # For Luxembourg users set here your decryption key 95 | #decryption_key: !secret decryption_key // enable this when using decryption for Luxembourg; key like '00112233445566778899AABBCCDDEEFF' 96 | 97 | sensor: 98 | - platform: dsmr 99 | energy_delivered_lux: 100 | name: "Energy Consumed Luxembourg" 101 | energy_delivered_tariff1: 102 | name: "Energy Consumed Tariff 1" 103 | energy_delivered_tariff2: 104 | name: "Energy Consumed Tariff 2" 105 | energy_returned_lux: 106 | name: "Energy Produced Luxembourg" 107 | energy_returned_tariff1: 108 | name: "Energy Produced Tariff 1" 109 | energy_returned_tariff2: 110 | name: "Energy Produced Tariff 2" 111 | power_delivered: 112 | name: "Power Consumed" 113 | accuracy_decimals: 3 114 | power_returned: 115 | name: "Power Produced" 116 | accuracy_decimals: 3 117 | electricity_failures: 118 | name: "Electricity Failures" 119 | icon: mdi:alert 120 | electricity_long_failures: 121 | name: "Long Electricity Failures" 122 | icon: mdi:alert 123 | voltage_l1: 124 | name: "Voltage Phase 1" 125 | voltage_l2: 126 | name: "Voltage Phase 2" 127 | voltage_l3: 128 | name: "Voltage Phase 3" 129 | current_l1: 130 | name: "Current Phase 1" 131 | current_l2: 132 | name: "Current Phase 2" 133 | current_l3: 134 | name: "Current Phase 3" 135 | power_delivered_l1: 136 | name: "Power Consumed Phase 1" 137 | accuracy_decimals: 3 138 | power_delivered_l2: 139 | name: "Power Consumed Phase 2" 140 | accuracy_decimals: 3 141 | power_delivered_l3: 142 | name: "Power Consumed Phase 3" 143 | accuracy_decimals: 3 144 | power_returned_l1: 145 | name: "Power Produced Phase 1" 146 | accuracy_decimals: 3 147 | power_returned_l2: 148 | name: "Power Produced Phase 2" 149 | accuracy_decimals: 3 150 | power_returned_l3: 151 | name: "Power Produced Phase 3" 152 | accuracy_decimals: 3 153 | gas_delivered: 154 | name: "Gas Consumed" 155 | gas_delivered_be: 156 | name: "Gas Consumed Belgium" 157 | - platform: uptime 158 | name: "SlimmeLezer Uptime" 159 | - platform: wifi_signal 160 | name: "SlimmeLezer Wi-Fi Signal" 161 | update_interval: 60s 162 | 163 | text_sensor: 164 | - platform: dsmr 165 | identification: 166 | name: "DSMR Identification" 167 | p1_version: 168 | name: "DSMR Version" 169 | p1_version_be: 170 | name: "DSMR Version Belgium" 171 | # timestamp: 172 | # name: "Timestamp" 173 | - platform: wifi_info 174 | ip_address: 175 | name: "SlimmeLezer IP Address" 176 | ssid: 177 | name: "SlimmeLezer Wi-Fi SSID" 178 | bssid: 179 | name: "SlimmeLezer Wi-Fi BSSID" 180 | - platform: version 181 | name: "ESPHome Version" 182 | hide_timestamp: true 183 | --------------------------------------------------------------------------------