13 KiB
CLAUDE.md - Boiler Auxiliary Heater Custom Integration
Purpose
Home Assistant custom integration (boiler_aux_heater_3phase) 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 optional circulation pump control
- Preset-based temperature control - Normal, Eco, Boost, Sleep, Away presets with adjustable target temperatures
- All-modes boost - allow the aux heater to run whenever the thermostat demands heat (not just PV/bath boost)
- Manual override - force heater ON for testing/emergency
Architecture Overview
The integration follows a coordinator pattern. BoilerAuxHeaterCoordinator is the central brain that:
- Listens to external entity state changes (boiler temp, grid power, relay state, heat pump aux heater)
- Reads internal entity states (switches, numbers) via direct references
- Runs decision logic to determine whether the heater relay should be ON or OFF
- Controls the physical relay via
switch.turn_on/switch.turn_offservice 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 x1-3, 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 all-modes heating demand (separate larger hysteresis + recovery offset)
+-- 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, stop_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), bath boost remaining timer, decision_reason, pv_state |
switch.py |
4 config switches: manual_override, allow_usage, pv_excess_mode, boost_all_modes |
number.py |
15 configurable number entities (temperatures, thresholds, delays, power rating, etc.) |
button.py |
Bath boost start + cancel buttons (only created if circulation pump is configured) |
services.yaml |
Service definitions for start_bath_boost and stop_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_3phase, version=1.0.0, iot_class=local_push |
dashboard.yaml |
Ready-to-import Lovelace dashboard for monitoring and control |
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_stopbath_boost_active- whether bath boost is runningdecision- final ON/OFF decision for the relaydecision_reason- human-readable string explaining the current decisionpv_sufficient- whether PV export exceeds heater power needsrelay_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, 1-3x CT power sensors, heater relay, optionally HP aux heater). Changes are debounced (1s) before triggering _recalculate().
PV State Machine:
idle->waiting_to_startwhen PV is sufficient (starts delay timer)waiting_to_start->activewhen start delay elapses and PV still sufficientwaiting_to_start->idleif PV drops before delayactive->waiting_to_stopwhen PV drops (starts stop delay timer)waiting_to_stop->activeif PV recovers before stop delaywaiting_to_stop->idlewhen stop delay elapses
Bath Boost Flow:
async_start_bath_boost()stores pre-boost preset, turns on circulation pump, sets climate to Boost preset, starts countdown timerasync_stop_bath_boost()can be called manually (cancel button / service) to end early_bath_boost_finished()callback fires when timer expires - turns off boost, restores pump schedule, restores previous preset, fires HA event
Boost State Persistence: Bath boost state (active, end_time, pre_boost_preset) is persisted to HA storage so it survives HA restarts.
BoilerAuxHeaterClimate (climate.py)
Single climate entity with:
- HVAC modes:
heat,off - Presets: Normal, Eco, Boost, Sleep, Away
- Uses
RestoreEntityto persist state across restarts current_temperaturereads from external boiler temp sensorhvac_actionreports 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_activeon coordinator; any other preset deactivates it (callsasync_stop_bath_boost)
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.
A periodic 60-second energy tick (async_track_time_interval) notifies listeners to accumulate energy during steady state when no external events fire.
PvTargetTempNumber (number.py)
Special subclass of BoilerAuxHeaterNumber whose native_max_value is dynamically capped at cut_off_temp - 1, preventing the PV target from exceeding the safety cut-off.
Decision Logic (coordinator.py:_recalculate)
The heater relay is turned ON when (evaluated in priority order):
1. manual_override → ON (bypasses everything)
2. NOT allow_usage → OFF
3. hp_aux_heater sensor unavailable → OFF (safety)
4. hp_aux_on → OFF (interlock)
5. boiler_temp >= cut_off_temp → OFF (safety)
6. hvac_mode == "off" → OFF
7. pv_excess_mode AND pv_shadow_heating → ON (PV excess)
8. bath_boost_active AND main_thermostat_heating → ON (bath boost)
9. boost_all_modes AND all_modes_heating_demand → ON (all-modes boost)
10. otherwise → OFF
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 ON (heater disabled) if unavailable - SAFETY interlock
PV sufficient calculation (supports 1 or 3 phases):
- When relay is OFF: each active CT phase must export more than
(buffer + heater_power) / phase_count - When relay is ON (hysteresis): each active phase must still be exporting (value <= 0)
Main thermostat heating (hysteresis):
- Turns ON when
boiler_temp < target - temp_hysteresis - Turns OFF when
boiler_temp >= target + MAIN_HOT_TOLERANCE (0.0)
All-modes heating demand (separate, larger hysteresis):
- Turns ON when
boiler_temp < target - boost_all_modes_hysteresis - Turns OFF when
boiler_temp >= target - boost_recovery_offset
Config Flow (config_flow.py)
3-step wizard:
- Core Hardware: boiler temp sensor (temperature device class) + heater relay switch
- Solar Monitoring: phase count (1 or 3) + CT power sensors (power device class) for grid monitoring
- 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 | Hysteresis for main thermostat and PV shadow thermostat |
pv_target_temp |
55 | 40-70 °C | Target temp for PV shadow thermostat (max capped at cut_off - 1) |
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 |
boost_all_modes_hysteresis |
10 | 3-20 °C | How far below target before all-modes boost activates |
boost_recovery_offset |
3 | 0-15 °C | How close to target before all-modes boost deactivates |
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 to trigger relay outside PV/bath boost modes |
Known Issues / Design Notes
-
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. -
Energy sensors use
time.monotonic()- Energy integration accuracy depends on how frequently_recalculatefires (debounced to 1s on external state changes) plus the 60s periodic tick. Long periods without either would not accumulate energy. -
Single instance only - Config flow enforces one instance via unique_id. Multiple boiler setups would need code changes.
-
3-phase assumption for full accuracy - PV calculation with 3 phases requires all 3 phases to independently meet the threshold. Single-phase mode supported (only CT1 used).
-
Bath boost button conditional -
button.pyonly creates the bath boost buttons ifcirculation_pumpis configured. Thestart_bath_boost/stop_bath_boostservices are always registered regardless. -
Relay control via service calls - The coordinator calls
switch.turn_on/turn_offservices to control the relay rather than directly setting state. Failure is caught and logged but doesn't retry. -
_enable_turn_on_off_backwards_compat = Falsein climate - opts out of HA 2024.x backwards compatibility forturn_on/turn_off. -
No
async_setup(YAML config) - Integration only supports config entries (UI setup), not YAML configuration.
Patterns Used
- RestoreEntity on climate, switch, number, and energy sensors for state persistence across restarts
- Persistent storage (
homeassistant.helpers.storage.Store) for bath boost state across restarts - Entity registration with coordinator via
register_*()methods duringasync_added_to_hass - Data-driven entity creation from
NUMBER_DEFINITIONS,SWITCH_DEFINITIONS,BINARY_SENSOR_DEFINITIONSdicts - Debounced state change handling with
async_call_later(1s debounce) - Translation keys (
_attr_translation_key) for all entities with translations intranslations/en.json - DeviceInfo with shared identifiers groups all entities under one device in HA