Skip to content

hirofairlane/ha-energy-optimizer

Repository files navigation

Energy Optimizer — Home Assistant Add-on

Smart energy management add-on for Home Assistant OS. Its primary goal is to minimise your electricity bill — whether you have a solar battery, a set of schedulable loads (heat pump, EV charger, pool pump, irrigation…), or both. A scikit-learn ML model combined with dynamic tariff rules decides in real time when to charge or discharge the battery, when to shift loads to cheap/solar windows, and how much to pre-charge at night — so you import as little peak-rate energy as possible and export as little solar as possible.

🔒 100% local — no cloud, no telemetry. Everything runs inside the Docker container on your own hardware. Your energy data, sensor readings, and ML model never leave your home network. The add-on only communicates with your local Home Assistant instance and, optionally, a local InfluxDB instance.

image

Installation: Settings → Add-ons → Add-on store → ⋮ → Repositories → add https://github.com/hirofairlane/ha-energy-optimizer

Python dependencies (bundled in Docker image)

Library Version Purpose
scikit-learn ≥ 1.3 GradientBoostingRegressor + Pipeline + StandardScaler for SOC prediction
Flask ≥ 3.0 Internal API and web panel (port 8765)
APScheduler ≥ 3.10 Decision cycle and retrain scheduling
requests Home Assistant REST API client
numpy / pandas Feature engineering and time-series resampling

Table of contents

  1. Installation
  2. Features
  3. Setup Wizard
  4. Web panel
  5. Energy flow diagram
  6. Battery charging logic
  7. Savings calculation
  8. ML model
  9. Solar terrain correction
  10. InfluxDB integration
  11. Configuration reference
  12. Electricity tariff
  13. Persistent data
  14. Changelog

v5 deep dive: the predictive engine introduced in v5.0.0 has its own architecture write-up at docs/architecture-v5.md and a wiring guide for activating the dormant v5_engine_enabled flag at docs/v5-wiring.md.


Installation

Requirements: Home Assistant OS or Supervised (any architecture: amd64, aarch64, armv7).

  1. In HA go to Settings → Add-ons → Add-on store → ⋮ menu → Repositories and add:
    https://github.com/hirofairlane/ha-energy-optimizer
    
  2. Find Energy Optimizer in the store and click Install.
  3. Start the add-on and open the web panel — the Setup Wizard will guide you through the rest.

No YAML editing required. All configuration is done through the wizard and the web panel.

What you need

Required Optional but recommended
Battery Any smart battery with HA entities for SOC + charge/discharge power Working mode select, cutoff SOC, force-charge switch (Huawei Luna2000 natively supported)
Solar Forecast.Solar or PVforecast integration Real-time production sensor
History HA Recorder (14-day window) InfluxDB for 90-day ML training window
Schedulable loads At least one (heat pump, pool pump, EV charger, or any switch.*) Multiple loads — each adds a scheduling optimisation layer

The add-on is useful even with no battery if you have loads you can shift: it will schedule them to coincide with solar surplus or valley-tariff windows.


Features

Feature Description
Setup Wizard 8-step guided configuration — auto-discovers your HA entities with ML-based scoring. No YAML editing required
image | **Smart valley charging** | Calculates exactly how much battery to charge from the grid at night to cover tomorrow's peak demand — no more, no less | | **Energy flow diagram** | Animated SVG showing real-time power flows between Solar, Battery, Grid, and House nodes | image

| Live 7-day averages | Each sensor card shows instantaneous value + Ø7d rolling average (solar: daylight hours only) | | Solar terrain correction | Learns your real production vs HA forecast from InfluxDB history — corrects for local shading automatically | | Temperature-aware target | Cold days = more heat pump during peak hours → charges more battery | | Storm protection | Reads weather entity; pre-charges to a configurable reserve when adverse weather is imminent | | Heat pump control | Adjusts setpoints based on season, indoor temperature, and free solar surplus (SOC ≥ 99%) | | Pool pump | Runs during solar surplus or valley tariff to meet daily/weekly runtime targets | | Pool cleaner | Auto-starts with pool pump, auto-stops after 15 min (~1.5 kWh) | | Dishwasher | Monitored; recommendation to run during surplus or valley | | ML SOC prediction | GradientBoostingRegressor trained on up to 90 days of history. Dynamic features from wizard config | | Prediction accuracy chart | Live SOC: actual vs ML-predicted (24h) + 8h forward forecast. Shows MAE badge | | Solar history charts | 7-day and 12-month Actual vs HA Forecast line charts | | Daily savings chart | 7-day bar chart with € value labels; counterfactual method | | Historical averages | Day view KPI cards show all-time Ø below each value. History tab adds a 5-card summary with all-time and last-12-months averages for solar, consumption, export, import, and self-sufficiency | | Battery ROI calculator | Enter cost + capacity of additional storage — calculates payback period from your actual average daily savings | | Battery health mode | Three operating modes in Tweaks: ⚡ Bill Reducer (10–95%), ⚖️ Optimized (20–90%), 🛡️ Battery Guard (25–85%). Controls the SOC range used for the nightly charge target | | Split battery sensors | For inverters that report charge and discharge as two separate positive entities (Deye, Solarman, Growatt…), enable the "Split sensors" toggle in the Battery wizard step and pick the two entity IDs. The add-on combines them automatically (charge − discharge) | | Telegram instant alerts | Emergency charge, storm mode, forced grid charge | | Daily summary | HTML email + Telegram report at configurable time | | Debug section | Tweaks tab shows how each sensor role resolves (wizard → options → fallback) with live HA value |


Setup Wizard

The wizard is the recommended way to configure the add-on. It runs through 8 steps and auto-discovers your HA entities using a keyword + device class + unit scoring algorithm.

Data → Location → Grid → Solar → Battery → Loads → Tariff → Done

Steps

Step What it configures
Data History source: InfluxDB v2 (recommended, 90 days) or HA Recorder (14 days)
Location GPS coordinates and timezone — used for solar geometry and ML features
Grid Grid meter sensor (import/export)
Solar Production sensor + Forecast.Solar sensors (today/tomorrow/hourly)
Battery SOC sensor, charge/discharge power, working mode select, charge cutoff, backup SOC, force charge switch
Loads Per-device sub-wizards for each enabled appliance (see Supported loads)
Tariff Contracted power, tariff type, peak/shoulder/valley prices
Done Data quality score summary and save

Data Quality Thermometer

The wizard header shows a live quality score (0–100%):

Score Meaning
≥ 70% 🟢 Enough data for reliable ML predictions
40–70% 🟠 Partial data — predictions will work but with higher uncertainty
< 40% 🔴 Insufficient data — add InfluxDB or wait for recorder history to accumulate

Score factors: history source (30 pts) + key sensor coverage (25 pts) + sample count bonus (30 pts) + optional sensors (15 pts).

Entity auto-discovery

For each sensor role, the wizard queries all HA entities and scores them:

Signal Points
Matching device_class +50
Matching unit of measurement +40
Each matching keyword in entity ID +25

Top candidates are shown as clickable cards. The selected entity is saved to wizard_config.json and takes precedence over options.json for all decisions.

Supported loads

The Loads step shows a card for each appliance type. Select the ones present in your installation — the wizard then walks through a sub-wizard for each one. Each selected device also appears as a small emoji dot in the wizard navigation bar.

Load Icon What you configure What the engine does
HVAC 🌡️ Climate entity or heat/cool setpoint numbers per zone. 24h schedule with Comfort 🟢 / Surplus 🔵 / Minimum ⚫ temperature tiers Raises/lowers setpoints based on tariff period, solar surplus (SOC ≥ 99%), and indoor temperature. Multi-zone supported
Pool pump 🏊 Pool switch + optional runtime sensors (daily/weekly hours) Runs during solar surplus (SOC ≥ 99%) or valley tariff to meet runtime targets
Pool cleaner 🤿 Cleaner switch entity Auto-starts with the pool pump, auto-stops after 15 min (~1.5 kWh)
Dishwasher 🍽️ State sensor + optional switch Monitors cycle state; recommends (or triggers) start during solar surplus or valley
Washing machine 👕 State sensor + power meter Monitors cycle; recommends start during cheapest/greenest window
Dryer 🌀 State sensor + power meter Same as washing machine
EV Charger 🚗 Switch or number entity (Wallbox, OCPP, Zappi…) Schedules charging during valley tariff or solar surplus
Custom ⚙️ Any switch.* entity + estimated watts + schedule preference Schedules any switch-controlled load (irrigation pump, water heater, etc.) according to the selected window

Custom load scheduling options

When adding a Custom load you choose one of four scheduling modes:

Mode When the switch is turned on
🌙 Valley tariff only During the cheapest grid tariff window (typically 00:00–08:00)
☀️ Solar surplus only When solar production exceeds house consumption (SOC ≥ 99%)
☀️🌙 Solar + Valley Either of the above
Custom hours A fixed time range you specify (e.g. 10-14,22-06)

Multiple Custom loads can be added (e.g. irrigation pump + water heater), each with its own entity, wattage, and schedule.


Web panel

Five tabs, accessible via HA ingress (port 8765, no external port needed):

Tab Contents
📊 Dashboard Live KPIs · 4-card power panel · animated energy flow diagram · battery card with manual charge buttons · smart target reasoning · recent decision log
📈 Charts SOC actual vs predicted (24h) + MAE · Solar 7d actual vs forecast · Solar 12m actual vs forecast · Daily savings 7d · Power flow 24h
Tariff Per-day weekend config · per-hour timeline · price editor · Reset to defaults
⚙️ Setup (Tweaks) Notification toggles · battery threshold sliders · decision interval · Data Sources connectivity test · Debug: sensor resolution table
🧙 Wizard Full setup wizard (see above)

Setup (Tweaks) tab

The Tweaks tab is the runtime control panel — all changes take effect immediately without restarting the add-on.

Section What it does
Battery health mode Three-button selector controlling the SOC operating range for the nightly charge target (see below).
Notifications Enable/disable email daily summary, Telegram daily summary, and instant Telegram alerts individually.
Battery thresholds Sliders for emergency, low, medium, and storm SOC thresholds. Adjust without editing config.yaml.
Decision interval How often (in minutes) the optimization engine runs. Default 15 min.
Data Sources Connectivity test for InfluxDB and HA Recorder — shows last-read timestamp, row count, and active/fallback status.
Debug: sensor resolution Table that auto-loads when you open the tab and shows exactly how every sensor role was resolved. A Refresh button re-reads live HA values.

Battery health mode

Mode SOC range Description
Bill Reducer 10% – 95% Default. Uses full battery capacity every cycle — maximum daily savings.
⚖️ Optimized 20% – 90% Sweet spot for most installations: ~95% of savings benefit with moderate cycle protection.
🛡️ Battery Guard 25% – 85% Prioritises longevity — recommended for batteries older than 3 years or with visible capacity degradation.

The selected mode clamps the nightly charge target: the engine will never set a target below the mode's minimum or above its maximum, regardless of the calculated optimal. Saved in /data/setup.json and applied immediately without restart.

Debug sensor resolution table

Column Meaning
Role Internal name (e.g. solar_power, battery_soc)
Entity ID The HA entity resolved for this role
Source wizard (wizard_config.json) · options (config.yaml) · fallback (built-in default)
Value Current state read from HA at refresh time
Status ✓ valid reading · ⚠ entity exists but value is unexpected · ✗ not found or unavailable

This is the first place to look when a Dashboard card shows 0 W or "unavailable" — it shows whether the problem is entity resolution or a real sensor issue.


Day view

A date navigator (← Today →) plus five KPI cards: Solar / Consumed / Exported / Imported / Self-sufficiency. Each card shows the day's total and, below it, the all-time daily average (Ø) computed from all recorded days. Hover over a card for a tooltip explaining the calculation. Below the KPIs: a stacked hourly energy bar chart and a SOC line for the selected day.

History view

A 5-card summary row at the top shows all-time and last-12-months daily averages for all five KPIs — useful for spotting seasonal patterns or tracking improvement over time.

Below the charts, the Battery ROI calculator lets you enter the cost (€) and extra capacity (kWh) of additional storage and calculates:

  • Your current average daily savings (from savings.json)
  • Estimated extra savings proportional to the added capacity
  • Payback period in days or years

The calculator uses a linear proportionality assumption. Real results depend on your tariff, usage patterns, and how often the battery is the limiting factor.

Live power panel

Four cards updated every 30 seconds, each showing instantaneous value + Ø7d rolling average:

Card Color Average logic
☀️ Solar Yellow Mean only when solar > 0 W (excludes night)
⚡ Grid Green (export) / Red (import) Net mean (positive = selling)
🔋 Battery Green (charging) / Red (discharging) Net mean
🏠 House Orange Mean of consumption samples > 0

Energy flow diagram

An animated SVG diagram shows real-time power flows between four nodes:

image

Sensor conventions

Sensor Convention
solar Always ≥ 0 W
grid Positive = exporting (selling), Negative = importing (buying)
battery Positive = charging (energy IN to battery), Negative = discharging (energy OUT)

House balance: P_casa = solar − grid − battery

Flow priority order

A. Solar → Casa  (first priority)
   Solar → Battery  (surplus charging)
   Solar → Grid  (remaining export)

B. Battery → Casa  (discharge covers remaining house load)
   Battery → Grid  (excess discharge if any)

C. Grid → Battery  (emergency: grid covers what solar can't charge)
   Grid → Casa  (grid covers remaining house load)

Each line is animated only when its flow exceeds 10 W. Animation speed is proportional to power: speed = max(0.35, 2.4 − power/1800) seconds per cycle.


Battery charging logic

Core principle

At night (00:00–08:00) the tariff is valley (cheapest). Importing from the grid at night is cheap — the battery's job is to store cheap valley electricity so the house can avoid importing expensive peak electricity the next day.

The question the system answers:
"How full does the battery need to be at the start of tomorrow's peak hours so I never need to import from the grid at peak prices?"

Step-by-step calculation

1. Solar forecast (terrain-corrected)

solar_forecast_raw = tomorrow's production forecast (kWh)
terrain_factor     = median(actual_day / forecast_day) over last 30 days
solar_tomorrow     = solar_forecast_raw × terrain_factor

2. Solar during peak hours

~45% of daily production falls in the 10:00–15:00 morning peak window:

solar_during_peak = solar_tomorrow × 0.45

3. Peak consumption estimate

From average grid power during the last 14 nights (22:00–08:00) via InfluxDB:

peak_base_kwh = base_load_kW × 8   (8 peak hours)

4. Temperature correction

Outdoor temperature Correction
< 5 °C +3.0 kWh
5–10 °C +2.0 kWh
10–15 °C +1.0 kWh
15–25 °C 0 kWh
25–30 °C +0.5 kWh
> 30 °C +1.5 kWh

5. Battery charge target

battery_gap_kwh = max(0, peak_total_kwh − solar_during_peak_kwh)
target_SOC      = (battery_gap_kwh / battery_capacity_kWh) × 100 + 5%
target_SOC      = clamp(target_SOC, 30%, 95%)

The Dashboard "Smart target" line shows the full breakdown in real time.

Other charging rules

Situation Action
SOC < emergency threshold (default 10%) Force-charge at any tariff, any time
Storm forecast Pre-charge to storm threshold (default 80%)
Valley + SOC below smart target Charge at configured power
Peak tariff No grid charging under any normal circumstance
SOC ≥ 99% (free solar surplus) Heat pump boost / pool pump starts

Savings calculation

Methodology: counterfactual baseline

For every 15-minute decision cycle:

# Sensor conventions:
#   grid_power    < 0 → buying from grid (import)
#   grid_power    > 0 → selling to grid  (export)
#   battery_power > 0 → battery charging
#   battery_power < 0 → battery discharging

# Without battery: what would the grid meter read?
grid_without_battery = grid_power + battery_power

def energy_cost(g, import_price, export_price):
    if g < 0:
        return -g × interval_hours × import_price / 1000   # cost of import
    else:
        return -g × interval_hours × export_price / 1000   # income from export

saving = energy_cost(grid_without_battery) - energy_cost(grid_power)

Daily savings are the sum of all interval savings.


ML model (scikit-learn)

scikit-learn is bundled inside the Docker image — no manual installation needed.

What it predicts

A GradientBoostingRegressor wrapped in a scikit-learn Pipeline(StandardScaler → GBR) predicts the battery SOC for the current moment from recent sensor readings. Used as a sanity check and to populate the predicted-SOC line in Charts.

Dynamic features

The feature set is built from the sensors configured in the wizard. If a sensor has no history, it is excluded. The feature list is saved alongside the model:

Feature Condition
hour, weekday, month Always
lag1, lag4, roll4 (SOC lags) Always
solar_proxy (geometric sun elevation 0.0–1.0) Always
temp_out, temp_out_lag4 If outdoor temp sensor has history
solar_lag1, solar_roll4 If solar sensor has history
grid_lag1, grid_roll4, grid_abs_lag1 If grid sensor has history
sm_{name}_lag1 For each configured sub-meter

Training

  • Source: InfluxDB v2 (wizard config, 90 days) → InfluxDB v1 (options, 60 days) → MariaDB direct (60 days, recorder DB) → HA Recorder REST (14 days)
  • Schedule: nightly at 03:00 (retrain_cron option)
  • Pipeline: StandardScaler → GradientBoostingRegressor(n_estimators=150, max_depth=4)
  • Validation: 3-fold cross-validation R² shown in Dashboard
  • Auto-retrain: triggers when feature version changes

8-hour forward forecast

Computed by chaining single-step predictions, updating lag features with each predicted value. Shown as a dashed line in Charts.


Solar terrain correction

HA solar forecast sensors (Forecast.Solar, etc.) use panel orientation and capacity but have no knowledge of local terrain. Hills or buildings cause systematic over-prediction that the system learns and corrects automatically.

Every 6 hours, from 30 days of InfluxDB history:

ratios = [actual_day_D / forecast_day_{D-1} for each day D]
terrain_factor = median(ratios)
terrain_factor = clamp(terrain_factor, 0.30, 1.50)

The median is robust against cloudy-day outliers. Requires at least 7 days of data. Shown as "Terrain factor: XX%" in the Dashboard.


InfluxDB integration

InfluxDB is the primary data source for ML training and multi-day charts. HA Recorder is the fallback.

Connection

Parameter Default Notes
influxdb_url http://172.30.32.1:8086 HA supervisor bridge IP (standard for HAOS)
influxdb_db homeassistant
influxdb_user (empty) Leave empty if auth disabled
influxdb_password (empty)

Auth is auto-detected: tries with credentials first, retries without if InfluxDB returns 401.

Data format

The HA→InfluxDB integration (pre-2023) stores data as:

  • Measurement = unit of the sensor (%, W, kWh, °C)
  • Tag entity_id = sensor name without domain prefix (e.g. battery_state_of_capacity)

MariaDB / MySQL recorder integration

If your HA recorder is backed by MariaDB or MySQL and the REST history endpoint returns empty results (a known issue for some installations — see GitHub issue #2), the add-on can read history directly from the recorder database, bypassing the REST API.

Configured from the Setup Wizard's first step. The schema is auto-detected:

  • Modern (HA ≥ 2023.4): JOIN states_meta sm ON s.metadata_id = sm.metadata_id WHERE sm.entity_id = ?
  • Legacy: WHERE entity_id = ? directly on states

Connection parameters (kept in wizard_config.json only — no secrets in options):

Field Typical value
Host / IP core-mariadb (HA add-on) or your MariaDB server
Port 3306
Database homeassistant
Username / Password as configured in the recorder db_url

The Test connection button counts samples for every configured sensor over the last 60 days; if any returns rows, MariaDB is used as the active source on subsequent cycles. Falls back to HA Recorder REST otherwise.


Configuration reference

Since v3.0 the Setup wizard (Setup tab → "Configure entities") is the recommended way to fill these. The wizard scans your HA states, scores candidates by name/unit/device_class and proposes the best match for each role. All options can also be set in the HA add-on Configuration UI as a fallback, but the wizard's /data/wizard_config.json takes priority over them for all sensor lookups.

Sensors

Option Default Description
sensor_battery_soc sensor.battery_state_of_capacity Battery state of charge (%) — Huawei Modbus standard
sensor_battery_power sensor.battery_charge_discharge_power Charge/discharge power (W, +ve = charging) — Huawei Modbus standard
sensor_grid_power (empty) Grid meter (W, +ve = export, −ve = import)
sensor_solar_power (empty) Panel output right now (W, always ≥ 0)
sensor_solar_current_hour sensor.energy_current_hour Solar production this hour (kWh) — HA Energy dashboard
sensor_solar_next_hour sensor.energy_next_hour Solar forecast next hour (kWh)
sensor_solar_today sensor.energy_production_today Cumulative production today (kWh)
sensor_solar_tomorrow sensor.energy_production_tomorrow Forecast for tomorrow (kWh)
sensor_temp_outdoor (empty) Outdoor temperature (°C)
sensor_temp_salon (empty) Indoor temperature (°C)
sensor_weather (empty) Weather entity (for storm detection)

Actuators

Option Default Description
switch_pool (empty) Pool pump switch
switch_pool_cleaner (empty) Pool cleaner switch
number_hvac_cool (empty) Heat pump cooling setpoint
number_hvac_heat (empty) Heat pump heating setpoint
number_battery_charge_cutoff number.battery_grid_charge_cutoff_soc Battery grid charge cutoff SOC — Huawei Modbus standard
number_battery_charge_power (empty) Battery charge power limit
number_battery_backup_soc (empty) Battery backup SOC
switch_battery_force_charge (empty) Battery force charge switch
select_battery_mode select.battery_working_mode Battery working mode select — Huawei Modbus standard
sensor_dishwasher_state (empty) Dishwasher state sensor

Battery thresholds

Option Default Description
battery_emergency_threshold 10% Force-charge below this SOC at any tariff
battery_low_threshold 30% Low battery level
battery_medium_threshold 50% Medium battery level
battery_storm_threshold 80% Pre-charge target when storm is forecast
battery_capacity_kwh 10.0 Total usable battery capacity (kWh)

Scheduling

Option Default Description
decision_interval_minutes 15 How often the optimization cycle runs
retrain_cron 0 3 * * * Nightly ML retrain schedule
summer_start_month 6 First month of summer mode
summer_end_month 9 Last month of summer mode

Notifications

Option Default Description
notify_email_service (empty) HA notify service name for email
notify_email_target (empty) Recipient email address — set in HA add-on config UI, not in repo
notify_telegram_service (empty) HA notify service name for Telegram
notify_daily_time 23:00 Time to send daily summary
notify_email_enabled true Enable email daily summary
notify_telegram_daily_enabled true Enable Telegram daily summary
notify_telegram_alerts_enabled true Enable instant Telegram alerts

Electricity tariff

Default prices — Spain 2.0TD, all costs prorated including taxes and IVA:

Period Hours Price
Peak (Punta) Weekdays 10–14h, 18–22h €0.2234/kWh
Shoulder (Llano) Weekdays 08–10h, 14–18h, 22–00h €0.1483/kWh
Valley (Valle) 00–08h + all day weekends €0.1147/kWh
Export (Excedentes) €0.040/kWh

All prices and weekend days are editable per-hour in the Tariff tab. Use "↩ Reset to defaults" to restore the above values.


Persistent data

All data lives in /data/ inside the add-on container (persists across restarts and updates):

File Contents
model.pkl Trained scikit-learn pipeline + metadata (R², feature list, feature version)
wizard_config.json Entity IDs, hardware selection, HVAC zones, tariff from the setup wizard
decisions.json Last 500 decision cycles — full sensor snapshot, tariff, actions, prediction
savings.json Cumulative kWh avoided at peak + EUR saved since first run
tariff.json Custom tariff configuration (periods, prices, weekend days)
setup.json GUI runtime overrides — notification toggles, threshold sliders

Changelog

v5.0.15 — Narrative daily summary + Δ verdict numeric collapse

The daily summary (sent at notify_daily_time to email + Telegram) used to dump every action of the day as a flat table — 200-500 rows, mostly noise. Useless as a "what did the engine do today" briefing. Rewritten as a concise causal narrative.

New _summarize_day_narrative(decisions, sensors) builds one line per relevant decision category, each phrased as WHAT happened and WHY:

⚡ Grid charge → 95% from 02:30 to 07:45: Valley — smart target 95%.
❄️ Cooling active 60 min today (~4 cycles).
🌬️ Cooling skipped 60 cycles — main reason: Cool night forecast (60×).
🏊 Pool: 1.4 h runtime today (13:15–15:00) — Solar surplus.
🌙 Night-forecast gate active: AEMET min 14.0°C (≤ threshold) — surplus saved for the battery.

Both Telegram and email use the same narrative. Email keeps the four KPI tiles (SOC / Solar / Cycles / Savings) on top; the long actions-table is gone.

Drive-by fix on the quiet log mode (v5.0.12): the verdict fingerprint that decides whether to emit [Δ ...] lines used to truncate to 60 chars and could split a number in half, so a 0.1 °C drift in t_outdoor would dirty-flag the verdict every cycle. Replaced with a regex that collapses every number to * before comparing. Symptoms: spurious Δ-spam right after a rebuild stops on this release.

Both changes are pure logging / UX — zero impact on the decision logic itself.

Drive-by 3: ha_set_number, ha_set_select and ha_switch now treat an empty entity_id as a silent no-op (return True) instead of forwarding the call to HA. Previously, when an optional integration entity wasn't present in the wizard (e.g. battery_backup_soc on inverters that don't expose a writable backup-SoC register), the service call went to HA with entity_id="" and came back as a 400/500 once per cycle — 96 spurious WARNINGs per day. Silently skipping is the right semantics: "no entity → nothing to do, success".

v5.0.14 — Comfort feedback endpoint

Two new endpoints to start collecting subjective comfort data ("¿estoy bien, frío o caliente?") with full sensor snapshot:

  • POST /api/comfort_feedback body {feedback: "frio"|"perfecto"|"calor", note: "..."} → appends a JSON line to /data/comfort_feedback.jsonl capturing current sensors (t_indoor/t_outdoor/zones, ΔT_hp, SoC, solar, hour, weekday). Logs as [COMFORT] perfecto @ t_in=21.2 t_out=18.7.
  • GET /api/comfort_feedback?limit=N returns the last N entries for review / future calibration.

Designed to be called from a Home Assistant automation tied to an input_select.confort_subjetivo, so logging a feedback is one tap on a Lovelace card. With ~30 samples it becomes feasible to calibrate per-hour temp_comfort to Sergio's actual perception rather than a global default.

v5.0.13 — Forecast-nocturno cooling gate

If AEMET predicts a cool night ahead (default ≤ 18 °C), don't burn solar surplus on running the heat pump for cooling — the house will free-cool overnight via open windows + radiant-floor inertia. The surplus stays in the battery.

  • New wizard role aemet_night_low (legacy key sensor_aemet_night_low). Maps to a numeric AEMET forecast sensor, typically sensor.aemet_daily_forecast_temperature_low.
  • New cycle gate in _hp_zone_decision() and _hp_legacy_decision(): when surplus is detected and aemet_night_low ≤ cooling_skip_if_night_low_c (default 18.0), the cooling action is skipped with reason Cool night forecast (X.X°C ≤ Y.Y°C): skip cooling, the house will free-cool tonight. Logged as [COOLING-GATE].
  • No-op when the role isn't mapped (sensor value None / 0 / placeholder).
  • Hierarchy: thermal override > free-cooling-outdoor gate > night-forecast gate (new) > surplus cooling > inactive.

New option (with schema): cooling_skip_if_night_low_c: 18.0.

v5.0.12 — Quiet log mode: changes-only + hourly heartbeat

Each 15-min cycle was emitting ~15-20 INFO lines, almost all of them identical to the previous cycle. Useful when debugging a regression, useless to keep an eye on day-to-day operation. New log_quiet_mode option (default false):

  • When enabled, a _QuietCycleFilter Python logging filter intercepts every INFO record that fires inside run_cycle() and drops it unless it's the cycle banner or the new structured summary lines.
  • At the end of the cycle, _emit_cycle_summary() compares the current verdict per category against the one persisted in /data/log_summary_state.json and emits one INFO line per category that changed:
    [Δ battery] Mid — opportunistic top-up to smart target → Valley, at optimal level
    [Δ heat_pump:aerotermia_principal] Surplus cooling → Inactive (no surplus)
    
  • Once per hour an [HB] heartbeat compacts all current verdicts into a single line — confirms the addon is alive even when nothing has changed.
  • WARNING/ERROR records bypass the filter unconditionally. The Activity tab JSON (decisions.json) is not filtered — full history is preserved for analysis.

To opt in: set log_quiet_mode: true in addon options.

v5.0.11 — Reset cooling setpoint when surplus ends

When the addon decided "Surplus cooling" during the solar peak it wrote an aggressive value to the heat-pump cooling setpoint (e.g. Z1CoolingTemp = 16 °C on Sergio's install — the floor of the slider's range). When the surplus window closed, the decision flipped to Inactive (no surplus) and the slider was left untouched. It stayed at 16 °C for hours and overnight, even though the addon was no longer asking for cooling. A manual flip of the heat pump or any ebusd internal loop would then ramp the unit hard against an inappropriate target.

Now the addon detects the edge active → skip (per zone) and writes the configured cooling_off_setpoint_c (default 25.0 — matches the Z1CoolingTemp value from Sergio's 2026-05-07 baseline) to the slider. The transition is logged as:

[HP/aerotermia_principal] active→skip transition → reset number.ebusd_ctls2_z1coolingtemp_tempv to 25.0°C

Steady-state cycles do NOT keep writing — manual edits made by the user on the slider while the addon is in skip are respected until the next surplus arrives.

State persists at /data/hvac_decision_state.json so the addon doesn't lose the "did I just stop cooling?" knowledge through restarts.

New option: cooling_off_setpoint_c (default 25.0).

v5.0.10 — Manual pool ON: exact 50-min run, then auto-off (fix v5.0.9)

v5.0.9 honored a manual ON for at least 50 min but then handed control back to decide_pool(). If there happened to be solar surplus at minute 50, the pump kept running. Sergio's actual intent: flip on → run for 50 min → turn off, full stop.

  • On manual ON detection the addon now schedules a deferred APScheduler job (same pattern as the limpiafondos auto-off) that flips the switch off at the exact second T + pool_manual_min_runtime_min. The Activity tab logs it as Manual ON timer elapsed — pump auto-off.
  • Persistent state at /data/pool_manual_state.json now stores the absolute manual_off_at timestamp. On addon restart the job is restored if the deadline is still in the future, or fired immediately if it elapsed while the addon was down.
  • Cycle override during the window still applies (no surprise OFFs from decide_pool() while the timer runs), but the cycle no longer owns the cutoff — the APScheduler job does.
  • Flipping the switch off by hand cancels the pending timer cleanly.

v5.0.9 — Honour the user's manual pool switch ON (superseded by v5.0.10)

Initial implementation that respected a manual ON as a minimum runtime. Behaviour subtly wrong when solar surplus or valley tariff coincided with minute 50; replaced by v5.0.10's exact-duration timer.

v5.0.8 — Summer free-cooling gates + "open the windows" alert

In mountain climates with cool nights (Guadarrama is the reference install), running the heat pump for radiant cooling makes no sense once the outdoor air is colder than indoors. Ventilation does the same job for free. v5.0.7 was still happy to fire the thermal override at 23:00 with 16 °C outside.

Two gates added to _hp_zone_decision() / _hp_legacy_decision(), both inert if sensor_temp_outdoor isn't wired (behaviour matches v5.0.7 in that case):

  • Override gate. If t_indoor > hvac_summer_override_c but t_outdoor ≤ t_indoor − free_cooling_delta_c (default 2 °C), the heat pump is not started. Instead, the addon emits a [COOLING-GATE] log line and sends a Telegram alert telling you to open the windows. The alert is rate-limited to one every notify_open_windows_cooldown_h hours (default 4).
  • Surplus gate. If there's solar surplus and t_outdoor ≤ cooling_outdoor_max_c (default 22 °C), cooling is also skipped — better to keep the surplus charging the battery than to spend it cooling air that's already cold outside.

New options (all live under cfg() with safe defaults):

key default meaning
cooling_outdoor_max_c 22.0 Skip surplus cooling when outdoor is at or below this temperature
free_cooling_delta_c 2.0 How much colder outdoor must be vs indoor to skip the thermal override
notify_open_windows_enabled true Toggle the Telegram alert
notify_open_windows_cooldown_h 4.0 Minimum hours between repeat alerts

State for the alert deduplication persists at /data/cooling_gate_state.json so addon rebuilds don't reset the cooldown timer.

v5.0.7 — Heat-pump hydraulic loop as ground-truth signal

After a 24 h audit on Sergio's install we learned that the obvious "did the heat pump run?" signals — aerotermia_consumo_electrico, status01_pumpstate — are silently broken on his ebusd integration (the first stays unavailable, the second sticks at off even when the unit is clearly running). The only sensors that always tell the truth are the two water-loop temperatures: ida (water leaving the unit) and vuelta (water coming back). When they're equal the unit is idle. When ida is colder than vuelta the unit is cooling. When ida is hotter than vuelta it's heating. No ambiguity, no desync.

  • New wizard roles hp_flow_in_temp and hp_flow_out_temp resolve to two existing sensors. Sergio's install maps them to sensor.aerotermia_exterior_status01_temp1 and sensor.aerotermia_exterior_status01_temp1_2 respectively.
  • Per-cycle hydraulic log line. When both roles are configured, every run_cycle() emits:
    [HYDRAULIC] flow_in=13.5°C  flow_out=16.5°C  ΔT=-3.0°C → COOLING
    
    The verdict is HEATING for ΔT > +1 °C, COOLING for ΔT < −1 °C, idle otherwise. The sign convention (ΔT = ida − vuelta) matches Sergio's existing template sensor aerotermia_delta_t_calefaccion so the two read the same direction.
  • Three new fields in sensors and decisions.json (hp_flow_in_c, hp_flow_out_c, hp_delta_t_c) so the Activity tab and any downstream analysis have the ground truth recorded per cycle. Installs without the roles configured fall through silently — no log line, no extra fields.

v5.0.6 — Smart-target floor + night-consumption baseline (issue #8)

Two related bugs in the valley-charge planner reported by @andredp on issue #8, both compounding to under-fill the battery for tomorrow's peak on self-consumption installs.

  • Bug A — max(30, _hmin) shadows battery_health_mode. The smart-target final clamp, the emergency target and the recovery target all forced a hardcoded 30 % floor on top of the user's configured health-mode minimum, silently overriding bill_reducer (10) and battery_guard (25→30, paradoxically lower than guard's actual floor on some edges). Three call sites: calculate_optimal_soc (:822), decide_battery emergency floor and decide_battery low-target. All three now read directly from _health_mode_limits()[0] so the Tweak page selection flows end-to-end. bill_reducer users get smart_target down to 10 % when solar is plentiful tomorrow; battery_guard users get 25 %.
  • Bug B — Night consumption baseline ignores battery discharge. _refresh_consumption_cache computed abs(grid_power) averaged over 22:00–08:00. On self-consumption installs the battery covers the load and the grid value is near zero — Sergio's cache had been showing 1.1 kW avg night only because of occasional pre-2026-05-30 nights when the battery had not yet been topped up. Fix: pull solar_power, battery_power and grid_power together, align them on a 15-min grid, and compute house = solar + batt − grid (with the addon's positive=exporting convention for grid and positive=discharging for battery). Falls back to the legacy abs(grid) calculation for installs whose wizard does not have solar/battery wired (grid-only setups). The new log line is explicit:
    Consumption cache (house = solar + batt − grid): 0.512 kW avg night (892 samples)
    
  • Migration. No setup changes needed for installs that already have solar/battery wired in the wizard. Pure-grid setups keep the old code path with no behavioural change. Users on bill_reducer who were quietly being floor-clamped at 30 % will see their smart target dip toward 10 % on sunny-forecast days — that is the requested behaviour.

v5.0.5 — Surplus detection with hysteresis (radiant cooling never engaged on real installs)

Direct follow-up to a 48 h audit on Sergio's install. After v5.0.3 enabled radiant cooling under "solar surplus", the engine never once actuated the cooling path across two days in a heatwave (SoC stayed in the 80–95 % band, the cooling setpoint was skip-ed every cycle, the aerotermia never started the compressor). Root cause: free_power = (soc >= 99 and batt_power > 0) is a knife-edge condition. SoC sits at 99–100 % only a handful of minutes per day around the peak, and with a 15-minute cycle the engine almost always misses the window.

  • New surplus detector with hysteresis. Added _has_surplus_power() that crosses on at SoC ≥ 95 % with the battery not charging (batt_power ≥ 0) and only drops off when SoC < 90 % OR the battery starts charging hard (batt_power < −500 W). All three thresholds are tunable via setup.json (surplus_enter_soc, surplus_exit_soc, surplus_exit_charge_w). The active flag is persisted to /data/surplus_state.json so addon restarts don't lose context.
  • Avoids start/stop chatter. The 5 % SoC and 500 W power gates between entry and exit are wide enough to absorb cycle-to-cycle noise (cloud shading, occasional inverter ramps, opportunistic loads) without bouncing the engine in/out of surplus mode.
  • Both decision paths updated. _hp_zone_decision and _hp_legacy_decision now use the hysteresis detector wherever they previously called the raw (soc>=99 and batt_power>0) check. Reason strings updated from "Free solar power (SOC≥99%)" to "Solar surplus (hysteresis)" so logs match the actual mechanic.
  • No tuning changes shipped by default. The defaults match the values audited on Sergio's install (95 / 90 / −500), and hvac_summer_override_c stays at 29. Installs that want different bands set them in setup.json and the addon picks them up on the next cycle.

v5.0.4 — set_battery_self_consumption honours Battery Health Mode

Follow-up to v5.0.2 reported by @andredp on issue #7. The 5.0.2 fix correctly reset battery_backup_soc at peak start, but did so against the hardcoded default of 20 %, silently shadowing the user's selection on the Tweak page → Battery Health Mode card. Effect: even after upgrading, a bill_reducer install kept the floor at 20 instead of 10 (loses ~10 % of usable capacity every peak window), and a battery_guard install dropped to 20 instead of staying at 25 (more aggressive than the user asked for).

  • Root cause. The two call sites — the autonomous cycle when decide_battery returns self_consumption, and the manual POST /api/battery/self-consumption endpoint — both invoked set_battery_self_consumption() with no argument, taking the function's hardcoded default of min_soc=20. The same battery_health_mode setting that already drives the charging floors via _health_mode_limits() (bill_reducer→(10,95) / optimized→(20,90) / battery_guard→(25,85)) was not consulted on the release side.
  • Fix. set_battery_self_consumption(min_soc=None): when no explicit floor is passed, derive it from _health_mode_limits()[0]. Both call sites continue to call it with no argument, so the user's Tweak selection now flows end-to-end without any additional wiring.
  • Migration. Anyone who selected a non-default mode and noticed their battery being more conservative than expected at peak will see the floor change after upgrading: bill_reducer 20 → 10, battery_guard 20 → 25. optimized users are unaffected (mode floor is already 20). Explicit external API callers passing a custom min_soc keep their behaviour.

v5.0.3 — Radiant cooling regime + manual season selector

Adds a conservative summer regime for installs whose cooling side is a high-inertia radiant floor (or any system where overnight cooling is wasteful) and lets users flip seasons manually from a Home Assistant helper instead of relying on the calendar.

  • New season_select wizard role. Resolves to a Home Assistant input_select (typically input_select.season with options summer / winter, Spanish verano / invierno also accepted). When configured, the addon trusts that selector end-to-end — _season() returns its state and _is_summer() derives from it. When not configured, the legacy month-based heuristic (summer_start_month / summer_end_month) keeps working. The selector is also honoured by decide_pool, so pool hours-per-day vs hours-per-week switch with the same flip as the HVAC regime.
  • Conservative summer HVAC regime. _hp_zone_decision and _hp_legacy_decision now refuse to actuate the cooling setpoint unless one of two conditions holds: (a) a clear solar surplus (SOC ≥ 99 % with the battery exporting, or the zone schedule cell is surplus), or (b) indoor temperature crosses the new hvac_summer_override_c threshold (default 29 °C). Outside those windows the decision is reported as skip: True, the activity feed records the inaction, and no number.set_value or climate.set_temperature is sent — critical for radiant floors, which would otherwise be primed cold overnight at valley prices and stay cold through the morning when there's nothing to dump into.
  • No behaviour change for winter. The heating branch keeps the existing surplus / comfort / minimum ladder; only the summer branch was inverted.
  • Migration. Nothing required for installs that don't add the new selector — the month-based default keeps the previous behaviour modulo the summer inactivity rule. Installs that do want the radiant regime should: (1) create input_select.season in HA, (2) point the wizard's new "Season selector entity" field at it, (3) optionally tune hvac_summer_override_c in setup.json. A companion design for the HA side (templates that swap thermostats with the same selector) ships under docs/season-switch.md.

v5.0.2 — GoodWe support: opt-in mode select + battery_backup_soc reset

Fixes two bugs in the battery control layer that prevented Energy Optimizer from working on GoodWe inverters and could leave any installation with an inflated minimum discharge SOC after a valley charge. Reported and diagnosed by @andredp on a GoodWe GW3648-EM with the mletenay HACS integration (issue #7).

  • Bug A — mandatory mode select. set_battery_charge_target and set_battery_self_consumption unconditionally called select.select_option against the configured battery_mode_select entity. The hardcoded fallback was a Huawei Luna2000-specific id (select.battery_working_mode), so a GoodWe install with the wizard field left blank still fired the call against a non-existent entity, got a 400 back, and tainted the whole battery operation as ok=False. GoodWe doesn't need a mode select at all — goodwe_fast_charging_switch + goodwe_fast_charging_soc are standalone registers that operate independently of the inverter's operation mode.
  • Bug B — battery_backup_soc never reset. set_battery_charge_target raised battery_backup_soc to the valley charge target (e.g. 60 %) but set_battery_self_consumption never lowered it back when the engine flipped to self-consumption at peak start. On Huawei this entity is the minimum charging SOC and the silent inflation was tolerable; on GoodWe it maps to goodwe_battery_soc_protection and pinned the battery above the valley target all day, defeating the entire optimisation.
  • Fix. The mode select is now opt-in: the hardcoded Huawei default is removed, and the call is guarded with if mode_entity: so it only fires when the user explicitly populates select_battery_mode in the wizard or setup.json. set_battery_self_consumption now also resets battery_backup_soc to min_soc, mirroring how battery_cutoff_soc is already reset.
  • Migration note for Huawei users. If you were relying on the implicit select.battery_working_mode default and never set the entity in the wizard, you must add it explicitly under Setup Wizard → Battery → "Mode select entity" after upgrading. If you already configured it (the recommended path documented in the wizard since v3.x), nothing changes.

v5.0.1 — Retrain fix for InfluxDB-backed installs

Hotfix. The nightly retrain (retrain_cron, default 0 3 * * *) was silently failing on every cycle for any installation with data_source: influxdb in the wizard, leaving the model frozen at the SOC predictions it had at install time. Traceback was buried in the addon log — the engine kept producing decisions from a stale model and never surfaced the problem.

  • Root cause. _influx_query requests epoch=ms so InfluxDB returns time as an integer (Unix milliseconds), but _rows_to_15min_series assumed HA-REST style ISO strings and called .replace("Z", …) on the value. The except clause caught KeyError / ValueError / TypeError but not AttributeError, so the first row aborted the whole training set.
  • Fix. _rows_to_15min_series now detects the type of last_changed (str / int / float) and converts integer timestamps via magnitude thresholds (seconds / milliseconds / nanoseconds). AttributeError is also added to the except so a single malformed row no longer kills the batch.
  • Impact. Confirmed on a real install: the post-fix retrain produced 8637 samples × 22 features (R² 0.997), versus the previous 14-feature model trained 18 days earlier. The eight extra features are the wizard's grid_submeters (per-appliance power readings) that had been silently dropped because they were added after the last successful training.
  • Affects: HAOS installs with data_source: influxdb (the default for users who configure the InfluxDB step in the wizard). HA-REST-only installs were not affected because their last_changed is already a string.

v5.0.0 — Predictive engine rewrite

Major release. The decision engine moves from reactive heuristics to a predictive planner with explicit forecasting, deterministic SOC simulation, an iterative planner with convergence guarantees, a four-layer policy pipeline, and full per-cell traceability. The new engine is delivered behind a feature flag (v5_engine_enabled: false by default) so existing v4 installations keep running unchanged until the flag is flipped.

Why a major bump. Although wizard_config.json v3.5+ remains backward-compatible, the cycle's observable behaviour changes radically when the v5 engine is active: forecasts come from quantile heads, decisions are computed from a horizon-aware planner, and every override (capacity budget, peak prohibition, antiflap, degraded mode) is recorded in a structured trace. Calling that anything less than a major bump would be dishonest.

Architecture (under rootfs/usr/bin/eo/). Built strangler-fig style alongside the existing monolithic energy_optimizer.py. The v4 engine is untouched.

  • eo/forecasters/ClearSkyModel (deterministic PV baseline, no pvlib), AtmosphericFactorModel (ML residual with P10/P50/P90 quantile heads), SolarForecaster (composition), HouseForecaster (standalone quantile model, not chained to avoid the v3.x autoregressive inflated R²), ForecastQualityTracker (rolling MAE / bias / calibration error per series), training helpers.
  • eo/simulator/ — pure, side-effect-free 15-min-timestep SOC simulator with an injectable PhysicsModel (today SingleBatteryPhysicsModel; future EV / multi-battery without touching the simulator). Hard invariants for SOC bounds, charge/discharge mutex, inverter capacity, energy conservation, min-runtime, contradictory actions, debt monotonicity. Soft mode in production, strict mode in tests.
  • eo/planner/ — debt-state classifier with the two bug fixes from review (window-mutation truncation + telemetry-coverage scaling), 11-row decision matrix from the spec, utility-score function, iterative convergence loop with plan-hash + 2-cycle detection + MAX_PLANNER_ITERATIONS=5 guardrail. Implements the forced_states injection insight from the audit round so the simulated trajectory never diverges from execution.
  • eo/policy/ — four-layer pipeline (capacity_budgetpeak_prohibitionantiflapdegraded_mode). Each layer is a pure transform; the triple raw_plan / policy_adjusted_plan / execution_plan is preserved end-to-end. Three-level degraded mode (forecast MAE high → drop min_runtime_only decisions; AEMET stale → drop everything except rule_id=3; sensors stale > 30 min → all deferred loads OFF).
  • eo/scenario/ScenarioBuilder collapses quantiles into a single coherent Scenario whose risk tolerance is derived from the worst debt state across declared loads.
  • eo/state/ — aggregated SystemState dataclass (battery + forecast quality + planner history + load debt + antiflap + execution world state) with atomic JSON persistence (tmp + fsync + os.replace).
  • eo/execution/ — pure execution engine and cycle orchestrator. Optimistic-hybrid dispatch (no synchronous ACK wait); telemetry-driven reconciliation after restart (telemetry beats persisted state if they diverge).

Tests. 355 pytest cases across the package. Includes the three high-pressure scenarios from the spec review: "Monstruo del Bucle" (oscillating load hits max_iter cap), "Sábado de Gloria" (concurrent loads on weekend valley serialised by capacity budget), "Cap del Fin del Mundo" (unreachable quota produces irreachable debt state and Telegram alert).

Defaults. v5_engine_enabled: false. The flag turns the engine on once the addon-specific wiring is complete (forecasters trained on the user's historical data, data-source bindings for hours-on-per-day rebuilds, entity registry for is_on / send_command). Wiring guide ships in a future point release.

v4.0.2 — Setup integrity checker + first modular package

Hotfix release that lays the ground for the upcoming v5.0.0 rewrite while delivering one immediately useful fix.

  • Setup integrity checker: detects when the same entity_id is configured in multiple actuable roles in the wizard (e.g. the same switch.* used as pool_switch and as a custom_loads[].switch). This was the root cause of the "blitzwolf switching by itself" symptom reported in #4 — the pool branch issued turn_on and the custom-load branch issued turn_off in the very same decision cycle. On startup the add-on now logs every conflict, publishes binary_sensor.energy_optimizer_setup_conflict to Home Assistant (with the full conflict list as attributes), and fires a Telegram alert if any CRITICAL collision is found.
  • First module of the new eo package: eo.checks.setup_integrity is a pure, unit-tested module (18 pytest cases) introduced as the template for how v5.0.0 modules will be organised. The legacy monolithic energy_optimizer.py keeps running unchanged; new functionality grows alongside it (strangler-fig pattern).
  • pytest.ini and tests/ directory: development tests now live in the repo and run with python3 -m pytest tests/ (not shipped inside the Docker image).

v4.0.1 — Full traceability of side effects

Audit pass on top of v4.0.0 to close every "black box" — every switch toggle, every number write, every API call the add-on performs is now recorded in decisions.json and visible in the Activity tab. No silent actions.

  • Pool cleaner auto-stop: the 15-min APScheduler-deferred turn-off was firing outside the cycle and never reaching decisions.json. Now wrapped through a new _record_event() helper that persists deferred / out-of-cycle actions to the same history. Tagged scheduler_event in the UI.
  • Pool cleaner start: previously bundled implicitly with the pool pump's reason string. Now emitted as its own pool_cleaner entry with its own explanation.
  • Heat pump dual call (number.set_value + optional climate.set_temperature): previously only the number write was logged. The climate mirror call now gets its own climate_setpoint entry, tracked independently with its own ok/explanation.
  • Manual API endpoints (/api/battery/charge, /api/battery/self-consumption): when called from outside the engine (UI button, external automation, curl), the action is now recorded as a manual_api event with the requester IP, the target SOC, and the full reasoning. No more invisible overrides.
  • Activity tab visual coverage: extended the tagMap and CSS to include custom_load, pool_cleaner, climate_setpoint, dishwasher, manual_api, scheduler_event. Previously unknown types rendered as undifferentiated gray pills; now each event family has a distinct colour so external interventions are visually distinguishable from cycle decisions.

v4.0.0 — Decision engine rewrite + transparency layer

Major release. The internal decision engine has been rewritten to (a) respect every user-configurable setting that was previously silently overridden, and (b) explain what it did and why, in every cycle.

The two visible changes for everyone:

  1. Every decision now ships an explanation dict with what, why, the inputs used, the formula applied, the actual calculation, and the alternatives_rejected. The Activity tab renders an expandable row per decision (click the ▶ in front of any reason) that shows the full reasoning. The legacy reason short string is kept on every decision for backwards compatibility with downstream consumers.
  2. The smart battery target SOC adapts to your actual tariff geometry, not a hardcoded Spanish 2.0TD shape. Users with longer/shorter peak windows (Ukraine 16h peak, Australia 4h peak, Germany variable, …) now get targets that match their real consumption profile.

Decision engine fixes, by impact:

  • calculate_optimal_soc peak-hour scaling. The previous peak_base_kwh = base_kw × 8 hardcode caused systematic 2× errors on tariffs with non-Spanish peak length. Surfaced by @Karplyak in issue #5 (Ukrainian 16h peak ⇒ target stuck at ~36 % when ~70 % was right). Now scales with len(tariff.peak_hours).
  • calculate_optimal_soc solar-overlap. Previously solar_during_peak = solar_tomorrow × 0.45 (Spanish heuristic). Now computed as the overlap between the user's actual peak window and a 7-19h "useful sun" window, divided by that window's length.
  • Temperature adjustment scales with peak length. The HVAC-load bands (0/0.5/1/2/3 kWh) were tuned for an 8h peak. Now multiplied by peak_h_ratio = len(peak_hours) / 8.
  • decide_battery respects battery_health_mode. Previously hardcoded target_soc: 30/40 and if soc >= 95 ignored the user's selected profile, meaning Battery Guard (25-85 %) users would "never reach full" according to the engine. Now resolves the floor as max(30, health_mode_min) and the ceiling as health_mode_max.
  • decide_battery uses configurable battery_low_threshold. Previously hardcoded if soc < 20 ignored the option even though it was configurable. Now reads cfg("battery_low_threshold", 30).
  • decide_battery uses the configured battery_charge_power entity. Previously hardcoded 1500/2000/3000 W. Now reads the wizard's configured charge-power number entity and caps emergency charges to it.
  • Mid-tariff opportunistic top-up. Previously mid-period only reacted to dangerously low SOC. Now also tops up toward the smart target if mid is the cheapest period available before peak (for tariffs where mid-hours overlap with daytime sun, e.g. Spanish 2.0TD 14-18h).

v3.5.5

  • Custom loads scheduler actually drives the switches now (issue #4, reported by @Karplyak). The wizard's Loads step lets the user add custom_loads with a switch entity, watts estimate, and a scheduling mode (valley, solar, both, hours). The wizard was persisting them correctly to wizard_config.json, but no Python code on the engine side was consuming them — the switches were never being turned on/off by the optimizer. Implemented decide_custom_loads() and wired it into the decision cycle:
    • valley → on during P3 tariff only
    • solar → on when grid export ≥ load wattage and SOC > 30 %
    • both → on if either of the above
    • hours → on when current hour is inside any range in the user's spec (10-14,22-06 style, ranges wrap midnight) Only emits a service call when the desired state differs from the current one, so quiet hours don't spam the bus.

v3.5.4

  • Wizard "Flip sign" toggle on the Grid step actually does something now. It was being saved to wizard_config.json as grid_flip but never read back — users whose meter reports +ve = importing from grid had a no-op toggle and the engine kept seeing inverted signs (battery decisions + savings counter all biased). Reported by @Karplyak in issue #2 after testing v3.5.3.
  • _influx_wizard_history query corrected. Was using FROM "<entity_short>" while the HA→Influx integration stores measurement = unit + entity_id as TAG. The query returned an empty result for every entity and the loader silently fell back to ha_history_influx. Worked for setups with the legacy influxdb_url option populated; broken for users configuring InfluxDB only through the wizard. Switched to the same FROM /.*/ WHERE entity_id = ... pattern as the rest of the code.
  • /api/wizard/data-quality now counts grid_submeters. The endpoint iterated only over the wizard's sensors dict — the score reported OK while sub-meter feeds (Meross, etc.) used as sm_<name> features in ML training were silently uncounted. Built a unified all_entities dict spanning both, used in the InfluxDB / MariaDB / HA-Recorder loops.

v3.5.3

  • MariaDB direct query fix (issue #2, reported by @Karplyak): the recorder query was filtering by last_changed_ts, which HA only populates when the state value actually changes. Power sensors that emit the same reading repeatedly, or rows where only attributes were updated, leave last_changed_ts NULL and got silently dropped → 0 rows returned. Switched to last_updated_ts (and the legacy last_updated), which is populated on every write. Hotfix on top of v3.5.2.

v3.5.2

  • MariaDB / MySQL recorder support (issue #2): wizard now offers a third data source besides InfluxDB and HA Recorder REST. When the HA recorder is backed by MariaDB and the REST endpoint returns nothing (a known issue for some installations), the add-on can read history directly from the recorder DB. New section in the Setup Wizard's first step: host, port, database, user, password, and a Test connection button. Schema is auto-detected (legacy states.entity_id vs post-2023.4 states_meta JOIN).
  • ML predict feature-name fix: the chained 8 h SOC forecast was passing temp_out to a model trained with temp_outdoor, raising a sklearn warning every cycle. Renamed to match the trained feature; dropped two stale features (temp_out_lag4, grid_abs_lag1) that were never trained.
  • Version drift fix: ADD_ON_VERSION was 3.5.0 while config.yaml was 3.5.1; aligned both to 3.5.2.

v3.5.1

  • Savings counter rewritten with counterfactual method: the previous filter (peak hours + battery discharging + grid import > 500 W) was under-counting by ~97 % because it ignored self-consumption in valley/off-peak and the cases where the battery covered 100 % of the load. New logic compares grid cost with battery against a hypothetical no-battery scenario per cycle (grid_cf = grid + battery). JSON is backwards-compatible: total_eur_saved stays primary, total_kwh_avoided_peak is kept as secondary, total_kwh_throughput added for diagnostics. Per-cycle delta is capped at ±0.30 € as sanity.

v3.5.0

  • Single source of truth for version: ADD_ON_VERSION in energy_optimizer.py is now canonical. Build copies config.yaml to /addon-config.yaml and a startup check logs a warning if the two drift, so panel and Supervisor never disagree.
  • Wizard data-quality fix: the InfluxDB sample-count query was using entity_id as the measurement name. With the HA→InfluxDB integration the measurement is the unit (%, W, kWh, …) and entity_id is a tag — query corrected accordingly.
  • Branch consolidation: merges parallel v3.4.1 work that had diverged on a separate machine. No user-facing regressions; per-version entries below remain authoritative for individual features.

v3.4.1

  • Split battery sensors (Deye/Solarman/Growatt support): The Battery step of the Setup Wizard now has a "Split sensors" toggle for inverters that report charge and discharge as two separate positive-valued entities instead of one signed sensor. Enable the toggle and select the two entities — the engine combines them as charge − discharge so the rest of the logic behaves identically. The debug table in Tweaks also adapts to show both entities when split mode is active.

v3.4.0

  • Average consumption metrics in Charts: Day view KPI cards show all-time daily averages (Ø) below each value with tooltip explaining the calculation. History tab adds a full 5-card summary row with all-time and last-12-months averages for solar, consumption, export, import, and self-sufficiency.
  • Battery ROI calculator: New section in the History tab. Enter the cost and capacity of additional storage — the calculator uses your actual average daily savings to estimate payback time and projected annual gain.
  • Battery health mode: Three-button selector in the Tweaks tab controls the SOC operating range the engine targets: ⚡ Bill Reducer (10–95%, default), ⚖️ Optimized (20–90%), 🛡️ Battery Guard (25–85%). Affects the nightly charge target clamp. Persisted in setup.json.

v3.3.1

  • Removed all personal/installation-specific defaults from config.yaml and Python fallbacks. Add-on now ships clean for any installation.

v3.3.0

  • Sensor convention corrected: battery_power is +ve = charging, −ve = discharging throughout display, flow physics, and averages.
  • House balance: P_casa = solar − grid − battery.
  • Flow vectors rewritten with correct priority order including emergency-charge-from-grid path.
  • Battery card: green = charging, red = discharging.

v3.2.9

  • Debug section in Tweaks tab: table showing sensor role resolution (wizard → options → fallback), raw HA state, parsed value, and ✓/⚠/✗ status. Auto-loads when tab opens.

v3.2.6 – v3.2.8

  • _live_averages_7d(): W-level averages from 15-min decision samples. Solar average excludes night samples.
  • /api/debug/sensors endpoint added.
  • Physics-based flow animation: 7 computed flow vectors, speed proportional to power.

v3.0.0

  • Setup Wizard: 8-step guided configuration with entity auto-discovery, data quality thermometer, HVAC multi-zone scheduling, device sub-dots in nav bar.
  • Dynamic ML features: feature set built from wizard-configured sensors, saved alongside model.
  • Multi-source history: InfluxDB v2 → InfluxDB v1 → HA Recorder cascade.
  • _wiz() resolution: wizard_config.json → options.json → hardcoded fallback for every sensor role.

v2.6.1 – v2.6.4

  • Grid power sign convention fixed.
  • Continuous solar proxy (geometric sun elevation 0.0–1.0).
  • Solar terrain correction factor (median actual/forecast, 30 days, cached 6h).
  • Battery logic reoriented to cover tomorrow's peak demand.
  • Temperature correction for heat pump load.

v2.5.6 – v2.6.0

  • InfluxDB as primary ML source (365 days) with auth auto-detection.
  • Data Sources debug panel in Setup.
  • Solar charts: Actual vs HA Forecast (7d + 12m).
  • Savings bar chart with € labels and counterfactual method.
  • 8-hour chained ML forecast.

v2.4 – v2.5

  • Weather forecast widget (5-day, storm alert).
  • HA ingress fix (X-Ingress-Path header).
  • Tariff editor with per-day weekend configuration.
  • Telegram instant alerts.
  • 4-tab GUI: Dashboard, Charts, Tariff, Setup.

About

Energy Optimizer - Add-on para Home Assistant

Resources

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages