[APP][Pro] Homewizard 🧙‍♂️

:memo: Latest Updates (v3.15.49–v3.15.55)

Battery Policy — DP Optimizer Fixes

  • getSlot biased toward active slot (v3.15.55) — At the exact midpoint between two hourly slots (e.g. 12:30), getSlot() previously picked the next slot due to a millisecond timing offset, causing that slot’s action to be applied up to 30 minutes too early. It now always returns the most recently started slot (the one currently being executed), falling back to nearest-future only when no past slot exists yet (e.g. on first startup)

  • Partial-slot charge modelling (v3.15.55) — When the optimizer recomputes mid-slot, it previously modelled the current slot as a full 1-hour charging opportunity (0.8 kWh). This caused it to overestimate how much charge could be obtained in the remaining time and incorrectly skip the next slot. The DP now scales chargeSocDeltaG and chargeKwhFull for slot 0 based on the fraction of the slot still remaining (slot0RemainingFrac), so it correctly plans additional charge slots when needed (e.g. charge=2 instead of charge=1)

  • vPreserve opportunity cost (v3.15.53) — The DP previously treated PV charging during preserve slots as free: storing surplus PV had zero cost in the value function. It now subtracts the foregone export revenue (storedKwh × price × exportRatio) from vPreserve. This corrects the bias toward preserve when PV could profitably be exported instead, making standby more competitive at low positive prices

Battery Policy — Planning & Consumption

  • Consumption slot timezone fix (v3.15.49) — Price records now carry explicit Amsterdam hour/minute fields. When present, consumption lookups use getPredictedConsumptionForSlot() instead of deriving the hour from the UTC timestamp. Previously, UTC timestamps without a timezone indicator were shifted by +2h (CEST), causing all consumption forecasts to land on the wrong slot and return the baseload floor (~314 W) everywhere

  • Null consumption when nothing is learned (v3.15.49) — If the learning engine has not yet accumulated any non-zero consumption data, consumptionWPerSlot is passed as null to the optimizer instead of an all-zero array. An all-zero array caused the baseload floor to over-constrain discharge planning as if the house never consumed power; null correctly instructs the optimizer to use unconstrained max discharge power (800 W)

  • pvStoreWins simulation in planning forward pass (v3.15.49) — The optimizer’s forward pass now simulates the _pvStoreWins override that the runtime policy engine applies. When a standby slot would have pvStoreWins active (PV surplus worth more than current export price), the planning chart shows zero_charge_only and updates the projected SoC accordingly — matching what actually happens at runtime

  • Planning slot reasons (v3.15.49)_mapActionToHwModeForPlanning now returns a reason string alongside hwMode (e.g. dp:charge negative_price, preserve:pv_strong(3200W)). Stored in the schedule and visible in the settings UI for easier diagnostics

Battery Policy — PV Estimation

  • PV estimation fallback to weather forecast (v3.15.54)_estimatePvProduction() is now used everywhere house consumption is calculated. When no flow card is supplying live PV data (or the data is stale), it falls back to a weather-based estimate using sun score and configured PV capacity. Previously this._pvProductionW ?? 0 was used directly, causing 0 W PV during stale periods and overcounting house consumption by the full battery charge power in the learning engine

  • P1 firmware batteryPower = 0 correction (v3.15.55) — The P1/DSMR firmware incorrectly reports battery power as 0 W when the battery is in to_full mode. The battery-policy device now detects this case (mode = to_full, reported power = 0) and substitutes the configured max charge power from device state. Without this correction the learning engine recorded house consumption ~800 W too high during every grid-charging session


Previous Updates (v3.15.40)

Battery Policy — Negative Price Optimizer

  • RTE correction for physical SoCchargeSocDeltaG was previously computed including the RTE factor, causing the DP to think the battery needed 5 hours to fill instead of the physical 4 hours (at 800 W / 2.688 kWh). RTE losses are now applied only on the discharge side (dischargeValue × RTE), matching how the firmware reports physical SoC

  • Skip marginal negative slots — The DP now skips marginally negative slots (e.g. −€0.021) when better slots (e.g. −€0.367) provide sufficient capacity to fill the battery. Previously all negative slots were charged, including the least valuable ones

  • Preserve at negative price → standby — When spot price is negative and PV is strong, the battery is now held in standby instead of zero_charge_only. Charging from PV surplus via zero_charge_only would consume capacity before cheaper grid-charge slots, partially missing the best −€0.367 windows

  • PV accuracy protected at negative pricesrecordPvAccuracy() is no longer called when spot price is negative. Users often disable their inverter to avoid export costs; recording actual=0 against a positive forecast would incorrectly degrade PV learning data


Previous Updates (v3.15.38)

Battery Cycle Tracking

  • Cycle accumulators survive restarts_cycleKwhDischarged, _cycleRevenue and _cycleCost are now persisted to device store (alongside _costEnergy/_costAvg). Previously a restart during an active discharge silently reset all accumulators to zero, causing the evening discharge to be missing from cycle history and ROI tracking

  • RTE learning cycle fix — Balance guard lowered from 1.45 → 1.40. The old threshold (1.45) meant the minimum measurable RTE at guard-passage was 1/1.45 = 68.9%, just below the 70% floor — so every measurement that barely passed the guard was immediately discarded. With 1.40 the minimum is 71.4%, ensuring a valid measurement every time the guard passes

Battery Policy

  • Negative price → always charge to full — When spot price is negative, the mapper now returns to_full regardless of PV state. Charging at negative prices earns money, so PV-mode guards (zero_charge_only, pvStoreWins) are bypassed

UI

  • Learning status always currentlearning_status is now written on every _updateWeather call (hourly), not only when the optimizer runs. Previously, with policy disabled or in predictive mode, the Leerdagen/coverage/PV-accuracy pills in the settings UI would show stale values

Previous Updates (v3.15.37)

Battery Policy — PV Forecast & Learning

  • Yield-factor normalisation (v1 + v2) — Yield factors learned while radiation_bias_factor > 1.5 was active were systematically under-calibrated (biased radiation as baseline). One-time reset so they re-learn against unbiased radiation. v2 also resets the bias factor itself, which had been artificially inflated to the cap by the miscalibrated yield factors

  • Solcast moved to _updateWeather — Solcast forecast is now fetched on every weather update (including when policy is disabled), keeping the PV chart current at all times

  • Intraday PV scaling tuned — Lower learning weights for intraday correction; prevents overreaction to temporary deviations early in the day

  • Cycle recorded on discharge→charge transition — Battery cycles are now also recorded when SoC never reaches 0% (typical on PV-heavy days): trigger is the transition from discharging to charging once ≥ 0.3 kWh has been discharged

Battery Policy — Battery Mode Camera

  • Predictive modes visible in camera — In predictive mode (HW Slim Laden active) modes were not recorded because the policy does not run. The slot interval now also records mode + SoC when policy is disabled, so switches between predictive_charge, predictive_discharge, predictive_zero and predictive_standby become visible in the Battery Modes webcam

Memory & Stability

  • Startup crash fix (v3.15.35)homey.settings.set allocates ~30 MB V8 heap per call regardless of payload size. With multiple devices initialising concurrently, heap peaked at 70+ MB → Memory Warning. All drivers now use a serialised write queue (8 s between writes). Rebuildable UI state (planning, explainability, weather) lives in _liveState in memory and is served via api.js — never written to homey.settings

  • Settings page live-stateHomey.get on the settings page now automatically merges in-memory live-state via a GET /getLiveState API call. Existing render code requires no changes

  • SDM230_v2 / SDM630_v2 polling spread (v3.15.36) — When multiple SDM devices are present, the first poll is spread across the polling interval to avoid simultaneous HTTP requests

  • Initial weather and policy deferred — Weather fetch deferred to T+30 s, first policy check to T+45 s after startup. Prevents a cumulative heap peak of 70+ MB during the onInit cascade on setups with many devices

  • Device-type counter in memory log[MEM] log line now shows instance counts per driver type (energy_v2=2 plugin_battery=1 …) to speed up triage of crashes from users with different device configurations


:memo: Previous Updates (v3.15.10)

Battery Policy — Optimizer & PV Modelling

  • pvCoverage uses net PV surplus — PV coverage fraction in the DP now subtracts house consumption before dividing by max charge power: max(0, pvW − consW) / maxChargeW. Previously raw pvW was used, causing the DP to overstate free SoC gain during zero_charge_only slots, underestimate effective charge cost, and overcount cumulative pvKwhFromT. All downstream calculations (pvStrongCoverage threshold, dp.fill guard, terminal value discount) now reflect what the battery can actually absorb

  • pvKwhTomorrow is net-absorbable — Tomorrow’s PV estimate passed to the optimizer changed from raw forecast watts to net surplus after learned house consumption, further capped by battery group max charge rate. Fixes over-optimistic terminal value discounting and headroom floor decisions on days where house load consumes most of the PV output

  • dp.fill guard restricted to non-PV slots — The dp-flattening step (when PV tomorrow can fully refill the battery) is now skipped during slots with strong PV coverage. Previously flattening during PV hours caused the DP to lose sight of the evening peak value; the DP then relied on the discharge floor as a crutch and could miss profitable peaks

Battery Policy — Planning

  • Three-tier per-slot discharge floor — Each price slot now gets its own floor based on expected PV output: strong PV (surplus ≥ 50% of max charge power) → min_discharge_price (default €0.22, prevents round-trip loss during solar peak); weak PV (50–400W surplus) → break-even + €0.02; night → €0 or break-even depending on PV day. Previously a single flat floor was applied to all slots

  • Linear interpolation for policy-engine PV lookup_getPvWForTimestamp now uses linear interpolation between forecast points (consistent with the optimizer’s _getPvForSlot). The old nearest-neighbour (35-min threshold) produced different pvStrong decisions for :15/:30/:45-offset slots, causing planning and optimizer to disagree on zero_charge_only transitions

Battery Policy — PV Forecast & Planning

  • Solcast integration - Satellite-based PV forecast (30-min resolution) blended with Open-Meteo weather model. Optional, requires Solcast API key and resource ID in settings. Lazy-loaded; cached across restarts

  • Blend log split by day - [PV blend] log now shows today and tomorrow separately, making it easy to verify forecast accuracy per day

  • Self-sufficiency tracking - Daily grid import vs. house consumption accumulated in real time (15 s poll). Persisted to settings across restarts; visible in battery expansion analysis

  • SoC plan snapshot - Planned SoC per slot stored on first computation, never overwritten. Enables frontend to show “planned vs. actual” SoC for past slots

  • currentPrice fix - Widget/settings were showing the first slot (which could be in the past); now correctly uses the first future slot

  • Consumption margin - Optimizer assumes 20% higher consumption than learned average while evening patterns are still building up

  • _recomputeOptimizer made async - Required for Solcast API calls inside the optimizer path

Battery Policy — PV Camera Image & Planning UI

  • PV Opwek camera image - New third camera image (planning_pv / “PV Opwek”) on the battery-policy device shows PV actual vs forecast as a webcam-style chart. Visible in the Homey app without needing the settings page — ideal for quick access from phone or tablet. Uses quickchart.io with Chart.js 4, 900×900 resolution, dark theme

  • SoC forward simulation - Fixed stale SoC projection on the planning chart. Previously the optimizer’s socProjected was computed hours ago with a different starting SoC, causing the dashed SoC line to jump unrealistically. Now simulates forward from the real current SoC using mode + pvW per slot, producing a realistic trajectory

  • PV surplus text repositioned - “Zonne-energie vandaag” forecast text (e.g. “Batterij wordt volledig vol van zonne-energie”) moved from a separate block below the daily profit table to an inline caption between today’s PV chart and the planning hour cards

Polling & Connectivity

  • Plugin battery polling floor - Polling interval now enforced to minimum 5 seconds (Math.max(..., 5)) in all three code paths (startup, settings change, interval restart); settings UI also enforces min: 5

  • SDM230 backoff on failure - After 3 consecutive poll failures the SDM230 slows to a 60 s backoff interval. Automatically restores normal interval on next successful poll

  • Cloud WebSocket race condition fix - mainWs was assigned before the socket was ready; a concurrent reconnect could replace it mid-handshake, leaving stale event listeners firing on the wrong socket. Fixed by using a local ws variable for all event listeners, with a guard (if (this.mainWs !== ws) return) that silently drops events from superseded sockets. Also removed the redundant double-open guard that was papering over the root cause


Battery Policy — Previous (v3.15.3)

Battery Policy — Multi-Battery Discharge Fix

  • Discharge power capped at 800 W - HW firmware limits discharge (max_production_w) to 800 W regardless of battery count (charge scales linearly, discharge does not). The fallback calculation incorrectly assumed unitCount × 800 W, causing 3-battery setups to report “capaciteit: 2400W” in explainability text while actual discharge was locked at 800 W. Fixed in policy-engine, explainability-engine, and device _getBatteryState() fallback. If the max_production_w and max_consumption_w are properly set (eg. 1600w) that value will be used.

  • WebSocket capability guard - max_consumption_w and max_production_w are now only updated when actually present in the WS payload (typeof === 'number'). Previously, missing fields were written as 0, which caused the ?? fallback to pass through 0 instead of triggering the corrected 800 W fallback

  • Confidence rounding - Learning-adjusted confidence now uses Math.round() after adjustment, preventing 14-decimal-place values (e.g. 99.33326922747905) in timeline entries and flow tokens

Battery Policy — Learning Engine

  • 15-minute consumption resolution - Consumption patterns upgraded from hourly (7 × 24 = 168 slots) to 15-minute (7 × 24 × 4 = 672 slots). Includes automatic migration from old hourly format, spreading existing averages evenly across quarter slots

  • Amsterdam timezone fix - All consumption recording now uses _getAmsterdamTime() (via toLocaleString with Europe/Amsterdam timezone) instead of getHours() which returns UTC on Homey. One-time reset migration clears old UTC-indexed data; re-learning takes ~24–48h

  • Daily profile export - New getDailyProfile(dayOfWeek) method returns 96 slots with predicted wattage, enabling per-day consumption charts in the settings UI

Battery Policy — Expansion Analysis (new)

  • What-if battery comparison - New computeExpectedProfit() method on OptimizationEngine runs the DP for 1–4 battery scenarios without modifying the live schedule. Shows marginal daily/yearly profit per additional battery, power bottleneck slots where house consumption exceeds discharge capacity, and payback period based on configurable investment cost

  • Settings tab “Uitbreiding” - New tab visualises expansion scenarios with per-unit profit cards, shortfall indicators, and user-adjustable battery price input

Battery Policy — Consumption Profile Chart (new)

  • Learned consumption chart - New chart in the planning tab renders the learned 15-min consumption profile per day-of-week. Features day selector (Ma–Zo), peak detection with top-3 labels, colour-coded bars (green → yellow → red), and current-slot highlight. Updates hourly via policy_consumption_profile setting

Optimizer Engine — Refactoring

  • Pure DP kernel - Backward induction extracted into _runBackwardDP() — a fully side-effect-free method returning {dp, policy, ...}. Forward pass remains in compute(). Enables computeExpectedProfit() to reuse the same DP logic without touching _schedule

  • Projected profit tracking - _schedule now includes projectedProfit (€) from the DP value function at current SoC, used by expansion analysis


:open_book: Full Changelog

Click to expand complete version history