Archived
Fixing frequent switching
This commit is contained in:
@@ -0,0 +1,200 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user