#:########################################################################################:#
# 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;