1328 lines
45 KiB
YAML
1328 lines
45 KiB
YAML
#:########################################################################################:#
|
|
# 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.4 2026-07-12 Added Home Assistant Energy Dashboard metadata.
|
|
# Marked cumulative PZEM, mains total, and 32A Wallcharger total energy
|
|
# sensors as energy / total_increasing.
|
|
# Marked mains and EV power sensors as power / measurement.
|
|
# Added single-process compilation limit to reduce Builder memory use.
|
|
# 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)
|
|
# - 32A Wallcharger Total Energy is cumulative pulse-derived energy in kWh
|
|
# - Cumulative active-energy sensors are exposed to Home Assistant with:
|
|
# device_class: energy
|
|
# state_class: total_increasing
|
|
# - This allows the cumulative totals to be selected in the HA Energy Dashboard
|
|
# - 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.4"
|
|
|
|
# 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_charger_name: "32A Wallcharger"
|
|
ev_power_name: "${ev_charger_name} Power"
|
|
ev_charging_name: "${ev_charger_name} Charging"
|
|
ev_total_energy_name: "${ev_charger_name} Total Energy"
|
|
ev_today_name: "${ev_charger_name} Energy Today"
|
|
ev_yesterday_name: "${ev_charger_name} Energy Yesterday"
|
|
ev_month_name: "${ev_charger_name} Energy This Month"
|
|
ev_last_month_name: "${ev_charger_name} Energy Last Month"
|
|
ev_year_name: "${ev_charger_name} Energy This Year"
|
|
ev_last_year_name: "${ev_charger_name} 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}"
|
|
|
|
# Restrict PlatformIO to one simultaneous compiler process to reduce host RAM use.
|
|
compile_process_limit: 1
|
|
|
|
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
|
|
unit_of_measurement: "W"
|
|
device_class: power
|
|
state_class: measurement
|
|
accuracy_decimals: 0
|
|
energy:
|
|
name: "${pzem_red_e_name}"
|
|
id: pzem_red_energy_kwh
|
|
unit_of_measurement: "kWh"
|
|
device_class: energy
|
|
state_class: total_increasing
|
|
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
|
|
unit_of_measurement: "W"
|
|
device_class: power
|
|
state_class: measurement
|
|
accuracy_decimals: 0
|
|
energy:
|
|
name: "${pzem_yellow_e_name}"
|
|
id: pzem_yellow_energy_kwh
|
|
unit_of_measurement: "kWh"
|
|
device_class: energy
|
|
state_class: total_increasing
|
|
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
|
|
unit_of_measurement: "W"
|
|
device_class: power
|
|
state_class: measurement
|
|
accuracy_decimals: 0
|
|
energy:
|
|
name: "${pzem_blue_e_name}"
|
|
id: pzem_blue_energy_kwh
|
|
unit_of_measurement: "kWh"
|
|
device_class: energy
|
|
state_class: total_increasing
|
|
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"
|
|
device_class: power
|
|
state_class: measurement
|
|
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"
|
|
device_class: energy
|
|
state_class: total_increasing
|
|
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"
|
|
device_class: power
|
|
state_class: measurement
|
|
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"
|
|
device_class: energy
|
|
state_class: total_increasing
|
|
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; |