#:########################################################################################:# # TITLE: MAIN HOUSE 3 PHASE POWER MONITOR # zorruno.com layout v1.1 2026 #:########################################################################################:# # REPO: # https://home.fox.co.nz/gitea/zorruno/zorruno-homeassistant/src/branch/master/esphome/esp-mainpowermonitor.yaml #:########################################################################################:# # VERSIONS: # v1.3 2026-02-25 Updated yaml to zorruno layout V1.1 # v1.2 2026-02-25 Updated yaml to zorruno layout V1.1. Added EV charging binary sensor # and EV power rolling average (default 5 min). Clarified EV pulse notes. # v1.1 2026-02-21 Fix globals array init compile error, reduce flash writes (globals update_interval) # v1.0 2026-02-21 Initial Version #:########################################################################################:# # HARDWARE: # - Wemos D1 mini (ESP8266) # - 3x PZEM-004T V3 (Modbus, 9600 baud) # - DHT11 (GPIO2) # - EV pulse input (GPIO14) #:########################################################################################:# # OPERATION NOTES: # - Power Monitor using 3x PZEM-004T V3 (Modbus) for 3-phase monitoring # - Pulse input energy monitoring for 32A EV Charger (2 Wh per pulse) # - Adds a 5 minute rolling average for EV power to avoid spiky readings at low loads # - Adds a binary sensor to indicate when the EV charger is operating (threshold configurable) # # NOTES: # - UART is on GPIO1 (TX) and GPIO3 (RX). Logger UART is disabled (baud_rate: 0). # - You will usually need to disconnect PZEM UART wires while flashing via USB. # - DHT11 is on GPIO2 (boot-strap pin and D1 mini onboard LED pin). Usually OK, but if you ever # get boot issues or flaky DHT readings, move DHT to a different GPIO. # - PZEM "energy" from ESPHome pzemac is in Wh, converted to kWh in this YAML. # - Phase mapping requested: Red=0, Yellow=1, Blue=2 (Tasmota-style). In ESPHome pzemac Modbus # addresses must be 0x01..0xF7, so this YAML maps to Red=0x01, Yellow=0x02, Blue=0x03. # - EV pulse power at very low load is inherently "chunky" due to 2 Wh pulses. Energy totals # remain accurate. Rolling average reduces spikes but can still read 0 W between infrequent pulses. #:########################################################################################:# # OFFLINE NOTES: # a) Home Assistant OFFLINE, WiFi OK # - Device continues measuring power, energy, and pulses locally. # - MQTT/API publishing will fail until HA is back. # b) WiFi OFFLINE (LAN down), Device powered # - No SNTP updates, so time may become invalid or drift (depending on last sync). # - Power and energy totals continue accumulating, but period rollovers (today/month/year) # will not update unless time is valid. # c) Entire WiFi/Network OFFLINE # - Accurate time IS needed for daily/monthly/year rollovers (SNTP required for correct period boundaries). # - Loss of network will drift the timeclocks; time of day can be reset by powering off then on again at 12pm (midday) #:########################################################################################:# #:########################################################################################:# # SUBSTITUTIONS: Specific device variable substitutions #:########################################################################################:# substitutions: # Device Naming device_name: "esp-mainpowermonitor" friendly_name: "Main House 3 Phase Power Monitor" description_comment: "Power Monitor using 3x PZEM004 for 3 phase monitoring plus energy monitoring for 32A EV Charger (Layout V1.1)" device_area: "Data Cupboard" # Project Naming project_name: "Wemos.D1Mini PZEM-004T V3 3-Phase Power Monitor" project_version: "v1.3" # Passwords api_key: !secret esp-api_key ota_pass: !secret esp-ota_pass static_ip_address: !secret esp-mainpowermonitor_ip mqtt_command_main_topic: !secret mqtt_command_main_topic # If we are changing IP addresses, you must update the current IP address here, otherwise it remains # Don't forget to switch it back when changed. current_ip_address: ${static_ip_address} # Device Settings log_level: "ERROR" pzem_update_interval: "10s" env_update_interval: "60s" rollover_check_interval: "30s" # Globals persistence tuning (gentler on flash) globals_save_interval_slow: "10min" globals_save_interval_pulses: "2min" # PZEM Modbus Addresses (valid 0x01..0xF7) # Requested phase IDs: Red=0, Yellow=1, Blue=2 -> map to 0x01, 0x02, 0x03 pzem_addr_red: "0x01" pzem_addr_yellow: "0x02" pzem_addr_blue: "0x03" # EV Pulse Settings ev_pulse_wh: "2.0" # Each pulse is 2Wh ev_pulse_filter_ms: "50ms" # Debounce / filter ev_pulse_pin_mode: "INPUT_PULLUP" # EV Charging Detection (Averaged power + binary sensor) # Minimum EV charging power threshold (W) for binary sensor # NOTE: Pick a value comfortably above idle (idle can be ~0.2 kWh/day = very infrequent pulses). ev_charging_min_power_w: "300" # Rolling average window - configuration notes: # - Substitutions are string replacements, so window samples must be updated manually if you change times. # - window_samples = (avg_minutes * 60) / sample_interval_seconds ev_power_avg_window_minutes: "5" # informational ev_power_avg_sample_interval: "10s" ev_power_avg_sample_interval_seconds: "10" # must match the value above ev_power_avg_window_samples: "30" # 5 min @ 10s = 30 samples ev_power_avg_buffer_size: "180" # ring buffer >= window_samples (180 @ 10s = 30 minutes) # Names - PZEM (per phase) pzem_red_v_name: "Main Power Red Voltage" pzem_red_i_name: "Main Power Red Current" pzem_red_p_name: "Main Power Red Active Power" pzem_red_e_name: "Main Power Red Active Energy" pzem_red_f_name: "Main Power Red Frequency" pzem_red_pf_name: "Main Power Red Power Factor" pzem_yellow_v_name: "Main Power Yellow Voltage" pzem_yellow_i_name: "Main Power Yellow Current" pzem_yellow_p_name: "Main Power Yellow Active Power" pzem_yellow_e_name: "Main Power Yellow Active Energy" pzem_yellow_f_name: "Main Power Yellow Frequency" pzem_yellow_pf_name: "Main Power Yellow Power Factor" pzem_blue_v_name: "Main Power Blue Voltage" pzem_blue_i_name: "Main Power Blue Current" pzem_blue_p_name: "Main Power Blue Active Power" pzem_blue_e_name: "Main Power Blue Active Energy" pzem_blue_f_name: "Main Power Blue Frequency" pzem_blue_pf_name: "Main Power Blue Power Factor" # Names - Totals mains_total_p_name: "Main Power Total Active Power" mains_total_s_name: "Main Power Total Apparent Power" mains_total_pf_name: "Main Power Total Power Factor" mains_total_e_name: "Main Power Total Active Energy" mains_today_name: "Main Power Energy Today" mains_yesterday_name: "Main Power Energy Yesterday" mains_month_name: "Main Power Energy This Month" mains_last_month_name: "Main Power Energy Last Month" mains_year_name: "Main Power Energy This Year" mains_last_year_name: "Main Power Energy Last Year" mains_daily_max_365_name: "Main Power Daily Energy Max (365d)" mains_daily_min_365_name: "Main Power Daily Energy Min (365d)" # Names - EV Wallcharger ev_power_name: "EV Wallcharger Power" ev_charging_name: "EV Wallcharger Charging" ev_total_energy_name: "EV Wallcharger Total Energy" ev_today_name: "EV Wallcharger Energy Today" ev_yesterday_name: "EV Wallcharger Energy Yesterday" ev_month_name: "EV Wallcharger Energy This Month" ev_last_month_name: "EV Wallcharger Energy Last Month" ev_year_name: "EV Wallcharger Energy This Year" ev_last_year_name: "EV Wallcharger Energy Last Year" # Names - Environment temp_name: "Main Power Monitor Temperature" hum_name: "Main Power Monitor Humidity" dew_name: "Main Power Monitor Dew Point" #:########################################################################################:# # PACKAGES: Included Common Packages #:########################################################################################:# packages: #### WIFI, Network (Static/DHCP/IPV6 etc), Fallback AP, Safemode #### 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}" local_current_ip_address: "${current_ip_address}" #### HOME ASSISTANT API #### common_api: !include file: common/api_common.yaml vars: local_api_key: "${api_key}" #### MQTT #### common_mqtt: !include file: common/mqtt_common.yaml vars: local_device_name: "${device_name}" #### SNTP #### common_sntp: !include common/sntp_common.yaml #### DIAGNOSTICS Sensors #### diag_basic: !include common/include_basic_diag_sensors.yaml diag_more: !include common/include_more_diag_sensors.yaml #:########################################################################################:# # ESPHOME: # https://esphome.io/components/esphome.html #:########################################################################################:# esphome: name: "${device_name}" friendly_name: "${friendly_name}" comment: "${description_comment}" area: "${device_area}" project: name: "${project_name}" version: "${project_version}" on_boot: priority: -100 then: - script.execute: init_history_arrays - delay: 3s - script.execute: rollover_check #:########################################################################################:# # ESP PLATFORM AND FRAMEWORK: # https://esphome.io/components/esp8266/ #:########################################################################################:# esp8266: board: d1_mini restore_from_flash: true preferences: flash_write_interval: 5min mdns: disabled: false #:########################################################################################:# # LOGGING: # https://esphome.io/components/logger.html #:########################################################################################:# logger: level: "${log_level}" baud_rate: 0 #:########################################################################################:# # UART + MODBUS (PZEM-004T V3): #:########################################################################################:# uart: id: uart_pzem_bus tx_pin: GPIO1 rx_pin: GPIO3 baud_rate: 9600 modbus: id: modbus_pzem_bus send_wait_time: 200ms #:########################################################################################:# # GLOBALS (Energy rollovers + 365-day history + temp min/max): #:########################################################################################:# globals: # Date tracking for rollovers (restored) - id: g_last_day type: int restore_value: yes update_interval: ${globals_save_interval_slow} initial_value: "0" - id: g_last_month type: int restore_value: yes update_interval: ${globals_save_interval_slow} initial_value: "0" - id: g_last_year type: int restore_value: yes update_interval: ${globals_save_interval_slow} initial_value: "0" # Main (3-phase) baseline totals for period calculations (restored) - id: g_mains_day_start_kwh type: float restore_value: yes update_interval: ${globals_save_interval_slow} initial_value: "-1.0" - id: g_mains_month_start_kwh type: float restore_value: yes update_interval: ${globals_save_interval_slow} initial_value: "-1.0" - id: g_mains_year_start_kwh type: float restore_value: yes update_interval: ${globals_save_interval_slow} initial_value: "-1.0" # Stored prior periods (restored) - id: g_mains_yesterday_kwh type: float restore_value: yes update_interval: ${globals_save_interval_slow} initial_value: "0.0" - id: g_mains_last_month_kwh type: float restore_value: yes update_interval: ${globals_save_interval_slow} initial_value: "0.0" - id: g_mains_last_year_kwh type: float restore_value: yes update_interval: ${globals_save_interval_slow} initial_value: "0.0" # Daily history ring buffer (last 365 days) - RAM ONLY (NOT restored) # NOTE: Array globals MUST NOT use initial_value: "{}" (causes compile error). - id: g_mains_daily_hist_kwh type: float[365] restore_value: no - id: g_mains_daily_hist_index type: int restore_value: no initial_value: "0" - id: g_mains_daily_hist_count type: int restore_value: no initial_value: "0" # EV baseline totals for period calculations (restored) - id: g_ev_day_start_kwh type: float restore_value: yes update_interval: ${globals_save_interval_slow} initial_value: "-1.0" - id: g_ev_month_start_kwh type: float restore_value: yes update_interval: ${globals_save_interval_slow} initial_value: "-1.0" - id: g_ev_year_start_kwh type: float restore_value: yes update_interval: ${globals_save_interval_slow} initial_value: "-1.0" # EV stored prior periods (restored) - id: g_ev_yesterday_kwh type: float restore_value: yes update_interval: ${globals_save_interval_slow} initial_value: "0.0" - id: g_ev_last_month_kwh type: float restore_value: yes update_interval: ${globals_save_interval_slow} initial_value: "0.0" - id: g_ev_last_year_kwh type: float restore_value: yes update_interval: ${globals_save_interval_slow} initial_value: "0.0" # EV pulse accumulation (persisted across reboots) (restored) - id: g_ev_pulses_total_abs type: uint32_t restore_value: yes update_interval: ${globals_save_interval_pulses} initial_value: "0" - id: g_ev_pulses_raw_last type: uint32_t restore_value: yes update_interval: ${globals_save_interval_pulses} initial_value: "0" # EV rolling average power (RAM only) - id: g_ev_power_avg_w type: float restore_value: no initial_value: "0.0" # EV pulse history ring buffer for rolling average (RAM only) - id: g_ev_pulses_hist type: uint32_t[${ev_power_avg_buffer_size}] restore_value: no - id: g_ev_pulses_hist_index type: int restore_value: no initial_value: "0" - id: g_ev_pulses_hist_count type: int restore_value: no initial_value: "0" # Temperature min/max tracking (RAM ONLY - not restored, gentler on flash) - id: g_temp_today_min type: float restore_value: no initial_value: "9999.0" - id: g_temp_today_max type: float restore_value: no initial_value: "-9999.0" - id: g_temp_yesterday_min type: float restore_value: no initial_value: "9999.0" - id: g_temp_yesterday_max type: float restore_value: no initial_value: "-9999.0" - id: g_temp_month_min type: float restore_value: no initial_value: "9999.0" - id: g_temp_month_max type: float restore_value: no initial_value: "-9999.0" - id: g_temp_last_month_min type: float restore_value: no initial_value: "9999.0" - id: g_temp_last_month_max type: float restore_value: no initial_value: "-9999.0" #:########################################################################################:# # INTERVAL (periodic rollover checks + EV rolling average updates): #:########################################################################################:# interval: - interval: "${rollover_check_interval}" then: - script.execute: rollover_check - interval: "${ev_power_avg_sample_interval}" then: - script.execute: ev_power_avg_update #:########################################################################################:# # BINARY SENSORS: #:########################################################################################:# binary_sensor: - platform: template name: "${ev_charging_name}" id: ev_wallcharger_charging device_class: power icon: "mdi:ev-station" lambda: |- const float p = id(ev_power_avg_w).state; if (isnan(p)) return false; return (p >= ${ev_charging_min_power_w}); #:########################################################################################:# # SENSORS: #:########################################################################################:# sensor: ######################################################################################## # PZEM-004T V3 - RED PHASE ######################################################################################## - platform: pzemac id: pzem_red modbus_id: modbus_pzem_bus address: ${pzem_addr_red} update_interval: ${pzem_update_interval} voltage: name: "${pzem_red_v_name}" id: pzem_red_voltage accuracy_decimals: 1 current: name: "${pzem_red_i_name}" id: pzem_red_current accuracy_decimals: 3 power: name: "${pzem_red_p_name}" id: pzem_red_power_w accuracy_decimals: 0 energy: name: "${pzem_red_e_name}" id: pzem_red_energy_kwh unit_of_measurement: "kWh" accuracy_decimals: 3 filters: - multiply: 0.001 frequency: name: "${pzem_red_f_name}" id: pzem_red_freq_hz accuracy_decimals: 1 power_factor: name: "${pzem_red_pf_name}" id: pzem_red_pf accuracy_decimals: 2 - platform: template name: "Main Power Red Apparent Power" id: pzem_red_apparent_power_va unit_of_measurement: "VA" accuracy_decimals: 0 update_interval: ${pzem_update_interval} lambda: |- if (isnan(id(pzem_red_voltage).state) || isnan(id(pzem_red_current).state)) return NAN; return id(pzem_red_voltage).state * id(pzem_red_current).state; - platform: template name: "Main Power Red Reactive Power" id: pzem_red_reactive_power_var unit_of_measurement: "var" accuracy_decimals: 0 update_interval: ${pzem_update_interval} lambda: |- const float p = id(pzem_red_power_w).state; const float s = id(pzem_red_apparent_power_va).state; if (isnan(p) || isnan(s)) return NAN; const float d = (s * s) - (p * p); if (d <= 0.0f) return 0.0f; return sqrtf(d); - platform: integration name: "Main Power Red Apparent Energy" id: pzem_red_apparent_energy_kvah sensor: pzem_red_apparent_power_va time_unit: h unit_of_measurement: "kVAh" accuracy_decimals: 3 filters: - multiply: 0.001 ######################################################################################## # PZEM-004T V3 - YELLOW PHASE ######################################################################################## - platform: pzemac id: pzem_yellow modbus_id: modbus_pzem_bus address: ${pzem_addr_yellow} update_interval: ${pzem_update_interval} voltage: name: "${pzem_yellow_v_name}" id: pzem_yellow_voltage accuracy_decimals: 1 current: name: "${pzem_yellow_i_name}" id: pzem_yellow_current accuracy_decimals: 3 power: name: "${pzem_yellow_p_name}" id: pzem_yellow_power_w accuracy_decimals: 0 energy: name: "${pzem_yellow_e_name}" id: pzem_yellow_energy_kwh unit_of_measurement: "kWh" accuracy_decimals: 3 filters: - multiply: 0.001 frequency: name: "${pzem_yellow_f_name}" id: pzem_yellow_freq_hz accuracy_decimals: 1 power_factor: name: "${pzem_yellow_pf_name}" id: pzem_yellow_pf accuracy_decimals: 2 - platform: template name: "Main Power Yellow Apparent Power" id: pzem_yellow_apparent_power_va unit_of_measurement: "VA" accuracy_decimals: 0 update_interval: ${pzem_update_interval} lambda: |- if (isnan(id(pzem_yellow_voltage).state) || isnan(id(pzem_yellow_current).state)) return NAN; return id(pzem_yellow_voltage).state * id(pzem_yellow_current).state; - platform: template name: "Main Power Yellow Reactive Power" id: pzem_yellow_reactive_power_var unit_of_measurement: "var" accuracy_decimals: 0 update_interval: ${pzem_update_interval} lambda: |- const float p = id(pzem_yellow_power_w).state; const float s = id(pzem_yellow_apparent_power_va).state; if (isnan(p) || isnan(s)) return NAN; const float d = (s * s) - (p * p); if (d <= 0.0f) return 0.0f; return sqrtf(d); - platform: integration name: "Main Power Yellow Apparent Energy" id: pzem_yellow_apparent_energy_kvah sensor: pzem_yellow_apparent_power_va time_unit: h unit_of_measurement: "kVAh" accuracy_decimals: 3 filters: - multiply: 0.001 ######################################################################################## # PZEM-004T V3 - BLUE PHASE ######################################################################################## - platform: pzemac id: pzem_blue modbus_id: modbus_pzem_bus address: ${pzem_addr_blue} update_interval: ${pzem_update_interval} voltage: name: "${pzem_blue_v_name}" id: pzem_blue_voltage accuracy_decimals: 1 current: name: "${pzem_blue_i_name}" id: pzem_blue_current accuracy_decimals: 3 power: name: "${pzem_blue_p_name}" id: pzem_blue_power_w accuracy_decimals: 0 energy: name: "${pzem_blue_e_name}" id: pzem_blue_energy_kwh unit_of_measurement: "kWh" accuracy_decimals: 3 filters: - multiply: 0.001 frequency: name: "${pzem_blue_f_name}" id: pzem_blue_freq_hz accuracy_decimals: 1 power_factor: name: "${pzem_blue_pf_name}" id: pzem_blue_pf accuracy_decimals: 2 - platform: template name: "Main Power Blue Apparent Power" id: pzem_blue_apparent_power_va unit_of_measurement: "VA" accuracy_decimals: 0 update_interval: ${pzem_update_interval} lambda: |- if (isnan(id(pzem_blue_voltage).state) || isnan(id(pzem_blue_current).state)) return NAN; return id(pzem_blue_voltage).state * id(pzem_blue_current).state; - platform: template name: "Main Power Blue Reactive Power" id: pzem_blue_reactive_power_var unit_of_measurement: "var" accuracy_decimals: 0 update_interval: ${pzem_update_interval} lambda: |- const float p = id(pzem_blue_power_w).state; const float s = id(pzem_blue_apparent_power_va).state; if (isnan(p) || isnan(s)) return NAN; const float d = (s * s) - (p * p); if (d <= 0.0f) return 0.0f; return sqrtf(d); - platform: integration name: "Main Power Blue Apparent Energy" id: pzem_blue_apparent_energy_kvah sensor: pzem_blue_apparent_power_va time_unit: h unit_of_measurement: "kVAh" accuracy_decimals: 3 filters: - multiply: 0.001 ######################################################################################## # 3-PHASE TOTALS (Active/Real + Apparent + Reactive + PF) ######################################################################################## - platform: template name: "${mains_total_p_name}" id: mains_total_active_power_w unit_of_measurement: "W" accuracy_decimals: 0 update_interval: ${pzem_update_interval} lambda: |- if (isnan(id(pzem_red_power_w).state) || isnan(id(pzem_yellow_power_w).state) || isnan(id(pzem_blue_power_w).state)) return NAN; return id(pzem_red_power_w).state + id(pzem_yellow_power_w).state + id(pzem_blue_power_w).state; - platform: template name: "${mains_total_s_name}" id: mains_total_apparent_power_va unit_of_measurement: "VA" accuracy_decimals: 0 update_interval: ${pzem_update_interval} lambda: |- if (isnan(id(pzem_red_apparent_power_va).state) || isnan(id(pzem_yellow_apparent_power_va).state) || isnan(id(pzem_blue_apparent_power_va).state)) return NAN; return id(pzem_red_apparent_power_va).state + id(pzem_yellow_apparent_power_va).state + id(pzem_blue_apparent_power_va).state; - platform: template name: "Main Power Total Reactive Power" id: mains_total_reactive_power_var unit_of_measurement: "var" accuracy_decimals: 0 update_interval: ${pzem_update_interval} lambda: |- if (isnan(id(pzem_red_reactive_power_var).state) || isnan(id(pzem_yellow_reactive_power_var).state) || isnan(id(pzem_blue_reactive_power_var).state)) return NAN; return id(pzem_red_reactive_power_var).state + id(pzem_yellow_reactive_power_var).state + id(pzem_blue_reactive_power_var).state; - platform: template name: "${mains_total_pf_name}" id: mains_total_pf accuracy_decimals: 2 update_interval: ${pzem_update_interval} lambda: |- const float p = id(mains_total_active_power_w).state; const float s = id(mains_total_apparent_power_va).state; if (isnan(p) || isnan(s) || s <= 0.0f) return NAN; float pf = p / s; if (pf < 0.0f) pf = 0.0f; if (pf > 1.0f) pf = 1.0f; return pf; filters: - round: 2 - platform: template name: "${mains_total_e_name}" id: mains_total_active_energy_kwh unit_of_measurement: "kWh" accuracy_decimals: 3 update_interval: ${pzem_update_interval} lambda: |- if (isnan(id(pzem_red_energy_kwh).state) || isnan(id(pzem_yellow_energy_kwh).state) || isnan(id(pzem_blue_energy_kwh).state)) return NAN; return id(pzem_red_energy_kwh).state + id(pzem_yellow_energy_kwh).state + id(pzem_blue_energy_kwh).state; ######################################################################################## # MAIN ENERGY PERIOD TOTALS (Today / Yesterday / Month / Year) ######################################################################################## - platform: template name: "${mains_today_name}" id: mains_energy_today_kwh unit_of_measurement: "kWh" accuracy_decimals: 3 update_interval: 60s lambda: |- const float total = id(mains_total_active_energy_kwh).state; if (isnan(total) || id(g_mains_day_start_kwh) < 0.0f) return NAN; float v = total - id(g_mains_day_start_kwh); if (v < 0.0f) v = 0.0f; return v; - platform: template name: "${mains_yesterday_name}" id: mains_energy_yesterday_kwh unit_of_measurement: "kWh" accuracy_decimals: 3 update_interval: 5min lambda: |- return id(g_mains_yesterday_kwh); - platform: template name: "${mains_month_name}" id: mains_energy_this_month_kwh unit_of_measurement: "kWh" accuracy_decimals: 3 update_interval: 5min lambda: |- const float total = id(mains_total_active_energy_kwh).state; if (isnan(total) || id(g_mains_month_start_kwh) < 0.0f) return NAN; float v = total - id(g_mains_month_start_kwh); if (v < 0.0f) v = 0.0f; return v; - platform: template name: "${mains_last_month_name}" id: mains_energy_last_month_kwh unit_of_measurement: "kWh" accuracy_decimals: 3 update_interval: 10min lambda: |- return id(g_mains_last_month_kwh); - platform: template name: "${mains_year_name}" id: mains_energy_this_year_kwh unit_of_measurement: "kWh" accuracy_decimals: 3 update_interval: 10min lambda: |- const float total = id(mains_total_active_energy_kwh).state; if (isnan(total) || id(g_mains_year_start_kwh) < 0.0f) return NAN; float v = total - id(g_mains_year_start_kwh); if (v < 0.0f) v = 0.0f; return v; - platform: template name: "${mains_last_year_name}" id: mains_energy_last_year_kwh unit_of_measurement: "kWh" accuracy_decimals: 3 update_interval: 10min lambda: |- return id(g_mains_last_year_kwh); - platform: template name: "${mains_daily_max_365_name}" id: mains_daily_energy_max_365d_kwh unit_of_measurement: "kWh" accuracy_decimals: 3 update_interval: 10min lambda: |- if (id(g_mains_daily_hist_count) <= 0) return NAN; float maxv = NAN; for (int i = 0; i < 365; i++) { const float v = id(g_mains_daily_hist_kwh)[i]; if (isnan(v)) continue; if (isnan(maxv) || v > maxv) maxv = v; } return maxv; - platform: template name: "${mains_daily_min_365_name}" id: mains_daily_energy_min_365d_kwh unit_of_measurement: "kWh" accuracy_decimals: 3 update_interval: 10min lambda: |- if (id(g_mains_daily_hist_count) <= 0) return NAN; float minv = NAN; for (int i = 0; i < 365; i++) { const float v = id(g_mains_daily_hist_kwh)[i]; if (isnan(v)) continue; if (isnan(minv) || v < minv) minv = v; } return minv; ######################################################################################## # DHT11 (Temp / Humidity / Dewpoint) + TEMP MIN/MAX (Today / Yesterday / Month / Last Month) ######################################################################################## - platform: dht pin: GPIO2 model: DHT11 update_interval: ${env_update_interval} temperature: name: "${temp_name}" id: env_temp unit_of_measurement: "°C" accuracy_decimals: 1 on_value: then: - script.execute: update_temp_minmax humidity: name: "${hum_name}" id: env_humidity unit_of_measurement: "%" accuracy_decimals: 0 - platform: template name: "${dew_name}" id: env_dewpoint unit_of_measurement: "°C" accuracy_decimals: 1 update_interval: ${env_update_interval} lambda: |- const float t = id(env_temp).state; const float rh = id(env_humidity).state; if (isnan(t) || isnan(rh) || rh <= 0.0f) return NAN; const float h = rh / 100.0f; const float a = 17.67f; const float b = 243.5f; const float gamma = (a * t) / (b + t) + logf(h); return (b * gamma) / (a - gamma); - platform: template name: "Main Power Temp Min Today" id: temp_min_today unit_of_measurement: "°C" accuracy_decimals: 1 update_interval: 2min lambda: |- return (id(g_temp_today_min) > 9000.0f) ? NAN : id(g_temp_today_min); - platform: template name: "Main Power Temp Max Today" id: temp_max_today unit_of_measurement: "°C" accuracy_decimals: 1 update_interval: 2min lambda: |- return (id(g_temp_today_max) < -9000.0f) ? NAN : id(g_temp_today_max); - platform: template name: "Main Power Temp Min Yesterday" id: temp_min_yesterday unit_of_measurement: "°C" accuracy_decimals: 1 update_interval: 10min lambda: |- return (id(g_temp_yesterday_min) > 9000.0f) ? NAN : id(g_temp_yesterday_min); - platform: template name: "Main Power Temp Max Yesterday" id: temp_max_yesterday unit_of_measurement: "°C" accuracy_decimals: 1 update_interval: 10min lambda: |- return (id(g_temp_yesterday_max) < -9000.0f) ? NAN : id(g_temp_yesterday_max); - platform: template name: "Main Power Temp Min This Month" id: temp_min_month unit_of_measurement: "°C" accuracy_decimals: 1 update_interval: 30min lambda: |- return (id(g_temp_month_min) > 9000.0f) ? NAN : id(g_temp_month_min); - platform: template name: "Main Power Temp Max This Month" id: temp_max_month unit_of_measurement: "°C" accuracy_decimals: 1 update_interval: 30min lambda: |- return (id(g_temp_month_max) < -9000.0f) ? NAN : id(g_temp_month_max); - platform: template name: "Main Power Temp Min Last Month" id: temp_min_last_month unit_of_measurement: "°C" accuracy_decimals: 1 update_interval: 60min lambda: |- return (id(g_temp_last_month_min) > 9000.0f) ? NAN : id(g_temp_last_month_min); - platform: template name: "Main Power Temp Max Last Month" id: temp_max_last_month unit_of_measurement: "°C" accuracy_decimals: 1 update_interval: 60min lambda: |- return (id(g_temp_last_month_max) < -9000.0f) ? NAN : id(g_temp_last_month_max); ######################################################################################## # EV WALLCHARGER PULSE ENERGY (GPIO14) ######################################################################################## - platform: pulse_counter id: ev_pulse_rate_ppm internal: true pin: number: GPIO14 mode: ${ev_pulse_pin_mode} inverted: false update_interval: 10s internal_filter: ${ev_pulse_filter_ms} total: id: ev_pulses_raw_total internal: true on_value: then: - lambda: |- uint32_t raw = (uint32_t) x; uint32_t last = id(g_ev_pulses_raw_last); if (raw < last) { last = 0; } id(g_ev_pulses_total_abs) += (raw - last); id(g_ev_pulses_raw_last) = raw; - platform: template name: "${ev_power_name}" id: ev_power_avg_w unit_of_measurement: "W" accuracy_decimals: 0 update_interval: "${ev_power_avg_sample_interval}" lambda: |- return id(g_ev_power_avg_w); - platform: template name: "${ev_total_energy_name}" id: ev_total_energy_kwh unit_of_measurement: "kWh" accuracy_decimals: 3 update_interval: 10s lambda: |- return ((float) id(g_ev_pulses_total_abs)) * (${ev_pulse_wh} / 1000.0f); - platform: template name: "${ev_today_name}" id: ev_energy_today_kwh unit_of_measurement: "kWh" accuracy_decimals: 3 update_interval: 60s lambda: |- const float total = id(ev_total_energy_kwh).state; if (isnan(total) || id(g_ev_day_start_kwh) < 0.0f) return NAN; float v = total - id(g_ev_day_start_kwh); if (v < 0.0f) v = 0.0f; return v; - platform: template name: "${ev_yesterday_name}" id: ev_energy_yesterday_kwh unit_of_measurement: "kWh" accuracy_decimals: 3 update_interval: 10min lambda: |- return id(g_ev_yesterday_kwh); - platform: template name: "${ev_month_name}" id: ev_energy_this_month_kwh unit_of_measurement: "kWh" accuracy_decimals: 3 update_interval: 10min lambda: |- const float total = id(ev_total_energy_kwh).state; if (isnan(total) || id(g_ev_month_start_kwh) < 0.0f) return NAN; float v = total - id(g_ev_month_start_kwh); if (v < 0.0f) v = 0.0f; return v; - platform: template name: "${ev_last_month_name}" id: ev_energy_last_month_kwh unit_of_measurement: "kWh" accuracy_decimals: 3 update_interval: 30min lambda: |- return id(g_ev_last_month_kwh); - platform: template name: "${ev_year_name}" id: ev_energy_this_year_kwh unit_of_measurement: "kWh" accuracy_decimals: 3 update_interval: 30min lambda: |- const float total = id(ev_total_energy_kwh).state; if (isnan(total) || id(g_ev_year_start_kwh) < 0.0f) return NAN; float v = total - id(g_ev_year_start_kwh); if (v < 0.0f) v = 0.0f; return v; - platform: template name: "${ev_last_year_name}" id: ev_energy_last_year_kwh unit_of_measurement: "kWh" accuracy_decimals: 3 update_interval: 60min lambda: |- return id(g_ev_last_year_kwh); #:########################################################################################:# # SCRIPTS: #:########################################################################################:# script: # Initialize RAM-only history arrays and counters - id: init_history_arrays mode: queued then: - lambda: |- for (int i = 0; i < 365; i++) { id(g_mains_daily_hist_kwh)[i] = NAN; } id(g_mains_daily_hist_index) = 0; id(g_mains_daily_hist_count) = 0; // Init EV pulse history buffer so rolling average starts stable after boot const int ev_buf = ${ev_power_avg_buffer_size}; const uint32_t p0 = id(g_ev_pulses_total_abs); for (int i = 0; i < ev_buf; i++) { id(g_ev_pulses_hist)[i] = p0; } id(g_ev_pulses_hist_index) = 0; id(g_ev_pulses_hist_count) = ev_buf; id(g_ev_power_avg_w) = 0.0f; # EV rolling average update (based on absolute pulse total) - id: ev_power_avg_update mode: queued then: - lambda: |- const int buf = ${ev_power_avg_buffer_size}; int idx = id(g_ev_pulses_hist_index); if (idx < 0 || idx >= buf) idx = 0; const uint32_t p_now = id(g_ev_pulses_total_abs); id(g_ev_pulses_hist)[idx] = p_now; idx = (idx + 1) % buf; id(g_ev_pulses_hist_index) = idx; if (id(g_ev_pulses_hist_count) < buf) id(g_ev_pulses_hist_count)++; int win = ${ev_power_avg_window_samples}; if (win < 1) win = 1; if (win > buf) win = buf; if (id(g_ev_pulses_hist_count) < win) { return; } int old_idx = idx - win; if (old_idx < 0) old_idx += buf; const uint32_t p_old = id(g_ev_pulses_hist)[old_idx]; const uint32_t dp = (p_now >= p_old) ? (p_now - p_old) : 0; const float wh = ((float) dp) * (${ev_pulse_wh}); const float seconds = ((float) win) * (${ev_power_avg_sample_interval_seconds}); if (seconds <= 0.0f) { id(g_ev_power_avg_w) = 0.0f; return; } // W = Wh * 3600 / seconds id(g_ev_power_avg_w) = wh * 3600.0f / seconds; # Period rollover check (day/month/year) for: # - Main total energy (sum of 3 PZEM channels) # - EV total energy (pulse derived) # - Temperature min/max rollovers (today/yesterday, month/last_month) - id: rollover_check mode: restart then: - lambda: |- auto now = id(sntp_time).now(); if (!now.is_valid()) { return; } const int d = now.day_of_month; const int m = now.month; const int y = now.year; const float mains_total = id(mains_total_active_energy_kwh).state; const float ev_total = id(ev_total_energy_kwh).state; // Initialize date markers on first valid time if (id(g_last_year) == 0) { id(g_last_day) = d; id(g_last_month) = m; id(g_last_year) = y; if (!isnan(mains_total)) { id(g_mains_day_start_kwh) = mains_total; id(g_mains_month_start_kwh) = mains_total; id(g_mains_year_start_kwh) = mains_total; } if (!isnan(ev_total)) { id(g_ev_day_start_kwh) = ev_total; id(g_ev_month_start_kwh) = ev_total; id(g_ev_year_start_kwh) = ev_total; } return; } // Helper: protect against meter reset / wrap (total < baseline) auto clamp_baseline = [&](float total, float &baseline) { if (baseline < 0.0f) baseline = total; if (total < baseline) baseline = total; }; if (!isnan(mains_total)) { clamp_baseline(mains_total, id(g_mains_day_start_kwh)); clamp_baseline(mains_total, id(g_mains_month_start_kwh)); clamp_baseline(mains_total, id(g_mains_year_start_kwh)); } if (!isnan(ev_total)) { clamp_baseline(ev_total, id(g_ev_day_start_kwh)); clamp_baseline(ev_total, id(g_ev_month_start_kwh)); clamp_baseline(ev_total, id(g_ev_year_start_kwh)); } // --- YEAR ROLLOVER --- if (y != id(g_last_year)) { if (!isnan(mains_total) && id(g_mains_year_start_kwh) >= 0.0f) { float ykwh = mains_total - id(g_mains_year_start_kwh); if (ykwh < 0.0f) ykwh = 0.0f; id(g_mains_last_year_kwh) = ykwh; id(g_mains_year_start_kwh) = mains_total; } if (!isnan(ev_total) && id(g_ev_year_start_kwh) >= 0.0f) { float ykwh = ev_total - id(g_ev_year_start_kwh); if (ykwh < 0.0f) ykwh = 0.0f; id(g_ev_last_year_kwh) = ykwh; id(g_ev_year_start_kwh) = ev_total; } id(g_last_year) = y; } // --- MONTH ROLLOVER --- if (m != id(g_last_month)) { if (!isnan(mains_total) && id(g_mains_month_start_kwh) >= 0.0f) { float mkwh = mains_total - id(g_mains_month_start_kwh); if (mkwh < 0.0f) mkwh = 0.0f; id(g_mains_last_month_kwh) = mkwh; id(g_mains_month_start_kwh) = mains_total; } if (!isnan(ev_total) && id(g_ev_month_start_kwh) >= 0.0f) { float mkwh = ev_total - id(g_ev_month_start_kwh); if (mkwh < 0.0f) mkwh = 0.0f; id(g_ev_last_month_kwh) = mkwh; id(g_ev_month_start_kwh) = ev_total; } // Temperature month rollover id(g_temp_last_month_min) = id(g_temp_month_min); id(g_temp_last_month_max) = id(g_temp_month_max); id(g_temp_month_min) = 9999.0f; id(g_temp_month_max) = -9999.0f; id(g_last_month) = m; } // --- DAY ROLLOVER --- if (d != id(g_last_day)) { if (!isnan(mains_total) && id(g_mains_day_start_kwh) >= 0.0f) { float dkwh = mains_total - id(g_mains_day_start_kwh); if (dkwh < 0.0f) dkwh = 0.0f; id(g_mains_yesterday_kwh) = dkwh; // Push into RAM ring buffer int idx = id(g_mains_daily_hist_index); if (idx < 0) idx = 0; if (idx > 364) idx = 0; id(g_mains_daily_hist_kwh)[idx] = dkwh; idx = (idx + 1) % 365; id(g_mains_daily_hist_index) = idx; if (id(g_mains_daily_hist_count) < 365) id(g_mains_daily_hist_count)++; id(g_mains_day_start_kwh) = mains_total; } if (!isnan(ev_total) && id(g_ev_day_start_kwh) >= 0.0f) { float dkwh = ev_total - id(g_ev_day_start_kwh); if (dkwh < 0.0f) dkwh = 0.0f; id(g_ev_yesterday_kwh) = dkwh; id(g_ev_day_start_kwh) = ev_total; } // Temperature day rollover id(g_temp_yesterday_min) = id(g_temp_today_min); id(g_temp_yesterday_max) = id(g_temp_today_max); id(g_temp_today_min) = 9999.0f; id(g_temp_today_max) = -9999.0f; id(g_last_day) = d; } # Update temperature min/max (today + month) - id: update_temp_minmax mode: queued then: - lambda: |- const float t = id(env_temp).state; if (isnan(t)) return; if (t < id(g_temp_today_min)) id(g_temp_today_min) = t; if (t > id(g_temp_today_max)) id(g_temp_today_max) = t; if (t < id(g_temp_month_min)) id(g_temp_month_min) = t; if (t > id(g_temp_month_max)) id(g_temp_month_max) = t; #:########################################################################################:# # SWITCHES: #:########################################################################################:# switch: - platform: restart name: "Main Power Monitor Restart" #:########################################################################################:# # ROLLBACK COPY (v1.1) - EV POWER SECTION (for quick revert) # NOTE: This is commented out. Remove the new EV blocks and uncomment to restore old behavior. #:########################################################################################:# #sensor: # - platform: pulse_counter # id: ev_pulse_rate_ppm # pin: # number: GPIO14 # mode: ${ev_pulse_pin_mode} # inverted: false # update_interval: 10s # internal_filter: ${ev_pulse_filter_ms} # name: "${ev_power_name}" # unit_of_measurement: "W" # accuracy_decimals: 0 # filters: # - lambda: |- # return x * (${ev_pulse_wh} * 60.0f); # total: # id: ev_pulses_raw_total # internal: true # on_value: # then: # - lambda: |- # uint32_t raw = (uint32_t) x; # uint32_t last = id(g_ev_pulses_raw_last); # if (raw < last) { # last = 0; # } # id(g_ev_pulses_total_abs) += (raw - last); # id(g_ev_pulses_raw_last) = raw;