Latest Updates (v3.15.49–v3.15.55)
Battery Policy — DP Optimizer Fixes
-
getSlotbiased 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
chargeSocDeltaGandchargeKwhFullfor slot 0 based on the fraction of the slot still remaining (slot0RemainingFrac), so it correctly plans additional charge slots when needed (e.g.charge=2instead ofcharge=1) -
vPreserveopportunity cost (v3.15.53) — The DP previously treated PV charging duringpreserveslots as free: storing surplus PV had zero cost in the value function. It now subtracts the foregone export revenue (storedKwh × price × exportRatio) fromvPreserve. This corrects the bias toward preserve when PV could profitably be exported instead, makingstandbymore competitive at low positive prices
Battery Policy — Planning & Consumption
-
Consumption slot timezone fix (v3.15.49) — Price records now carry explicit Amsterdam
hour/minutefields. When present, consumption lookups usegetPredictedConsumptionForSlot()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,
consumptionWPerSlotis passed asnullto 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;nullcorrectly instructs the optimizer to use unconstrained max discharge power (800 W) -
pvStoreWinssimulation in planning forward pass (v3.15.49) — The optimizer’s forward pass now simulates the_pvStoreWinsoverride that the runtime policy engine applies. When a standby slot would havepvStoreWinsactive (PV surplus worth more than current export price), the planning chart showszero_charge_onlyand updates the projected SoC accordingly — matching what actually happens at runtime -
Planning slot reasons (v3.15.49) —
_mapActionToHwModeForPlanningnow returns areasonstring alongsidehwMode(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. Previouslythis._pvProductionW ?? 0was 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 = 0correction (v3.15.55) — The P1/DSMR firmware incorrectly reports battery power as 0 W when the battery is into_fullmode. 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 SoC —
chargeSocDeltaGwas 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 viazero_charge_onlywould consume capacity before cheaper grid-charge slots, partially missing the best −€0.367 windows -
PV accuracy protected at negative prices —
recordPvAccuracy()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,_cycleRevenueand_cycleCostare 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_fullregardless of PV state. Charging at negative prices earns money, so PV-mode guards (zero_charge_only,pvStoreWins) are bypassed
UI
- Learning status always current —
learning_statusis now written on every_updateWeathercall (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.5was 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.setallocates ~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_liveStatein memory and is served viaapi.js— never written tohomey.settings -
Settings page live-state —
Homey.geton the settings page now automatically merges in-memory live-state via a GET/getLiveStateAPI 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
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 —
_getPvWForTimestampnow 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
-
_recomputeOptimizermade 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
socProjectedwas 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 enforcesmin: 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 -
mainWswas 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 localwsvariable 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 assumedunitCount × 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_wandmax_production_ware now only updated when actually present in the WS payload (typeof === 'number'). Previously, missing fields were written as0, which caused the??fallback to pass through0instead 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()(viatoLocaleStringwithEurope/Amsterdamtimezone) instead ofgetHours()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_profilesetting
Optimizer Engine — Refactoring
-
Pure DP kernel - Backward induction extracted into
_runBackwardDP()— a fully side-effect-free method returning{dp, policy, ...}. Forward pass remains incompute(). EnablescomputeExpectedProfit()to reuse the same DP logic without touching_schedule -
Projected profit tracking -
_schedulenow includesprojectedProfit(€) from the DP value function at current SoC, used by expansion analysis