Skip to content

Instantly share code, notes, and snippets.

@pavax
Last active August 25, 2026 21:11
Show Gist options
  • Select an option

  • Save pavax/a6a425ee41beb4a6cd7770091b192ba5 to your computer and use it in GitHub Desktop.

Select an option

Save pavax/a6a425ee41beb4a6cd7770091b192ba5 to your computer and use it in GitHub Desktop.
Kamstrup Omnipower Smart-Meter Readings using EspHome and Esp-now
# 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