Last active
August 25, 2026 21:11
-
-
Save pavax/a6a425ee41beb4a6cd7770091b192ba5 to your computer and use it in GitHub Desktop.
Kamstrup Omnipower Smart-Meter Readings using EspHome and Esp-now
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # ESPHome for a Lolin S3 Mini on the HAN port of a Kamstrup Omnipower. | |
| # Powered from the meter (HAN); a supercapacitor buffers energy — no battery. | |
| # Reads DLMS telegrams (power/energy import+export) and sends them via | |
| # ESP-NOW to the hub. Radio stays off until it is time to transmit. | |
| # Capacitor check before metering: voltage too low → short sleep to recharge. | |
| # Otherwise wait for a telegram (or timeout), send, then deep sleep until the next slot. | |
| # | |
| # LED: brief blue if capacitor is OK, red if voltage is too low, green after telegram, off in sleep. | |
| # Flow: radio off → capacitor check → DLMS → ESP-NOW → sleep. | |
| # Capacitor < ${min_capacitor_voltage}: sleep ${charging_sleep_duration_seconds}s immediately (capacitor charges, no radio). | |
| # Timeout: after ${run_duration_seconds} send whatever is available and sleep. | |
| # HAN transmits every 10 s. Sleep of exactly 60 s plus finish overhead wakes just after | |
| # the slot, then wait ~10 s. So sleep a minute minus 3–5 s (55–57 s). 58–59 s plus | |
| # frame/boot (~2 s) slips past T+60. | |
| substitutions: | |
| name: kamstrup-omnipower-02 | |
| friendly_name: "Kamstrup Omnipower 02" | |
| run_duration_seconds: "15" | |
| sleep_duration_minutes: "1" | |
| espnow_channel: "1" | |
| min_capacitor_voltage: "3.80" | |
| charging_sleep_duration_seconds: "90" | |
| led_brightness: "50%" | |
| obis_power_import: "1.1.1.7.0.255" | |
| obis_power_export: "1.1.2.7.0.255" | |
| obis_energy_import: "1.1.1.8.0.255" | |
| obis_energy_export: "1.1.2.8.0.255" | |
| esphome: | |
| name: ${name} | |
| friendly_name: ${friendly_name} | |
| includes: | |
| - common/rtc_wakeup_count.h | |
| on_boot: | |
| - priority: 300 | |
| then: | |
| - lambda: |- | |
| id(wakeup_cycle_count) = bump_wakeup_count(); | |
| id(wakeup_count).publish_state(id(wakeup_cycle_count)); | |
| ESP_LOGI("boot", "Wake-up cycle %d (reset %d)", id(wakeup_cycle_count), (int) esp_reset_reason()); | |
| - script.execute: sleep_if_capacitor_low | |
| - script.wait: sleep_if_capacitor_low | |
| - light.turn_on: | |
| id: onboard_rgb | |
| red: 0% | |
| green: 0% | |
| blue: 100% | |
| brightness: ${led_brightness} | |
| transition_length: 0s | |
| - delay: 200ms | |
| - light.turn_off: | |
| id: onboard_rgb | |
| transition_length: 0s | |
| - lambda: "id(boot_ready) = true;" | |
| esp32: | |
| board: lolin_s3_mini | |
| cpu_frequency: 80MHz | |
| framework: | |
| type: esp-idf | |
| logger: | |
| espnow: | |
| id: espnow_id | |
| channel: ${espnow_channel} | |
| auto_add_peer: false | |
| enable_on_boot: false # radio stays off until finish_and_sleep | |
| peers: | |
| - !secret espnow_hub_mac | |
| packet_transport: | |
| - platform: espnow | |
| id: transport | |
| espnow_id: espnow_id | |
| peer_address: !secret espnow_hub_mac | |
| encryption: !secret espnow_transport_key | |
| update_interval: never | |
| sensors: | |
| - power_import | |
| - power_export | |
| - energy_import | |
| - energy_export | |
| - battery_voltage | |
| - uptime_seconds | |
| - wakeup_count | |
| globals: | |
| - id: wakeup_cycle_count | |
| type: int | |
| restore_value: no | |
| initial_value: "0" | |
| # finish_and_sleep only after capacitor check and blue LED. | |
| - id: boot_ready | |
| type: bool | |
| restore_value: no | |
| initial_value: "false" | |
| deep_sleep: | |
| id: deep_sleep_control | |
| sleep_duration: ${sleep_duration_minutes}min | |
| interval: | |
| - interval: 1s | |
| id: force_sleep_interval | |
| then: | |
| - if: | |
| condition: | |
| and: | |
| - not: | |
| script.is_running: finish_and_sleep | |
| - lambda: "return millis() / 1000 >= ${run_duration_seconds};" | |
| then: | |
| - logger.log: | |
| level: INFO | |
| format: "Max run duration reached, sending ESP-NOW and sleeping" | |
| - script.execute: finish_and_sleep | |
| uart: | |
| id: uart_dlms_bus | |
| rx_pin: GPIO44 | |
| baud_rate: 2400 | |
| data_bits: 8 | |
| parity: NONE | |
| stop_bits: 1 | |
| rx_buffer_size: 1024 | |
| dlms_meter: | |
| id: dlms_meter_hub | |
| uart_id: uart_dlms_bus | |
| auth_key: !secret dlms_auth_key | |
| decryption_key: !secret dlms_decryption_key | |
| # 500 ms idle: 200 ms was too short and parsed frames already during on_boot. | |
| receive_timeout: 500ms | |
| script: | |
| - id: sleep_if_capacitor_low | |
| mode: single | |
| then: | |
| - component.update: battery_voltage | |
| - delay: 80ms | |
| - if: | |
| condition: | |
| lambda: |- | |
| float v = id(battery_voltage).state; | |
| return std::isnan(v) || v < ${min_capacitor_voltage}f; | |
| then: | |
| - logger.log: | |
| level: WARN | |
| format: "Capacitor %.2f V < ${min_capacitor_voltage} V, sleeping ${charging_sleep_duration_seconds}s to charge" | |
| args: ["id(battery_voltage).state"] | |
| - light.turn_on: | |
| id: onboard_rgb | |
| red: 100% | |
| green: 0% | |
| blue: 0% | |
| brightness: ${led_brightness} | |
| transition_length: 0s | |
| - delay: 200ms | |
| - light.turn_off: | |
| id: onboard_rgb | |
| transition_length: 0s | |
| - delay: 50ms | |
| - deep_sleep.enter: | |
| id: deep_sleep_control | |
| sleep_duration: !lambda "return ${charging_sleep_duration_seconds} * 1000;" | |
| - id: finish_and_sleep | |
| mode: single | |
| then: | |
| - wait_until: | |
| condition: | |
| lambda: "return id(boot_ready);" | |
| timeout: 10s | |
| - logger.log: | |
| level: INFO | |
| format: "Finishing and sleeping" | |
| - light.turn_on: | |
| id: onboard_rgb | |
| red: 0% | |
| green: 100% | |
| blue: 0% | |
| brightness: ${led_brightness} | |
| transition_length: 0s | |
| - component.update: uptime_seconds | |
| - delay: 50ms | |
| - component.update: wakeup_count | |
| - delay: 50ms | |
| - lambda: |- | |
| id(espnow_id).enable(); | |
| - delay: 150ms | |
| - component.update: transport | |
| - logger.log: | |
| level: INFO | |
| format: "Everything sent, turning off LED and sleeping" | |
| - light.turn_off: | |
| id: onboard_rgb | |
| transition_length: 0s | |
| - delay: 100ms | |
| # Minute minus 3–5 s (55–57 s). 58–59 s + frame/finish/boot often slips past the slot. | |
| - deep_sleep.enter: | |
| id: deep_sleep_control | |
| sleep_duration: !lambda |- | |
| const uint32_t jitter_s = 3 + (random_uint32() % 3); | |
| const uint32_t sleep_s = ${sleep_duration_minutes} * 60U - jitter_s; | |
| ESP_LOGI("sleep", "Deep sleep %u s (HAN phase dither)", sleep_s); | |
| return sleep_s * 1000; | |
| sensor: | |
| - platform: template | |
| id: wakeup_count | |
| internal: true | |
| accuracy_decimals: 0 | |
| update_interval: never | |
| lambda: "return id(wakeup_cycle_count);" | |
| - platform: uptime | |
| id: uptime_seconds | |
| type: seconds | |
| internal: true | |
| update_interval: never | |
| - platform: dlms_meter | |
| id: power_import | |
| dlms_meter_id: dlms_meter_hub | |
| obis_code: "${obis_power_import}" | |
| internal: true | |
| accuracy_decimals: 0 | |
| on_value: | |
| then: | |
| - script.execute: finish_and_sleep | |
| - platform: dlms_meter | |
| id: power_export | |
| dlms_meter_id: dlms_meter_hub | |
| obis_code: "${obis_power_export}" | |
| internal: true | |
| accuracy_decimals: 0 | |
| on_value: | |
| then: | |
| - script.execute: finish_and_sleep | |
| - platform: dlms_meter | |
| id: energy_import | |
| dlms_meter_id: dlms_meter_hub | |
| obis_code: "${obis_energy_import}" | |
| internal: true | |
| accuracy_decimals: 3 | |
| filters: | |
| - multiply: 0.001 | |
| on_value: | |
| then: | |
| - script.execute: finish_and_sleep | |
| - platform: dlms_meter | |
| id: energy_export | |
| dlms_meter_id: dlms_meter_hub | |
| obis_code: "${obis_energy_export}" | |
| internal: true | |
| accuracy_decimals: 3 | |
| filters: | |
| - multiply: 0.001 | |
| on_value: | |
| then: | |
| - script.execute: finish_and_sleep | |
| - platform: adc | |
| pin: GPIO4 | |
| name: "Battery Voltage" | |
| id: battery_voltage | |
| attenuation: 12db | |
| samples: 16 | |
| sampling_mode: avg | |
| unit_of_measurement: "V" | |
| device_class: voltage | |
| accuracy_decimals: 1 | |
| entity_category: diagnostic | |
| update_interval: never | |
| filters: | |
| - multiply: 2.022653721682848 | |
| - round: 1 | |
| light: | |
| - platform: esp32_rmt_led_strip | |
| channel_colors: RGB | |
| pin: GPIO47 | |
| num_leds: 1 | |
| chipset: ws2812 | |
| id: onboard_rgb | |
| internal: true | |
| restore_mode: ALWAYS_OFF |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment