This repository has been archived on 2026-04-06. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
HA_Aux_Heater/custom_components/boiler_aux_heater/CLAUDE.md
T

12 KiB

CLAUDE.md - Boiler Auxiliary Heater Custom Integration

Purpose

Home Assistant custom integration (boiler_aux_heater) that manages an auxiliary electric heater mounted inside a DHW (Domestic Hot Water) boiler. The boiler's primary heat source is a heat pump; this integration controls a secondary resistive heater for:

  • PV excess utilization - diverts surplus solar export to the heater instead of feeding back to the grid
  • Boost heating - rapid hot water via bath boost timer with circulation pump control
  • Preset-based temperature control - Normal, Eco, Boost, Sleep, Away presets with adjustable target temperatures
  • Manual override - force heater ON for testing/emergency

Architecture Overview

The integration follows a coordinator pattern. BoilerAuxHeaterCoordinator is the central brain that:

  1. Listens to external entity state changes (boiler temp, grid power, relay state, heat pump aux heater)
  2. Reads internal entity states (switches, numbers) via direct references
  3. Runs decision logic to determine whether the heater relay should be ON or OFF
  4. Controls the physical relay via switch.turn_on/switch.turn_off service calls

All entities register themselves with the coordinator on async_added_to_hass and receive updates via a listener callback pattern (not HA's DataUpdateCoordinator).

External sensors (boiler temp, CT power x3, HP aux heater, relay state)
    |
    v  (async_track_state_change_event + debounce)
BoilerAuxHeaterCoordinator._recalculate()
    |
    +-- reads switch states (manual_override, allow_usage, pv_excess_mode, boost_all_modes)
    +-- reads number values (cut_off_temp, heater_power, pv_buffer, pv_target_temp, etc.)
    +-- runs PV state machine (idle -> waiting_start -> active -> waiting_stop)
    +-- computes main thermostat heating (hysteresis against climate target temp)
    +-- computes final decision = manual_override OR (safety_ok AND (pv_heating OR bath_boost OR all_modes_boost))
    |
    v
Controls physical relay + notifies all entity listeners to update HA state

File Structure

File Purpose
__init__.py Entry setup/unload, service registration (start_bath_boost), options update listener
const.py All constants: domain, config keys, presets, number/switch definitions, tolerances
coordinator.py BoilerAuxHeaterCoordinator - central logic, PV state machine, bath boost timer, relay control
config_flow.py 3-step config flow (core hardware, solar sensors, optional devices) + options flow
climate.py BoilerAuxHeaterClimate - climate entity with presets, reads boiler temp from external sensor
binary_sensor.py 5 binary sensors: decision, pv_sufficient, running_on_pv, running_on_grid, bath_boost_active
sensor.py Power sensors (total/pv/grid), energy sensors (Riemann sum integration), bath boost remaining timer
switch.py 4 config switches: manual_override, allow_usage, pv_excess_mode, boost_all_modes
number.py 13 configurable number entities (temperatures, thresholds, delays, power rating, etc.)
button.py Bath boost start button (only created if circulation pump is configured)
services.yaml Service definition for start_bath_boost
strings.json UI strings for config flow and entity names
translations/en.json English translations (mirrors strings.json + entity translations)
manifest.json Integration manifest: domain=boiler_aux_heater, version=1.0.0, iot_class=local_push

Key Classes

BoilerAuxHeaterCoordinator (coordinator.py)

Central state manager. Not a subclass of HA's DataUpdateCoordinator - uses a custom listener pattern instead.

State tracked:

  • pv_state - PV state machine: idle / waiting_to_start / active / waiting_to_stop
  • bath_boost_active - whether bath boost is running
  • decision - final ON/OFF decision for the relay
  • pv_sufficient - whether PV export exceeds heater power needs
  • relay_is_on - current physical relay state (read from external entity)
  • pv_shadow_heating - whether PV shadow thermostat logic wants heating

Entity registration: Internal entities call register_number(key, entity), register_switch(key, entity), register_climate(entity) during setup. The coordinator reads their values directly via get_number_value(key) and get_switch_state(key).

External state listening: Tracks state changes on configured external entities (boiler temp sensor, 3x CT power sensors, heater relay, optionally HP aux heater). Changes are debounced (1s) before triggering _recalculate().

PV State Machine:

  • idle -> waiting_to_start when PV is sufficient (starts delay timer)
  • waiting_to_start -> active when start delay elapses and PV still sufficient
  • waiting_to_start -> idle if PV drops before delay
  • active -> waiting_to_stop when PV drops (starts stop delay timer)
  • waiting_to_stop -> active if PV recovers before stop delay
  • waiting_to_stop -> idle when stop delay elapses

Bath Boost Flow:

  1. async_start_bath_boost() turns on circulation pump, sets climate to Boost preset, starts countdown timer
  2. _bath_boost_finished() callback turns off boost, restores pump schedule, turns off pump

BoilerAuxHeaterClimate (climate.py)

Single climate entity with:

  • HVAC modes: heat, off
  • Presets: Normal, Eco, Boost, Sleep, Away
  • Uses RestoreEntity to persist state across restarts
  • current_temperature reads from external boiler temp sensor
  • hvac_action reports HEATING when relay is on, IDLE when off
  • Setting a preset reads the target temp from the corresponding number entity
  • Setting Boost preset activates bath_boost_active on coordinator; any other preset deactivates it

Energy Sensors (sensor.py)

BoilerEnergySensor uses internal Riemann sum integration (not HA's integration platform). It accumulates energy by multiplying the heater power rating by elapsed time on each coordinator update. Uses RestoreEntity + SensorStateClass.TOTAL_INCREASING for long-term tracking. Tracks total, PV-only, and grid-only energy separately.

Decision Logic (coordinator.py:_recalculate)

The heater relay is turned ON when:

decision = manual_override OR (
    hp_aux_heater_off AND
    allow_usage AND
    boiler_temp < cut_off_temp AND
    (
        (pv_excess_mode AND pv_shadow_heating) OR
        (bath_boost_active AND main_thermostat_heating) OR
        (boost_all_modes AND main_thermostat_heating)
    )
)

Safety defaults for unavailable sensors:

  • Boiler temperature: defaults to 100.0 (heater stays OFF - above any cut-off)
  • CT power sensors: default to 0.0 (no export detected - PV won't trigger)
  • HP aux heater: treated as OFF if unavailable

PV sufficient calculation:

  • When relay is OFF: each of the 3 CT phases must export more than (buffer + heater_power) / 3
  • When relay is ON (hysteresis): each phase must still be exporting (value <= 0)

Main thermostat heating (hysteresis):

  • Turns ON when boiler_temp < target - MAIN_COLD_TOLERANCE (4.0)
  • Turns OFF when boiler_temp >= target + MAIN_HOT_TOLERANCE (0.0)
  • In tolerance band: maintains previous state

Config Flow (config_flow.py)

3-step wizard:

  1. Core Hardware: boiler temp sensor (temperature device class) + heater relay switch
  2. Solar Monitoring: 3x CT power sensors (power device class) for 3-phase grid monitoring
  3. Optional Devices: HP aux heater sensor (interlock), circulation pump switch (enables bath boost), pump schedule entity

Uses async_set_unique_id(DOMAIN) + _abort_if_unique_id_configured() - only one instance allowed.

Options flow allows reconfiguring all entity references post-setup.

Configurable Parameters (number entities, defined in const.py)

Key Default Range Purpose
cut_off_temp 60 40-85 C Hard safety limit - heater stops above this
temp_hysteresis 4 1-15 C Defined but not used in coordinator
pv_target_temp 55 40-70 C Target temp for PV shadow thermostat
pv_buffer 100 0-1000 W Extra export required before PV triggers
heater_power 4500 1500-9000 W Heater wattage (used for PV threshold + energy calc)
pv_start_delay 60 1-300 s Delay before activating PV mode
pv_stop_delay 60 1-300 s Delay before deactivating PV mode
temp_normal 44 30-65 C Normal preset target
temp_eco 40 30-60 C Eco preset target
temp_boost 50 40-70 C Boost preset target
temp_sleep 38 25-55 C Sleep preset target
temp_away 35 20-50 C Away preset target
bath_boost_duration 30 5-120 min How long bath boost runs

Mode Switches (switch entities, defined in const.py)

Key Default Purpose
manual_override OFF Bypass all logic, force relay ON
allow_usage ON Master enable for aux heater
pv_excess_mode ON Enable PV excess diversion
boost_all_modes OFF Allow main thermostat heating to trigger relay (not just PV/bath boost)

Known Issues / Design Notes

  1. temp_hysteresis number entity is defined but unused - The coordinator uses hardcoded MAIN_COLD_TOLERANCE (4.0) and MAIN_HOT_TOLERANCE (0.0) instead of reading from this entity.

  2. Custom coordinator pattern - Does not extend homeassistant.helpers.update_coordinator.DataUpdateCoordinator. Uses its own listener list and manual _notify_listeners(). This means no built-in polling fallback.

  3. Energy sensors use time.monotonic() - Energy integration accuracy depends on how frequently _recalculate fires (debounced to 1s on external state changes). Long periods without state changes won't accumulate energy even if the relay is on.

  4. Single instance only - Config flow enforces one instance via unique_id. Multiple boiler setups would need code changes.

  5. 3-phase assumption - PV calculation assumes 3-phase power with per-phase CTs. All 3 phases must meet threshold independently (no sum/average).

  6. Bath boost button conditional - button.py only creates the bath boost button if circulation_pump is configured. The start_bath_boost service is always registered regardless.

  7. Relay control via service calls - The coordinator calls switch.turn_on/turn_off services to control the relay rather than directly setting state. Failure is caught and logged but doesn't retry.

  8. bath_boost_active is toggled from two places - Both climate.async_set_preset_mode() (when selecting/deselecting Boost preset) and coordinator.async_start_bath_boost() / _bath_boost_finished() modify bath_boost_active. The bath boost timer in the coordinator and the preset selection in the climate entity are loosely coupled.

  9. No async_setup (YAML config) - Integration only supports config entries (UI setup), not YAML configuration.

  10. _enable_turn_on_off_backwards_compat = False in climate - opts out of HA 2024.x backwards compatibility for turn_on/turn_off but doesn't declare ClimateEntityFeature.TURN_ON | TURN_OFF.

Patterns Used

  • RestoreEntity on climate, switch, number, and energy sensors for state persistence across restarts
  • Entity registration with coordinator via register_*() methods during async_added_to_hass
  • Data-driven entity creation from NUMBER_DEFINITIONS, SWITCH_DEFINITIONS, BINARY_SENSOR_DEFINITIONS dicts
  • Debounced state change handling with async_call_later (1s debounce)
  • Translation keys (_attr_translation_key) for all entities with translations in translations/en.json
  • DeviceInfo with shared identifiers groups all entities under one device in HA