Skip to content

Instantly share code, notes, and snippets.

@KrzysztofHajdamowicz
Created June 5, 2025 05:26
Show Gist options
  • Select an option

  • Save KrzysztofHajdamowicz/3cd91264e8187b323e0a2a674300de2d to your computer and use it in GitHub Desktop.

Select an option

Save KrzysztofHajdamowicz/3cd91264e8187b323e0a2a674300de2d to your computer and use it in GitHub Desktop.
ESPHome + 9x Eastron SDM120, Eastron SDM630 Utility Meter
esphome:
name: "poc-modbus"
friendly_name: PoC-Modbus
platformio_options:
build_flags:
- -DCONFIG_ARDUINO_LOOP_STACK_SIZE=32768
external_components:
- source: github://pr#8032
components: [ modbus, uart, modbus_controller ]
esp32:
board: esp32dev
framework:
type: esp-idf
# Enable logging
logger:
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
# Enable fallback hotspot (captive portal) in case wifi connection fails
ap:
ssid: "Poc-Modbus Fallback Hotspot"
password: "..."
captive_portal:
uart:
id: uart_bus
tx_pin: GPIO25
rx_pin: GPIO27
baud_rate: 19200
stop_bits: 1
rx_full_threshold: 1
modbus:
id: modbus_1
uart_id: uart_bus
send_wait_time: 600ms
turnaround_time: 200ms
flow_control_pin: GPIO26
globals:
- id: current_meter_polling_index # Current position in polling carousel
type: int
initial_value: '0'
- id: ha_connected_initial # Tracks if HA has connected at least once since boot
type: bool
initial_value: 'false'
- id: modbus_polling_active # Controls if the polling lambda should run
type: bool
initial_value: 'false'
ota:
- platform: esphome
password: "..."
on_begin:
then:
- logger.log: "OTA Start. Disabling modbus polling"
- globals.set:
id: modbus_polling_active
value: 'true'
# Enable Home Assistant API
api:
encryption:
key: "..."
# This will be triggered when Home Assistant (or any other API client) connects
on_client_connected:
then:
- lambda: |-
if (!id(ha_connected_initial)) { // Only act fully on the very first connection
ESP_LOGI("main", "Home Assistant client connected for the first time. Scheduling Modbus polling activation.");
id(ha_connected_initial) = true; // Mark that HA has connected
// Execute the script to enable polling after a delay
// The script itself will check if polling is already active
id(start_modbus_polling_delayed).execute();
} else {
ESP_LOGI("main", "Home Assistant client reconnected. Polling state remains as is.");
}
script:
- id: start_modbus_polling_delayed
mode: single # Ensures this script only runs one instance at a time
then:
- lambda: |-
// Only proceed if polling is not already active
if (id(modbus_polling_active)) {
ESP_LOGI("script.start_modbus_polling_delayed", "Modbus polling is already active. Exiting script.");
return;
}
ESP_LOGI("script.start_modbus_polling_delayed", "Starting delay before activating Modbus polling.");
- delay: 15s # Wait for 15 seconds to settle HA connection handshake etc
- lambda: |-
ESP_LOGI("script.start_modbus_polling_delayed", "Delay complete. Activating Modbus polling now.");
id(modbus_polling_active) = true;
interval:
- interval: 2s # Poll one meter every 2 seconds. Adjust as needed.
# For 9 meters, this means each meter updates every 9 * 2s = 18 seconds.
then:
- lambda: |-
if (!id(modbus_polling_active)) {
if (id(ha_connected_initial)) {
ESP_LOGD("meter_polling_interval", "Modbus polling is not yet active (waiting for delayed start).");
} else {
ESP_LOGD("meter_polling_interval", "Waiting for Home Assistant to connect and polling to be activated.");
}
return; // Don't poll if not yet active
}
// Array of meter component IDs (defined via packages)
ESP_LOGD("meter_polling_interval", "Updating meter at index: %d", id(current_meter_polling_index));
switch (id(current_meter_polling_index)) {
case 0: id(sdm_meter_instance_200).update(); break;
case 1: id(sdm_meter_instance_1).update(); break;
case 2: id(sdm_meter_instance_2).update(); break;
case 3: id(sdm_meter_instance_3).update(); break;
case 4: id(sdm_meter_instance_4).update(); break;
case 5: id(sdm_meter_instance_5).update(); break;
case 6: id(sdm_meter_instance_6).update(); break;
// case 6: id(sdm_meter_instance_7).update(); break;
// case 7: id(sdm_meter_instance_8).update(); break;
}
id(current_meter_polling_index)++;
if (id(current_meter_polling_index) >= 7) { // 9 is the total number of meters
id(current_meter_polling_index) = 0;
}
packages:
wifi: !include includes/packages/wifi.yaml
board_temp: !include includes/packages/internal_temp.yaml
uptime: !include includes/packages/uptime_timestamp.yaml
time: !include includes/packages/time.yaml
restart: !include includes/packages/restart.yaml
meter200: !include
file: includes/packages/sdm_meter_template_3ph.yaml
vars:
address: "200"
meter_component_id: sdm_meter_instance_200
meter1: !include
file: includes/packages/sdm_meter_template_1ph.yaml
vars:
address: "1"
meter_component_id: sdm_meter_instance_1
meter2: !include
file: includes/packages/sdm_meter_template_1ph.yaml
vars:
address: "2"
meter_component_id: sdm_meter_instance_2
meter3: !include
file: includes/packages/sdm_meter_template_1ph.yaml
vars:
address: "3"
meter_component_id: sdm_meter_instance_3
meter4: !include
file: includes/packages/sdm_meter_template_1ph.yaml
vars:
address: "4"
meter_component_id: sdm_meter_instance_4
meter5: !include
file: includes/packages/sdm_meter_template_1ph.yaml
vars:
address: "5"
meter_component_id: sdm_meter_instance_5
meter6: !include
file: includes/packages/sdm_meter_template_1ph.yaml
vars:
address: "6"
meter_component_id: sdm_meter_instance_6
meter7: !include
file: includes/packages/sdm_meter_template_1ph.yaml
vars:
address: "7"
meter_component_id: sdm_meter_instance_7
meter8: !include
file: includes/packages/sdm_meter_template_1ph.yaml
vars:
address: "8"
meter_component_id: sdm_meter_instance_8
substitutions:
address: "1"
meter_component_id: sdm_meter_instance_${address}
sensor:
- platform: sdm_meter
modbus_id: modbus_1
address: ${address}
id: ${meter_component_id}
update_interval: "never"
phase_a:
voltage:
name: "Meter ${address} L1 Voltage"
current:
name: "Meter ${address} L1 Current"
active_power:
name: "Meter ${address} L1 Active Power"
apparent_power:
name: "Meter ${address} L1 Apparent Power"
reactive_power:
name: "Meter ${address} L1 Reactive Power"
power_factor:
name: "Meter ${address} L1 Power Factor"
phase_angle:
name: "Meter ${address} L1 Phase Angle"
frequency:
name: "Meter ${address} Grid Frequency"
total_power:
name: "Meter ${address} Total Active Power"
import_active_energy:
name: "Meter ${address} Energy Import Active"
export_active_energy:
name: "Meter ${address} Energy Export Active"
import_reactive_energy:
name: "Meter ${address} Energy Import Reactive"
export_reactive_energy:
name: "Meter ${address} Energy Export Reactive"
substitutions:
address: "1"
meter_component_id: sdm_meter_instance_${address}
sensor:
- platform: sdm_meter
modbus_id: modbus_1
address: ${address}
id: ${meter_component_id}
update_interval: "never"
phase_a:
voltage:
name: "Meter ${address} L1 Voltage"
current:
name: "Meter ${address} L1 Current"
active_power:
name: "Meter ${address} L1 Active Power"
apparent_power:
name: "Meter ${address} L1 Apparent Power"
reactive_power:
name: "Meter ${address} L1 Reactive Power"
power_factor:
name: "Meter ${address} L1 Power Factor"
phase_angle:
name: "Meter ${address} L1 Phase Angle"
phase_b:
voltage:
name: "Meter ${address} L2 Voltage"
disabled_by_default: true
current:
name: "Meter ${address} L2 Current"
disabled_by_default: true
active_power:
name: "Meter ${address} L2 Active Power"
disabled_by_default: true
apparent_power:
name: "Meter ${address} L2 Apparent Power"
disabled_by_default: true
reactive_power:
name: "Meter ${address} L2 Reactive Power"
disabled_by_default: true
power_factor:
name: "Meter ${address} L2 Power Factor"
disabled_by_default: true
phase_angle:
name: "Meter ${address} L2 Phase Angle"
disabled_by_default: true
phase_c:
voltage:
name: "Meter ${address} L3 Voltage"
disabled_by_default: true
current:
name: "Meter ${address} L3 Current"
disabled_by_default: true
active_power:
name: "Meter ${address} L3 Active Power"
disabled_by_default: true
apparent_power:
name: "Meter ${address} L3 Apparent Power"
disabled_by_default: true
reactive_power:
name: "Meter ${address} L3 Reactive Power"
disabled_by_default: true
power_factor:
name: "Meter ${address} L3 Power Factor"
disabled_by_default: true
phase_angle:
name: "Meter ${address} L3 Phase Angle"
disabled_by_default: true
frequency:
name: "Meter ${address} Grid Frequency"
total_power:
name: "Meter ${address} Total Active Power"
import_active_energy:
name: "Meter ${address} Energy Import Active"
export_active_energy:
name: "Meter ${address} Energy Export Active"
import_reactive_energy:
name: "Meter ${address} Energy Import Reactive"
export_reactive_energy:
name: "Meter ${address} Energy Export Reactive"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment