Initial push
This commit is contained in:
@@ -0,0 +1,215 @@
|
||||
# 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:
|
||||
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 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_stop`
|
||||
- `bath_boost_active` - whether bath boost is running
|
||||
- `decision` - final ON/OFF decision for the relay
|
||||
- `decision_reason` - human-readable string explaining the current decision
|
||||
- `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, 1-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()` stores pre-boost preset, turns on circulation pump, sets climate to Boost preset, starts countdown timer
|
||||
2. `async_stop_bath_boost()` can be called manually (cancel button / service) to end early
|
||||
3. `_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 `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 (calls `async_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:
|
||||
1. **Core Hardware**: boiler temp sensor (temperature device class) + heater relay switch
|
||||
2. **Solar Monitoring**: phase count (1 or 3) + CT power sensors (power device class) for 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 | 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
|
||||
|
||||
1. **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.
|
||||
|
||||
2. **Energy sensors use `time.monotonic()`** - Energy integration accuracy depends on how frequently `_recalculate` fires (debounced to 1s on external state changes) plus the 60s periodic tick. Long periods without either would not accumulate energy.
|
||||
|
||||
3. **Single instance only** - Config flow enforces one instance via unique_id. Multiple boiler setups would need code changes.
|
||||
|
||||
4. **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).
|
||||
|
||||
5. **Bath boost button conditional** - `button.py` only creates the bath boost buttons if `circulation_pump` is configured. The `start_bath_boost`/`stop_bath_boost` services are always registered regardless.
|
||||
|
||||
6. **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.
|
||||
|
||||
7. **`_enable_turn_on_off_backwards_compat = False`** in climate - opts out of HA 2024.x backwards compatibility for `turn_on`/`turn_off`.
|
||||
|
||||
8. **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 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
|
||||
Reference in New Issue
Block a user