756 lines
27 KiB
YAML
756 lines
27 KiB
YAML
##########################################################################################
|
|
##########################################################################################
|
|
# SMART DIN POWER MONITOR AND RELAY (Originally CBU)
|
|
# V1.0 2025-08-26 Initial Version
|
|
##########################################################################################
|
|
# Ex Tuya Smart Relay
|
|
# Uses this chip for power monitoring BL0942
|
|
# Replacement https://templates.blakadder.com/ESP8685-WROOM-06.html
|
|
#
|
|
# NOTES
|
|
# - DIN rail power monitor and relay
|
|
# - Relay is a pulse to latch ON (GPIO04) and pulse to latch OFF (GPIO05)
|
|
# - Has an "is it running" template sensor with a minimum wattage value of ${min_power_to_state_running_default}
|
|
# - The relay opens on over-current at ${max_current_trip_default} (Trip Active sensor set). Trip is latched
|
|
# (relay stays OFF) until manually reset via the "Clear Trip" button or you toggle the relay ON again.
|
|
# - The status_led flashes with a speed depending on current (faster near limit).
|
|
# - The status_led is ON when the relay has tripped
|
|
# - Inrush debounce: current must remain above the trip threshold for ${overcurrent_debounce_ms} ms
|
|
# before the trip occurs (helps ignore short inrush spikes).
|
|
# - Energy totals calculated on-device:
|
|
# * Last Hour Energy (kWh) - snapshot of the previous completed hour
|
|
# * Today / Yesterday (kWh) - resets at local midnight, survives reboots
|
|
# * This Week / Last Week (kWh) - resets at Monday 00:00 local time, survives reboots
|
|
# * This Month / Last Month (kWh) - resets at 1st of month 00:00 local time, survives reboots
|
|
# - For rolling 24h Min/Max Power, use HA "Statistics" helper instead of on-device buffers.
|
|
#
|
|
##########################################################################################
|
|
##########################################################################################
|
|
|
|
##########################################################################################
|
|
# SPECIFIC DEVICE VARIABLE SUBSTITUTIONS
|
|
# If NOT using a secrets file, just replace these with the passwords etc (in quotes)
|
|
##########################################################################################
|
|
substitutions:
|
|
# Device Naming
|
|
device_name: "dinpowermonitor-pmb"
|
|
friendly_name: "Din Power"
|
|
description_comment: "DIN Rail mounted current monitor and relay, with current based (software) trip"
|
|
device_area: "Hallway" # Allows ESP device to be automatically linked to an 'Area' in Home Assistant.
|
|
|
|
# Project Naming
|
|
project_name: "Generic ESP32.ESP8685-WROOM-06" # Project details, a dot separates the HA columns
|
|
project_version: "v1.0" # Project version allows checking of deployed vs latest version
|
|
|
|
# Passwords
|
|
api_key: !secret esp-api_key # unfortunately you cannot use substitutions inside secrets names
|
|
ota_pass: !secret esp-ota_pass
|
|
static_ip_address: !secret esp-dinpowermonitor-pmb_ip
|
|
|
|
# Device General Settings
|
|
log_level: "ERROR" # NONE, ERROR, WARN, INFO, DEBUG (Default), VERBOSE, VERY_VERBOSE
|
|
update_interval: "60s" # update time for general sensors etc
|
|
|
|
# Device Specific Defaults (used to seed HA-tunable numbers)
|
|
relay_1_name: "Relay"
|
|
button_1_name: "Toggle Button"
|
|
status_led_name: "Power Active"
|
|
min_power_to_state_running_default: 4 # Watts, initial value only
|
|
max_current_trip_default: 6 # Amps, initial value only
|
|
led_flash_slow_ms: 1000 # slow flash period when just running
|
|
led_flash_fast_ms: 100 # very fast flash period near trip
|
|
overcurrent_debounce_ms: 300 # Over-current inrush debounce (ms)
|
|
|
|
##########################################################################################
|
|
# PACKAGES: Included Common Packages
|
|
# https://esphome.io/components/packages.html
|
|
##########################################################################################
|
|
packages:
|
|
common_wifi: !include
|
|
file: common/network_common.yaml
|
|
vars:
|
|
local_device_name: "${device_name}"
|
|
local_static_ip_address: "${static_ip_address}"
|
|
local_ota_pass: "${ota_pass}"
|
|
common_api: !include
|
|
file: common/api_common.yaml
|
|
vars:
|
|
local_api_key: "${api_key}"
|
|
common_webportal: !include
|
|
file: common/webportal_common.yaml
|
|
common_mqtt: !include
|
|
file: common/mqtt_common.yaml
|
|
vars:
|
|
local_device_name: "${device_name}"
|
|
common_sntp: !include
|
|
file: common/sntp_common.yaml
|
|
common_general_sensors: !include
|
|
file: common/sensors_common.yaml
|
|
vars:
|
|
local_friendly_name: "${friendly_name}"
|
|
local_update_interval: "${update_interval}"
|
|
|
|
##########################################################################################
|
|
# ESPHome
|
|
# https://esphome.io/components/esphome.html
|
|
##########################################################################################
|
|
esphome:
|
|
name: "${device_name}"
|
|
friendly_name: "${friendly_name}"
|
|
comment: "${description_comment}" # Appears on the ESPHome page in HA
|
|
area: "${device_area}"
|
|
project:
|
|
name: "${project_name}"
|
|
version: "${project_version}"
|
|
on_boot:
|
|
priority: -100
|
|
then:
|
|
# Restore "Last Trip" text from persisted epoch (if any)
|
|
- lambda: |-
|
|
if (id(last_trip_epoch) > 0) {
|
|
auto t = time::ESPTime::from_epoch_local(id(last_trip_epoch));
|
|
char buf[24];
|
|
t.strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S");
|
|
id(ts_last_trip).publish_state(buf);
|
|
} else {
|
|
id(ts_last_trip).publish_state("Never");
|
|
}
|
|
# Apply Power Restore Mode selector (Always ON / Always OFF / Previous State)
|
|
- lambda: |-
|
|
std::string mode = id(sel_restore_mode).state;
|
|
if (mode == "Always ON") {
|
|
id(relay_virtual).turn_on();
|
|
} else if (mode == "Always OFF") {
|
|
id(relay_virtual).turn_off();
|
|
} else {
|
|
// "Previous State" -> do nothing (template switch will keep last or default OFF)
|
|
}
|
|
|
|
##########################################################################################
|
|
# ESP Platform and Framework
|
|
# https://esphome.io/components/esp32.html
|
|
##########################################################################################
|
|
esp32:
|
|
board: esp32-c3-devkitm-1
|
|
variant: esp32c3
|
|
framework:
|
|
type: esp-idf
|
|
|
|
##########################################################################################
|
|
# ESPHome Logging Enable
|
|
# https://esphome.io/components/logger.html
|
|
##########################################################################################
|
|
logger:
|
|
level: ${log_level}
|
|
|
|
##########################################################################################
|
|
# STATUS LED (BLUE) - low = ON per mapping
|
|
# https://esphome.io/components/status_led.html
|
|
##########################################################################################
|
|
status_led:
|
|
pin:
|
|
number: GPIO8
|
|
inverted: true
|
|
|
|
##########################################################################################
|
|
# UART BUS
|
|
# https://esphome.io/components/uart/
|
|
##########################################################################################
|
|
uart:
|
|
# for BL0942
|
|
id: bl_uart
|
|
rx_pin: GPIO19 # BL0942 TXD -> MCU RX
|
|
tx_pin: GPIO18 # BL0942 RXD -> MCU TX
|
|
baud_rate: 4800
|
|
parity: NONE
|
|
stop_bits: 1
|
|
|
|
##########################################################################################
|
|
# GLOBALS
|
|
# https://esphome.io/components/globals.html
|
|
##########################################################################################
|
|
globals:
|
|
# Used for LED blink period calculation (in milliseconds)
|
|
- id: _blink_period_ms
|
|
type: int
|
|
restore_value: no
|
|
initial_value: "500"
|
|
|
|
# Last trip time, saved across reboots.
|
|
- id: last_trip_epoch
|
|
type: uint32_t
|
|
restore_value: yes
|
|
initial_value: "0"
|
|
|
|
# Debounce start timestamp (ms since boot) for over-current detection
|
|
- id: _oc_start_ms
|
|
type: uint32_t
|
|
restore_value: no
|
|
initial_value: "0"
|
|
|
|
# ---- ENERGY ACCUMULATION & PERIOD SNAPSHOTS ----
|
|
- id: g_last_ms # millis() timestamp of last integration step
|
|
type: uint32_t
|
|
restore_value: no
|
|
initial_value: "0"
|
|
|
|
# Current-period totals (persisted)
|
|
- id: g_hour_kwh
|
|
type: float
|
|
restore_value: yes
|
|
initial_value: "0.0"
|
|
- id: g_today_kwh
|
|
type: float
|
|
restore_value: yes
|
|
initial_value: "0.0"
|
|
- id: g_week_kwh
|
|
type: float
|
|
restore_value: yes
|
|
initial_value: "0.0"
|
|
- id: g_month_kwh
|
|
type: float
|
|
restore_value: yes
|
|
initial_value: "0.0"
|
|
|
|
# Last-period snapshots (persisted)
|
|
- id: g_last_hour_kwh
|
|
type: float
|
|
restore_value: yes
|
|
initial_value: "0.0"
|
|
- id: g_yesterday_kwh
|
|
type: float
|
|
restore_value: yes
|
|
initial_value: "0.0"
|
|
- id: g_last_week_kwh
|
|
type: float
|
|
restore_value: yes
|
|
initial_value: "0.0"
|
|
- id: g_last_month_kwh
|
|
type: float
|
|
restore_value: yes
|
|
initial_value: "0.0"
|
|
|
|
# Boundary trackers (don't persist)
|
|
- id: g_last_hour_seen
|
|
type: int
|
|
restore_value: no
|
|
initial_value: "-1"
|
|
- id: g_last_doy_seen
|
|
type: int
|
|
restore_value: no
|
|
initial_value: "-1"
|
|
- id: g_last_wday_seen
|
|
type: int
|
|
restore_value: no
|
|
initial_value: "-1"
|
|
- id: g_last_month_seen
|
|
type: int
|
|
restore_value: no
|
|
initial_value: "-1"
|
|
|
|
##########################################################################################
|
|
# SENSORS
|
|
# https://esphome.io/components/sensor/
|
|
##########################################################################################
|
|
sensor:
|
|
- platform: bl0942
|
|
uart_id: bl_uart
|
|
update_interval: 250ms
|
|
voltage:
|
|
name: "${friendly_name} Voltage"
|
|
id: pm_voltage
|
|
accuracy_decimals: 1
|
|
unit_of_measurement: "V"
|
|
device_class: voltage
|
|
state_class: measurement
|
|
current:
|
|
name: "${friendly_name} Current"
|
|
id: pm_current
|
|
accuracy_decimals: 3
|
|
unit_of_measurement: "A"
|
|
device_class: current
|
|
state_class: measurement
|
|
on_value:
|
|
then:
|
|
- lambda: |-
|
|
// OVER-CURRENT TRIP HANDLER with inrush debounce
|
|
// Trip when current > n_max_trip and relay is ON, but only if it stays above
|
|
// for ${overcurrent_debounce_ms} ms (to ignore short inrush spikes).
|
|
if (id(bs_tripped).state) {
|
|
// Already tripped: reset debounce tracker and do nothing
|
|
id(_oc_start_ms) = 0;
|
|
} else if (id(relay_virtual).state && x > id(n_max_trip).state) {
|
|
// Over threshold while relay is ON
|
|
if (id(_oc_start_ms) == 0) {
|
|
id(_oc_start_ms) = millis(); // start debounce window
|
|
}
|
|
uint32_t elapsed = millis() - id(_oc_start_ms);
|
|
if (elapsed >= ${overcurrent_debounce_ms}) {
|
|
ESP_LOGI("trip",
|
|
"Overcurrent trip: %.3f A > %.3f A for %u ms (>= %u ms), opening relay",
|
|
x, id(n_max_trip).state, (unsigned) elapsed, (unsigned) ${overcurrent_debounce_ms});
|
|
|
|
// Mark trip, stop blinker FIRST
|
|
id(bs_tripped).publish_state(true);
|
|
id(led_blinker).stop();
|
|
|
|
// Open latching relay (template handles OFF coil pulse)
|
|
id(relay_virtual).turn_off();
|
|
|
|
// Force LED solid ON to indicate trip
|
|
auto call = id(led).turn_on();
|
|
call.perform();
|
|
|
|
// Time-stamp "Last Trip" using SNTP time (YYYY-MM-DD HH:MM:SS) and persist
|
|
auto now_clk = id(sntp_time).now();
|
|
if (now_clk.is_valid()) {
|
|
char buf[24];
|
|
now_clk.strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S");
|
|
id(ts_last_trip).publish_state(buf);
|
|
id(last_trip_epoch) = (uint32_t) now_clk.timestamp;
|
|
} else {
|
|
id(ts_last_trip).publish_state("unknown");
|
|
}
|
|
|
|
// Reset debounce tracker
|
|
id(_oc_start_ms) = 0;
|
|
}
|
|
} else {
|
|
// Below threshold or relay OFF: reset debounce tracker
|
|
id(_oc_start_ms) = 0;
|
|
}
|
|
|
|
// ------------------------------------------------------------------
|
|
// ENERGY ACCUMULATION (kWh) + PERIOD ROLLOVERS (hour/day/week/month)
|
|
// ------------------------------------------------------------------
|
|
// Integrate W over time using millis() -> kWh
|
|
uint32_t now_ms = millis();
|
|
if (id(g_last_ms) == 0) {
|
|
id(g_last_ms) = now_ms; // first sample since boot
|
|
} else {
|
|
uint32_t dt_ms = now_ms - id(g_last_ms);
|
|
id(g_last_ms) = now_ms;
|
|
|
|
float p_w = (isnan(x) || x < 0.0f) ? 0.0f : x; // sanitize power
|
|
float inc_kwh = p_w * (dt_ms / 3600000.0f); // W * hours -> kWh
|
|
|
|
id(g_hour_kwh) += inc_kwh;
|
|
id(g_today_kwh) += inc_kwh;
|
|
id(g_week_kwh) += inc_kwh; // Week rolls at Monday 00:00 local time
|
|
id(g_month_kwh) += inc_kwh;
|
|
}
|
|
|
|
// Use SNTP time for clean boundary changes
|
|
auto now = id(sntp_time).now();
|
|
if (now.is_valid()) {
|
|
// Hour rollover: when hour changes
|
|
if (id(g_last_hour_seen) != now.hour) {
|
|
if (id(g_last_hour_seen) != -1) {
|
|
id(g_last_hour_kwh) = id(g_hour_kwh);
|
|
id(s_last_hour_kwh).publish_state(id(g_last_hour_kwh));
|
|
}
|
|
id(g_hour_kwh) = 0.0f;
|
|
id(g_last_hour_seen) = now.hour;
|
|
}
|
|
|
|
// Day rollover (midnight local)
|
|
if (id(g_last_doy_seen) != now.day_of_year) {
|
|
if (id(g_last_doy_seen) != -1) {
|
|
id(g_yesterday_kwh) = id(g_today_kwh);
|
|
id(s_yesterday_kwh).publish_state(id(g_yesterday_kwh));
|
|
}
|
|
id(g_today_kwh) = 0.0f;
|
|
id(g_last_doy_seen) = now.day_of_year;
|
|
}
|
|
|
|
// Week rollover: Monday 00:00 local time (i.e., after Sunday)
|
|
// ESPHome day_of_week: Monday=1 ... Sunday=7
|
|
if (now.day_of_week == 1 && now.hour == 0 && id(g_last_wday_seen) != 1) {
|
|
id(g_last_week_kwh) = id(g_week_kwh);
|
|
id(s_last_week_kwh).publish_state(id(g_last_week_kwh));
|
|
id(g_week_kwh) = 0.0f;
|
|
}
|
|
id(g_last_wday_seen) = now.day_of_week;
|
|
|
|
// Month rollover: first day of month at 00:00
|
|
if (id(g_last_month_seen) != now.month) {
|
|
if (id(g_last_month_seen) != -1 && now.day_of_month == 1 && now.hour == 0) {
|
|
id(g_last_month_kwh) = id(g_month_kwh);
|
|
id(s_last_month_kwh).publish_state(id(g_last_month_kwh));
|
|
id(g_month_kwh) = 0.0f;
|
|
}
|
|
id(g_last_month_seen) = now.month;
|
|
}
|
|
}
|
|
power:
|
|
name: "${friendly_name} Power"
|
|
id: pm_power
|
|
accuracy_decimals: 1
|
|
unit_of_measurement: "W"
|
|
device_class: power
|
|
state_class: measurement
|
|
energy:
|
|
name: "${friendly_name} Energy"
|
|
id: pm_energy
|
|
unit_of_measurement: "Wh"
|
|
device_class: energy
|
|
state_class: total_increasing
|
|
frequency:
|
|
name: "${friendly_name} Frequency"
|
|
id: pm_freq
|
|
unit_of_measurement: "Hz"
|
|
device_class: frequency
|
|
state_class: measurement
|
|
# Optional calibration references (uncomment and tune if needed)
|
|
# voltage_reference: 15968
|
|
# current_reference: 124180
|
|
# power_reference: 309.1
|
|
# energy_reference: 2653
|
|
|
|
##########################################################################
|
|
# ENERGY TOTALS (kWh) EXPOSED TO HA
|
|
# These read the persisted globals; they update periodically.
|
|
##########################################################################
|
|
- platform: template
|
|
name: "${friendly_name} Last Hour Energy"
|
|
id: s_last_hour_kwh
|
|
unit_of_measurement: "kWh"
|
|
device_class: energy
|
|
state_class: total
|
|
accuracy_decimals: 3
|
|
update_interval: 30s
|
|
lambda: |-
|
|
return id(g_last_hour_kwh);
|
|
|
|
- platform: template
|
|
name: "${friendly_name} Today Energy"
|
|
id: s_today_kwh
|
|
unit_of_measurement: "kWh"
|
|
device_class: energy
|
|
state_class: total
|
|
accuracy_decimals: 3
|
|
update_interval: 30s
|
|
lambda: |-
|
|
return id(g_today_kwh);
|
|
|
|
- platform: template
|
|
name: "${friendly_name} Yesterday Energy"
|
|
id: s_yesterday_kwh
|
|
unit_of_measurement: "kWh"
|
|
device_class: energy
|
|
state_class: total
|
|
accuracy_decimals: 3
|
|
update_interval: 30s
|
|
lambda: |-
|
|
return id(g_yesterday_kwh);
|
|
|
|
- platform: template
|
|
name: "${friendly_name} This Week Energy"
|
|
id: s_week_kwh
|
|
unit_of_measurement: "kWh"
|
|
device_class: energy
|
|
state_class: total
|
|
accuracy_decimals: 3
|
|
update_interval: 30s
|
|
lambda: |-
|
|
return id(g_week_kwh);
|
|
|
|
- platform: template
|
|
name: "${friendly_name} Last Week Energy"
|
|
id: s_last_week_kwh
|
|
unit_of_measurement: "kWh"
|
|
device_class: energy
|
|
state_class: total
|
|
accuracy_decimals: 3
|
|
update_interval: 30s
|
|
lambda: |-
|
|
return id(g_last_week_kwh);
|
|
|
|
- platform: template
|
|
name: "${friendly_name} This Month Energy"
|
|
id: s_month_kwh
|
|
unit_of_measurement: "kWh"
|
|
device_class: energy
|
|
state_class: total
|
|
accuracy_decimals: 3
|
|
update_interval: 30s
|
|
lambda: |-
|
|
return id(g_month_kwh);
|
|
|
|
- platform: template
|
|
name: "${friendly_name} Last Month Energy"
|
|
id: s_last_month_kwh
|
|
unit_of_measurement: "kWh"
|
|
device_class: energy
|
|
state_class: total
|
|
accuracy_decimals: 3
|
|
update_interval: 30s
|
|
lambda: |-
|
|
return id(g_last_month_kwh);
|
|
|
|
##########################################################################################
|
|
# TEXT SENSOR COMPONENT
|
|
# https://esphome.io/components/text_sensor/
|
|
##########################################################################################
|
|
text_sensor:
|
|
# Holds the date/time of last trip (string with seconds)
|
|
- platform: template
|
|
name: "${friendly_name} Last Trip"
|
|
id: ts_last_trip
|
|
icon: "mdi:clock-alert"
|
|
entity_category: diagnostic
|
|
|
|
##########################################################################################
|
|
# BINARY SENSORS
|
|
# https://esphome.io/components/binary_sensor/
|
|
# https://esphome.io/components/binary_sensor/template.html
|
|
##########################################################################################
|
|
binary_sensor:
|
|
# "Is Running" template sensor
|
|
- platform: template
|
|
id: bs_running
|
|
name: "${friendly_name} Running"
|
|
lambda: |-
|
|
if (isnan(id(pm_power).state)) {
|
|
return false;
|
|
} else if (id(pm_power).state > id(n_min_power).state) {
|
|
return true;
|
|
} else {
|
|
return false;
|
|
}
|
|
filters:
|
|
- delayed_off: 15s
|
|
on_press:
|
|
then:
|
|
- if:
|
|
condition:
|
|
and:
|
|
- switch.is_on: relay_virtual
|
|
- lambda: 'return !id(bs_tripped).state;'
|
|
then:
|
|
- script.execute: led_blinker
|
|
on_release:
|
|
then:
|
|
- script.stop: led_blinker
|
|
- if:
|
|
condition:
|
|
lambda: 'return !id(bs_tripped).state;'
|
|
then:
|
|
- light.turn_off: led
|
|
|
|
# Trip flag exposed to HA; set true on over-current. Cleared when relay is turned ON or via Reset button.
|
|
- platform: template
|
|
id: bs_tripped
|
|
name: "${friendly_name} Trip Active"
|
|
device_class: problem
|
|
|
|
# On-board button (P17 -> IO9) pulls low when pressed
|
|
- platform: gpio
|
|
pin:
|
|
number: GPIO9
|
|
mode: INPUT_PULLUP
|
|
inverted: true
|
|
name: "${button_1_name}"
|
|
on_press:
|
|
- switch.toggle: relay_virtual
|
|
|
|
##########################################################################################
|
|
# NUMBER COMPONENT
|
|
# https://esphome.io/components/number/
|
|
##########################################################################################
|
|
# HA-TUNABLE THRESHOLDS (NUMBER ENTITIES)
|
|
# https://esphome.io/components/number/template.html
|
|
##########################################################################################
|
|
number:
|
|
- platform: template
|
|
name: "${friendly_name} Min Power Running"
|
|
id: n_min_power
|
|
optimistic: true
|
|
restore_value: true
|
|
initial_value: ${min_power_to_state_running_default}
|
|
min_value: 1
|
|
max_value: 100
|
|
step: 1
|
|
unit_of_measurement: "W"
|
|
icon: "mdi:flash-outline"
|
|
|
|
- platform: template
|
|
name: "${friendly_name} Max Current Trip"
|
|
id: n_max_trip
|
|
optimistic: true
|
|
restore_value: true
|
|
initial_value: ${max_current_trip_default}
|
|
min_value: 0
|
|
max_value: 10
|
|
step: 0.2
|
|
unit_of_measurement: "A"
|
|
icon: "mdi:current-ac"
|
|
|
|
##########################################################################################
|
|
# BUTTON COMPONENT
|
|
# Reset the trip latch from HA (does not close the relay automatically)
|
|
# https://esphome.io/components/button/
|
|
##########################################################################################
|
|
button:
|
|
- platform: template
|
|
name: "${friendly_name} Clear Trip"
|
|
id: btn_clear_trip
|
|
icon: "mdi:alert-remove-outline"
|
|
on_press:
|
|
- logger.log: "Trip reset requested (clearing Trip Active and LED state)"
|
|
- binary_sensor.template.publish:
|
|
id: bs_tripped
|
|
state: false
|
|
- lambda: |-
|
|
// Clear any pending debounce
|
|
id(_oc_start_ms) = 0;
|
|
- if:
|
|
condition:
|
|
and:
|
|
- switch.is_on: relay_virtual
|
|
- binary_sensor.is_on: bs_running
|
|
then:
|
|
- script.execute: led_blinker
|
|
else:
|
|
- light.turn_off: led
|
|
|
|
##########################################################################################
|
|
# SWITCH COMPONENT
|
|
# https://esphome.io/components/switch/
|
|
##########################################################################################
|
|
# LATCHING RELAY CONTROL (two coils: ON and OFF)
|
|
# P24 -> IO5 (close), P26 -> IO4 (open) per mapping.
|
|
# Two hidden GPIO pulse switches to drive the coils.
|
|
# User-facing switch sends a short pulse to the appropriate coil.
|
|
##########################################################################################
|
|
switch:
|
|
# Hidden coil driver: ON pulse (GPIO5)
|
|
- platform: gpio
|
|
id: relay_on_coil
|
|
pin:
|
|
number: GPIO5
|
|
inverted: false
|
|
restore_mode: ALWAYS_OFF
|
|
|
|
# Hidden coil driver: OFF pulse (GPIO4)
|
|
- platform: gpio
|
|
id: relay_off_coil
|
|
pin:
|
|
number: GPIO4
|
|
inverted: false
|
|
restore_mode: ALWAYS_OFF
|
|
|
|
# User-facing virtual switch
|
|
- platform: template
|
|
id: relay_virtual
|
|
name: "${relay_1_name}"
|
|
optimistic: true
|
|
# Use default restore behavior; "Previous State" is honored by doing nothing in on_boot.
|
|
# Set a safe baseline here (default off if no previous state).
|
|
restore_mode: RESTORE_DEFAULT_OFF
|
|
turn_on_action:
|
|
- logger.log: "Relay ON: pulsing ON coil"
|
|
- binary_sensor.template.publish:
|
|
id: bs_tripped
|
|
state: false # clear trip on manual ON
|
|
- switch.turn_on: relay_on_coil
|
|
- delay: 200ms
|
|
- switch.turn_off: relay_on_coil
|
|
- if:
|
|
condition:
|
|
and:
|
|
- binary_sensor.is_on: bs_running
|
|
- lambda: 'return !id(bs_tripped).state;'
|
|
then:
|
|
- script.execute: led_blinker
|
|
turn_off_action:
|
|
- logger.log: "Relay OFF: pulsing OFF coil"
|
|
- switch.turn_on: relay_off_coil
|
|
- delay: 200ms
|
|
- switch.turn_off: relay_off_coil
|
|
- script.stop: led_blinker
|
|
- if:
|
|
condition:
|
|
lambda: 'return !id(bs_tripped).state;'
|
|
then:
|
|
- light.turn_off: led
|
|
|
|
##########################################################################################
|
|
# OUTPUT COMPONENT
|
|
# https://esphome.io/components/output/ledc.html
|
|
##########################################################################################
|
|
# RED LED AS LIGHT (P9 -> IO10, low = ON)
|
|
##########################################################################################
|
|
output:
|
|
- platform: ledc
|
|
id: pow_red_led_pwm
|
|
pin:
|
|
number: GPIO10
|
|
inverted: true
|
|
|
|
##########################################################################################
|
|
# LIGHT COMPONENT
|
|
# https://esphome.io/components/light/monochromatic.html
|
|
##########################################################################################
|
|
# Hidden from HA; still used internally by scripts/logic
|
|
light:
|
|
- platform: monochromatic
|
|
id: led
|
|
output: pow_red_led_pwm
|
|
internal: true
|
|
|
|
##########################################################################################
|
|
# SELECT COMPONENT
|
|
# Choose how the relay state should be restored after boot
|
|
# https://esphome.io/components/select/template.html
|
|
##########################################################################################
|
|
select:
|
|
- platform: template
|
|
name: "${friendly_name} Power Restore Mode"
|
|
id: sel_restore_mode
|
|
optimistic: true
|
|
restore_value: true
|
|
options:
|
|
- "Always ON"
|
|
- "Always OFF"
|
|
- "Previous State"
|
|
initial_option: "Previous State"
|
|
icon: "mdi:power-settings"
|
|
|
|
##########################################################################################
|
|
# SCRIPTS
|
|
# https://esphome.io/components/script.html
|
|
##########################################################################################
|
|
script:
|
|
# LED blinker: variable-speed based on current vs n_max_trip; runs only when running, relay ON, not tripped
|
|
- id: led_blinker
|
|
mode: restart
|
|
then:
|
|
- while:
|
|
condition:
|
|
and:
|
|
- switch.is_on: relay_virtual
|
|
- binary_sensor.is_on: bs_running
|
|
- lambda: 'return !id(bs_tripped).state;'
|
|
then:
|
|
# Compute and store current-period ms in _blink_period_ms
|
|
- lambda: |-
|
|
float i = isnan(id(pm_current).state) ? 0.0f : id(pm_current).state;
|
|
float maxA = id(n_max_trip).state;
|
|
if (maxA < 0.1f) maxA = 0.1f;
|
|
float f = i / maxA; // 0.0 .. 1.0
|
|
if (f < 0.0f) f = 0.0f;
|
|
if (f > 1.0f) f = 1.0f;
|
|
int slow_ms = ${led_flash_slow_ms};
|
|
int fast_ms = ${led_flash_fast_ms};
|
|
if (slow_ms < fast_ms) { int t = slow_ms; slow_ms = fast_ms; fast_ms = t; } # ensure slow >= fast
|
|
int period = slow_ms - (int)((slow_ms - fast_ms) * f);
|
|
id(_blink_period_ms) = period;
|
|
# Blink with 50% duty cycle at the computed period
|
|
- light.turn_on:
|
|
id: led
|
|
brightness: 100%
|
|
- delay: !lambda 'return (uint32_t)(id(_blink_period_ms) / 2);'
|
|
- light.turn_off: led
|
|
- delay: !lambda 'return (uint32_t)(id(_blink_period_ms) / 2);'
|