Changelog¶
Changelog¶
[1.0.0b2] - 2026-06-30¶
Fixed¶
- Battery kept exporting to grid after it dropped RS485 control (#434): when a v3 silently slipped out of forced mode (ACK ok, ~0 W delivered) the non-responsive recovery toggled RS485 off → on — but the off step hands control to the battery's internal logic, which can export to grid, and the 5-minute pool exclusion then left it running free. Recovery now re-asserts RS485 without the off step and forces standby, and a battery commanded idle while still moving power is re-pinned (RS485 re-asserted + a real standby write) instead of trusting its matching set-points.
__init__.py. - Charge delay no longer unlocks early on balanced-forecast days (#4, #13): the binary "is grid needed today?" gate compared the raw solar forecast (instead of the 0.85 scheduling haircut, which flipped a balanced day into a false deficit) against a configurable deadband. A genuine grid-deficit day now holds the unlock until the cheapest import hour before solar is due, rather than releasing at the pre-dawn price peak. Thanks to @syphernl for the contribution.
control/charge_delay.py,pricing/engine.py. - PV-surplus charge delay now releases at the cheapest price hour (#12): when the delay holds to soak up PV surplus, it pulls the release forward to the cheapest price hour within the already-feasible window instead of the latest energy/time backstop, so self-charging gives up the least export value. Opt-in by data (no price data → unchanged edge-release); never moves the release later. Thanks to @syphernl for the contribution.
control/charge_delay.py,pricing/engine.py. - Transient no-forecast no longer disables Solar Charge Delay for the whole day (#11): a brief
solar_forecast_sensorgap (e.g. Forecast.Solar recomputing at the midnight rollover) latched ano_forecastfail-safe unlock as a permanent daily unlock, so the delay stayed off all morning even after the forecast returned. The fail-safe still allows charging for that cycle, butno_forecastunlocks are now re-evaluable, so the delay re-arms as soon as the forecast recovers; genuine unlocks latch as before. Thanks to @syphernl for the contribution.control/charge_delay.py. - Weekly full charge stalled an imbalanced pack at 3.58 V: the 60 s top-of-charge delta-V measurement held the battery at 0 W whenever the max cell reached the pause voltage, even during a weekly full charge. An imbalanced pack (whose highest cell hits 3.58 V well before the pack is full) ping-ponged at 3.58 V and never climbed to the real BMS cutoff. The measurement now steps aside during an active weekly charge so the taper drives the cell to the cutoff; the delta-V is still captured once at completion.
control/max_soc_charge.py. - Weekly full charge stopped near the top instead of charging to the BMS cutoff: an idle battery (≤10 W + Standby in the taper zone but not being commanded to charge) was mistaken for a real BMS cutoff, so a brief solar lull falsely marked it "full" at 94–98 % SOC and excluded it from charging for the rest of the run. A cutoff now only counts while the battery is actually commanded to charge yet refuses, and the confirmed-cutoff latch is held through the charge exclusion that follows.
control/weekly_full_charge.py.
Added¶
- Charge Delay Balance Deadband slider (kWh, default 0.5): runtime tolerance on the charge-delay energy balance — the delay only unlocks once the solar+stored shortfall exceeds it.
number.py. - Synthetic energy totals survive a delete + re-add (Zendure): lifetime charge/discharge kWh are now backed up keyed by device serial in a delete-proof Store, so re-adding a battery reclaims its totals instead of restarting at 0.
synthetic_energy_backup.py.
Changed¶
- PV-surplus charge delay scores the whole charge window, not just the start hour (#18): the cheapest-price release (#12) picked the single cheapest starting slot, but the charge runs across several slots, so a cheap start followed by pricey hours could sacrifice more export than a slightly dearer start inside a sustained trough. Each candidate start is now scored by the duration-weighted average price over
[start, start + charge_time_h], landing the whole charge in the cheapest sustained block; a window running past the available price data is skipped so an incomplete tail can't score as artificially cheap. The grid-deficit path (no defined charge length) keeps the legacy single-slot behaviour, and SOC safety is untouched — release only ever moves earlier. Thanks to @syphernl for the contribution.control/charge_delay.py,pricing/engine.py.
[1.0.0b1] - 2026-06-29¶
⚠️ Major change — new repository, integration renamed to “Omnibattery”¶
This is the continuation of Marstek Venus Energy Manager (
ffunes/marstek-venus-energy-manager), now living in a new repository —ffunes/omnibattery— and multi-brand (Marstek + Zendure). The HA domain also changed (marstek_venus_energy_manager→omnibattery). Because HACS cannot rename a custom integration's domain/folder in place, this is not an in-place update from the old repo: you install the new repo and migrate across to it. Everything is preserved — configuration, entity IDs (they staymarstek_venus_*), recorder history, long-term statistics, dashboards and automations.How to migrate (take a Home Assistant backup first; your data is safe at every step): 1. Update the old integration to its final release first. That build writes a recovery snapshot of your full configuration to disk, so nothing is lost even if the old integration is later removed. 2. In HACS → ⋯ → Custom repositories, add
https://github.com/ffunes/omnibattery(type: Integration) and install it. Leave the oldmarstek-venus-energy-managerrepository in place for now. 3. Restart Home Assistant, then go to Settings → Devices & Services → Add Integration → Omnibattery. 4. Confirm the migration when prompted. If your old config is still present it migrates seamlessly; if you had already removed the old integration, it offers to restore from the snapshot written in step 1. Either way it recreates your config on the new domain and re-links every entity, its history and stored state. 5. Once Omnibattery is running, remove the oldmarstek-venus-energy-managerintegration and repository from HACS. 6. Hard-refresh the browser (Ctrl+F5) so the renamed sidebar panel loads.This is a beta — please report anything odd on GitHub.
Added¶
- Modbus serial / RTU support (#350): a Marstek battery wired over RS485 (USB adapter) instead of WiFi can now be added by entering a serial port path (e.g.
/dev/ttyUSB0) instead of an IP. Fixed at 115200 8N1; leave the path empty for the usual Modbus TCP.infra/modbus_client.py,config_flow.py. - Zendure SolarFlow support: the integration now drives Zendure batteries (2400 AC / AC Pro / AC+) over local HTTP, auto-detecting the model. Capacity, soft-max charge and force-mode are configured from the setup/options flow. Built on the new driver layer, so Zendure and Marstek share the same control loop, sensors, dashboard and translations.
drivers/zendure.py. - Tibber price provider for dynamic pricing: select Tibber as the price integration — no price sensor needed. The engine polls the
tibber.get_pricesservice (today's 15-minute prices, tomorrow's after ~13:00), caches the slots and refreshes hourly.pricing/engine.py,pricing/calculations.py. - Per-device exclusion % slider: each excluded device gets a runtime slider (0–100%, default 100 = fully excluded) controlling how much of its demand stays off the battery, so the battery can cover part of a big load instead of all-or-nothing.
number.py,infra/external_loads.py. - No-PD direct-tracking mode (opt-in switch): the battery follows the consumption sensor 1:1 in a single cycle — no integral, derivative, smoothing, rate limiter or hysteresis — instead of the PD control law. Each cycle it reconstructs the home load from the battery's measured AC power (
new = measured − error), so it stays stable across the multi-second inverter ramp instead of oscillating rail-to-rail. Reuses the deadband, min charge/discharge power, relay min-ON and grid-setpoint sliders, plus a new No-PD Command Delay slider that debounces fast meters (collapses a burst into one command on the latest value). Off by default; the PD controller is unchanged.__init__.py,switch.py. - System power limits toggle: a runtime switch on the Control tab turns the combined system charge/discharge caps on/off without entering the options flow.
switch.py. - Delay weekly full charge toggle: a runtime switch (Weekly full charge card) lets the weekly 100% charge wait for the solar charge delay instead of charging immediately on its target day. Off by default, so the historic always-bypass behaviour is unchanged.
switch.py. - Re-evaluate dynamic pricing button: a system button that rebuilds today's dynamic-pricing charge schedule on demand (same evaluation as the 00:05 daily run). Shown only in dynamic-pricing mode.
button.py. - Separate discharge price floor (#408, dynamic pricing): optional second threshold so charge and discharge use different prices, opening an idle band between them (no grid charge, no discharge; solar-surplus charging still works) to avoid marginal cycles. Both thresholds are also exposed as live
numberentities so automations can rewrite them, and the floor is validated to stay at or above the charge ceiling. Empty = reuse the max price threshold for both (unchanged for existing installs).pricing/engine.py,number.py,config_flow.py. - Guaranteed minimum SOC floor (#417, predictive charging): a Guaranteed Minimum SOC slider (Control tab, 0 = off) forces an overnight grid charge to reach at least that SOC by the end of the charging window, even when the whole-day solar forecast nets to zero deficit — covering the morning gap before solar ramps up. Sizes the deficit to the floor, so it flows through the per-battery target SOC and dynamic-pricing slot sizing unchanged.
__init__.py,number.py. - Up to 3 predictive charging windows (Time Slot mode): configure 1, 2 or 3 charging windows, each with its own start/end and days. Fill only window 1 for the previous single-window behaviour. The consumption-window math now treats the union of all windows.
config_flow.py,__init__.py,tracking/consumption_tracker.py.
Changed¶
- Voltage taper power raised from 95 W to 200 W: at 95 W the cell did not sustain enough excitation to hold voltage in the taper zone for some batteries; 200 W keeps the cell more stable in the CV-like region without rushing to the BMS cutoff.
const/integration_const.py. - Slow actuators skip the hot-path readback: a slow actuator (Zendure HTTP, ~2.5 s settle) no longer blocks the shared control loop with a multi-second setpoint readback — its non-delivery is judged at poll time instead. The shared loop cadence and grid smoothing are NOT slowed to the fleet's slowest battery (that throttled fast Marstek tracking for the whole fleet); per-battery pacing belongs in the power distribution.
__init__.py,drivers/base.py. - Rebranded to Omnibattery (moved to the new
ffunes/omnibatteryrepo; domainmarstek_venus_energy_manager→omnibattery). Existing installs migrate the first time you add “Omnibattery”: if the legacy config entries are still present it detects them and, on confirm, recreates each one on the new domain, repoints the entity registry without changing any entity ID or unique ID (they staymarstek_venus_*, so recorder history and long-term statistics survive untouched), and copies the integration's.storagestate (daily energy totals, accumulators, balance history). If the old integration was already removed (HACS can't rename the domain in place across repos), it restores instead from a recovery snapshot the old build saved to disk. Nothing in your dashboards or automations breaks.domain_migration.py,migration_flow.py,config_backup.py. - Charge hysteresis is now mandatory (min 2%): the per-battery enable toggle is gone — hysteresis is always on. Existing batteries keep their configured percent; those that had it off get the 2% floor, which keeps the deadband wider than SOC-reading drift and prevents charge chatter at the top. Migrated on upgrade (config entry v7 → v8).
__init__.py,number.py. - Opt-in rename of system entities to
omnibattery_*: system entities now suggest anomnibattery_*entity ID while keeping their legacyunique_id(so recorder history and statistics stay linked). Existing installs are untouched until you trigger HA's built-in Settings → Devices & Services → Omnibattery → ⋯ → Recreate entity IDs, which renamessensor.marstek_venus_system_*→sensor.omnibattery_*in place (history preserved); new installs get the Omnibattery IDs from the start. The dashboard matches by translation_key, so it keeps working through the rename. ⚠️ Your own automations/templates/Energy-dashboard config that reference the old IDs must be repointed manually.infra/entity_naming.py. - Discharge-window chip shows the active slot number: the dashboard "Ventana de descarga" diagnostic now reads
Activa · Franja N(matching the configured-slot numbering) instead of justActiva.sensor.py,frontend/marstek-panel.js. - Hourly net balance flagged as Spain-only: the setup/options step now states the feature only applies under Spain's hourly surplus-compensation scheme (RD 244/2019) and shows your configured HA country, to deter accidental enabling abroad.
config_flow.py. - Control tab is now arrangeable: an Arrange toggle lets you pick a fixed column/row layout and drag the feature cards into the order (or matrix cells) you want, persisted per browser. Each feature's gate switch (predictive charging, charge delay, no-PD, capacity protection) now hides its parameter sliders while OFF, leaving just the switch.
frontend/marstek-panel.js. - Control tab tidy-up: the contracted-power slider moved into the shared Common control card, and the PD controller and No-PD direct-tracking panels are now mutually exclusive — enabling either collapses the other's parameters.
frontend/marstek-panel.js.
Fixed¶
- Voltage taper oscillated between full power and taper power: taper latch now requires the cell to drop to 3.44 V (40 mV below the 3.48 V entry threshold) before releasing — prevents the latch clearing on IR-induced relaxation at low charge power and re-triggering on the next cycle.
control/max_soc_charge.py. - Energy-flow Solar node showed double the production on DC-coupled systems (#407): since 2.0.5 the panel's solar source points at the
system_solar_poweraggregate (already external + Σ MPPT on vA/vD), but the live flow still added Σ MPPT a second time. It now uses the aggregate directly.frontend/marstek-panel.js. - Excluded device with "Allow solar surplus" still partly charged from the battery (#421/#415): the exclusion credited raw PV against the device, ignoring the home's own load, so the battery charged surplus the device should have consumed (an EV importing from grid while the battery charged the house PV). It now credits only the real surplus (
solar − home_base_load, a budget shared across devices), so PV offsets the device first and the battery charges only what the device can't absorb — never discharging into it, never exporting.infra/external_loads.py,__init__.py. - Recorder database bloat from diagnostic binary sensors: the predictive-charging, capacity-protection and charge-hysteresis sensors re-serialized large or per-cycle attribute payloads (consumption history, slot/decision dumps, live accumulators) into the recorder on every poll. Those attributes are now marked
_unrecorded_attributes, so the live state, dashboard and history-restore still see them but the history DB stops growing from them.binary_sensor.py. - Dynamic pricing charged the whole cheap slot instead of the calculated deficit (#409): predictive charging sized the stop-SOC off the gap-to-max minus solar surplus, which collapsed to "fill to max_soc" whenever consumption ≥ solar (winter/cloudy/overnight). It now targets the same
energy_deficit_kwhthe scheduler uses to size the slots, so charging stops at the calculated requirement.__init__.py,pricing/engine.py. - Evening grid-charge reevaluation overcharged on short-solar days (#409): the late-day top-up filled batteries to max SOC instead of sizing to need. It now projects today's actual consumption rate over the hours until midnight and grid-charges only the deficit the battery and remaining solar will not cover, publishing that figure to the same enforcer.
pricing/engine.py. - Solar charge delay let a higher-SOC battery overshoot the setpoint in multi-battery setups: the setpoint gate was system-wide on the minimum SOC, so a lower battery held it open while a higher one charged from grid past the setpoint. The floor is now enforced per battery.
control/charge_delay.py. - Relay min-ON time never held: the dwell was timed from when the battery first engaged, so after a normal multi-minute run the elapsed time already exceeded the setting and the hold was skipped. It is now timed from the moment the controller asks for idle, so the battery stays at minimum power for the configured seconds before the relay drops.
__init__.py. - Dynamic pricing missed cheap midday slots after an overnight drain (#411): the once-a-day 00:05 energy-balance read the SOC before the overnight discharge, locking in "no charge needed" for the whole day even if the battery woke near empty. A SOC-drop trigger now re-evaluates upward (reusing the evening recharge: deficit from current SOC, cheap slots in the next 12h) whenever live SOC falls ≥30% below the evaluated level, and an informational schedule whose cheap slots are already chosen is promoted to actually charge them.
pricing/engine.py. - Guaranteed minimum SOC floor never triggered and never stopped (two bugs): (1) the 30% SOC-swing re-evaluation threshold can't fire once
last_evaluation_socdrifts below 30%, so a battery draining past the floor was never re-evaluated — fixed byfloor_crossed, which forces a re-evaluation whenever SOC drops belowfloor − 5%regardless of the swing threshold; (2) once floor charging started it ran indefinitely on all-day slots (no stop condition existed) — fixed byfloor_recovered, which re-evaluates when SOC climbs back to the configured floor and stops charging if the floor was the only reason to charge.__init__.py,pricing/engine.py.
Internal¶
- Source reorganisation into subpackages: 18 modules moved out of the integration root into
sensors/,control/,tracking/,infra/. Root keeps only the HA-required platform files. No behaviour change. - Driver abstraction layer: all hardware access goes through a brand-agnostic
BatteryDriver; the control and platform layers read hardware traits fromcoordinator.capabilitiesinstead of branching on battery version. This is what makes the integration multi-brand (Marstek + Zendure).drivers/.
History inherited from Marstek Venus Energy Manager (predecessor integration)
[2.0.6] - 2026-06-27¶
Added¶
- Config backup for seamless rebrand migration: on setup and on every options change, the integration writes each config entry's data and options to a HA Store under the fixed key marstek_venus_energy_manager.config_backup. The key is domain-independent and survives a config-entry deletion, so the upcoming Omnibattery build can restore the full configuration even when the user removes the integration manually before reinstalling. config_backup.py, init.py.
[2.0.5] - 2026-06-20¶
Added¶
- Total Solar Power sensor (#391): a system-level
sensor.marstek_venus_system_solar_power(W) summing the external solar sensor and every Venus vA/vD unit's MPPT inputs — the live counterpart of Daily Solar Production, usable in HA's Energy dashboard. Added only on systems with DC-coupled PV (vA/vD).sensor.py.
Fixed¶
- Solar node opened only the external inverter (#391): on vA/vD systems the dashboard Solar node displayed external + MPPT but clicking it opened just the external sensor (or nothing when none was configured). It now links to the new Total Solar Power sensor, matching the displayed total.
__init__.py,frontend/marstek-panel.js. - Charge/Discharge blocks linked to AC-only power on solar models (#347): on vA/vD the SOC card's Charge/Discharge stats show real cell power (AC + DC MPPT), but clicking opened the AC-only System Charge/Discharge Power sensor, so the number didn't match. A new
sensor.marstek_venus_system_battery_cell_power(signed AC + MPPT, vA/vD only) now backs those blocks; non-solar systems keep the AC-only sensors.sensors/aggregate_sensors.py,sensor.py,frontend/marstek-panel.js. - Weekly full charge completed prematurely at ~95% SOC (#394): the weekly cycle ended as soon as a cell hit 3.58 V, even when the BMS still reported ~95% — closing it before the pack was full. It now completes only when the battery is genuinely full: reported SOC 100%, or a confirmed BMS cutoff (charge ≤10 W in Standby for 5 consecutive cycles). During the weekly charge the 3.58 V pause is lifted so charging keeps going at the tapered 95 W up to the cutoff, and that cutoff is now recognised even while the pack sits in the top taper zone (≥ 3.48 V) but mis-reports SOC (coulomb drift). On completion the configured max SOC is restored. Separately, the 100% voltage taper now also engages when
max_socis below 100% (previously only at exactly 100%). The Weekly Full Charge sensor now exposes abatteriesattribute with per-battery SOC, BMS-cutoff cycle count and a completion snapshot (soc_at_completion,max_cell_voltage_at_completion,completion_reason) for diagnostics.control/max_soc_charge.py,control/weekly_full_charge.py,sensor.py. - Home Consumption sensor had duplicate "system" in entity ID (#387): entity ID was
sensor.marstek_venus_system_system_home_consumption(doublesystem) instead ofsensor.marstek_venus_system_home_consumption. The dashboard Energy Flow panel and consumption backfill both referenced the wrong ID, making the Home node unavailable. Migrations on upgrade rename both theunique_idand the registryentity_id(the latter is not derived fromunique_id, so it must be renamed explicitly; skipped if the target id is already taken).sensors/aggregate_sensors.py,tracking/consumption_tracker.py,__init__.py. - Min SOC dashboard slider allowed values below 12% on v3/vA/vD: the software SOC limit entity for batteries without hardware cutoff registers had a native minimum of 5%, letting users drag it below the 12% floor enforced everywhere else. Now clamped to 12%.
number.py. - Manual Mode off could corrupt PD state mid-cycle:
async_turn_offreseterror_integral,_active_charge_batteries, etc. without holding_control_lock, so a running control cycle could observe a partially-reset state. Reset now happens insideasync with _control_lock.switch.py. - PD controller stuck after all batteries hit max_soc simultaneously (#390):
_stop_blocked_active_batteriescalled.remove()after anawait, which raisedValueErrorifswitch.async_turn_off/onreset the list during that suspension; the uncaught exception aborted_run_control_cycle, freezing the controller at setpoint 0 with no recovery.__init__.py. - Home Consumption wrong with inverted grid meter: when
meter_invertedis enabled, Home Consumption was calculated with the raw (wrong-sign) grid value instead of the negated one, causing inflated or negative readings.aggregate_sensors.py. - Predictive charging evaluated at midnight instead of 00:05: the ±5 min polling window around 00:05 included 00:00:00–00:10:00, so the first coordinator cycle after midnight triggered the daily evaluation instead of waiting for 00:05. Replaced the polling check with
async_track_time_change(hour=0, minute=5, second=0).pricing/engine.py,__init__.py. - Energy Flow Home node hit 0 with an "additional" excluded device: the panel subtracted every excluded device's power from the Home node, but "additional" devices (
included_in_consumption= false) are not in the home sensor, so subtracting them drove Home to 0. Now only theincluded_in_consumptionportion is subtracted.frontend/marstek-panel.js.
[2.0.4] - 2026-06-17¶
⚠️ Heads-up — entity IDs on new installs: entity IDs are now built from the English translation key (e.g.
sensor.marstek_venus_1_ac_power) instead of the localized display name. Existing entity IDs are kept untouched. If you rebuild the device (delete entities/device and re-add the integration), new IDs will be in English — update any dashboards, automations, or scripts that referenced the old localized IDs.
Changed¶
- Home consumption always derived: the legacy
household_consumption_sensoris fully removed from all calculations (config flow already dropped it in 2.0.1). Home consumption is always grid + battery AC + solar. A v5→v6 migration strips any stale key. "Solar produced today" now reads the measured solar+MPPT daily total instead of the household-balance accumulator.consumption_tracker.py,aggregate_sensors.py,__init__.py. - English entity IDs for new installs: entity IDs now use the English translation key instead of the localized display name, so they no longer vary by UI language. Friendly names stay localized. Battery binary sensors (WiFi/Cloud/Balancing) also moved to
translation_key; added missingbalancing_modetranslation in all languages.entity_naming.py. - Reduced Modbus bus traffic (#361): control writes are skipped when the battery is already in the commanded state (discharge re-sent only if the battery stops delivering); the post-write readback now runs every 5th write; contiguous registers (cell voltages, charge/discharge power, v2 cutoff group) are read in a single Modbus request on all models (v3/vA/vD/v2).
modbus_client.py,coordinator.py,__init__.py.
Fixed¶
- Home Consumption inflated by disconnected battery: a unit that dropped mid-discharge kept a frozen
ac_powerstill summed into Home/Solar/Charge/Discharge aggregates while the grid meter already carried the load. Aggregates now skip unavailable units.consumption_tracker.py,aggregate_sensors.py. - Round-Trip Efficiency on Venus A/D (#325): fixed >100% readings (AC-side cumulative ratio blew up on partial cycles; sensor now measures real conversion loss while MPPT=0, per-direction, clamped ≤100%) and a follow-up where efficiency stayed "unknown" on DC-only-charge units (now estimates from whichever leg has been sampled, switching to the real product once both exist). Per-leg
charge_efficiency/discharge_efficiencyexposed as attributes.calculated_sensors.py. - Daily solar production ignored Venus-connected PV (#354): daily total now sums external solar + each vA/vD unit's MPPT power; entity is created whenever either source exists.
consumption_tracker.py,sensor.py. - Integration stuck on "initialising" / crash on disabled sensors (#355): disabling any of several entities (
balance_neto, aggregate sensors, cell-balance diagnostics) raisedRuntimeError: Attribute hass is Noneinside the control cycle, stalling the PD loop or breaking the coordinator listener. All sensor pushes now guard onhass is None.hourly_balance.py,aggregate_sensors.py,balance_monitor.py. - Solar-surplus excluded device caused charge/discharge oscillation: when a device with Allow solar surplus drew more than current solar production, the sign-conditional adjustment had no stable fixed point and the battery bounced every few seconds. Adjustment is now
max(0, device_power − solar_power); without a solar sensor, discharge is blocked while the device is active. Also fixes a silent ×1000 scaling error when the power sensor reports in kW.external_loads.py,__init__.py. - Evening/startup ignored cheaper next-day slots (#356): after ~13:00, tomorrow's EPEX/Nordpool prices were ignored — the horizon now extends to
max(end_of_day, now + 12 h)so overnight cheap slots are considered.__init__.py. - EV charger cut-off ignored non-English charging states (#358): added NL/DE (laden), CA/PT (carreg), IT (caricand/carica), SV (ladd), NO/DA (lading).
external_loads.py. - Predictive charging backfill ignored disabled excluded devices: disabled devices were still subtracted when backfilling consumption history, inflating/deflating the forecast.
consumption_tracker.py. - v3 batteries marked non-responsive near full / charge ping-pong: charge pause now also latches on the BMS-cutoff signature (~0 W in standby at top zone), not only at 3.58 V; high-SOC standby treated as expected BMS-full (not a fault); SOC recalibration threshold raised 90→99 % so a stuck pack is driven to one BMS cutoff.
__init__.py,integration_const.py.
Internal¶
- Automated test suite (pytest) + CI on every PR.
tests/,tests.yml. external_loads,power_distribution,charge_delay,pricing, andmax_soc_chargemodules extracted from__init__.py; behavior unchanged, guarded by characterization tests.const.pysplit into aconst/package (all imports unchanged).
[2.0.3] - 2026-06-10¶
Fixed¶
- v3 reconnect storm: failed reads/writes no longer tear down and reopen the TCP connection (v3 firmware holds a single connection slot); reconnection is owned by the coordinator's health gate, and a fresh connect waits 1 s after closing so v3 can release its slot.
- Inter-message delay was never applied:
message_wait_millisecondsis not a pymodbus feature, so the configured spacing (150 ms for v3/vA/vD) was silently ignored and registers were read back-to-back. The client now sleeps the configured delay after every Modbus request. - Battery charged/discharged at max power with slow grid sensors (~30 s): the PD loop scales its incremental P term by elapsed/dt, which multiplied the gain ~15× for a 30 s sensor (vs ~1× at 2 s) — an open-loop step far past the stability bound, overshooting and reversing each update into rail-to-rail oscillation. P scaling is now capped to the discrete stability bound (
kp·ratio ≤ 1), and the stale-sensor safety recalc no longer integrates P on already-acted-on data (windup on sensors slower than the ~30 s watchdog).__init__.py.
[2.0.2] - 2026-06-09¶
Added¶
- Telegram notification blueprint: forwards Home Assistant persistent notifications to a Telegram chat, filtered by default to only this integration's notifications. Thanks to Adrià for the original automation.
persistent_notification_to_telegram_blueprint.yaml.
Changed¶
- Persistent notification IDs namespaced with a
marstek_venus_prefix so automations can reliably target only this integration's notifications (the blueprint above filters on it). Note: breaks automations referencing the old IDs.const.py. - Venus D charge/discharge limit raised to 2500 W (was 2200): firmware 149 supports the higher rating. The power sliders, setup/options flow, and dashboard panel now allow up to 2500 W. Existing Venus D configs keep their saved value — re-set the slider to use the new ceiling.
const.py.
Fixed¶
- Spurious active-balance cell-delta readings below the top voltage: the BMS charge-rejection test fired on transient ~0 W reads (charge ramp-up, current taper near 3.58 V) even when the BMS was not cutting, logging low-vmax deltas that distorted the balance history. Rejection now must persist 3 cycles before recording a measurement.
active_balance_mode.py. - Control-tab cards overflowed at ~1080p: sliders collapsed to the knob and values/buttons spilled outside the card box on narrow screens. The settings blocks now lay out in a responsive grid that adapts the column count to the window width (collapsing on narrow screens), and section labels can wrap, so nothing crushes or overflows.
marstek-panel.js. - Charge taper pinned v3 cells at the top voltage, blocking discharge (#293): the taper re-evaluated every cycle and re-paused whenever the cell touched 3.58 V, holding it there and leaving some v3 BMSs stuck in standby (ACK ok, ~7 W out) when discharge was commanded near full SOC. The pause now latches once the top voltage is reached and stays stopped — no re-trickle — releasing only after SOC drops a small margin (battery actually discharged).
const.py,__init__.py,switch.py. - Round-Trip Efficiency read >100% on Venus D/A with DC solar: the AC-side charge counter can't see DC-coupled PV charging the cells, while discharge counts everything, so the ratio overshot. On D/A units the sensor now integrates the true terminal power (
battery_cell_power=battery_power+ MPPT) by sign and persists the totals across restarts; AC-only models keep the accurate hardware counters.calculated_sensors.py.
[2.0.1] - 2026-06-08¶
Upgrade notes (no manual action required unless stated): - Hard-refresh the browser after updating (Ctrl+F5, or Cmd+Shift+R on macOS) so the new dashboard panel loads — a cached panel may otherwise persist. - Config entry auto-migrates v3→v5. PD gain defaults are lowered (see below) for installs still on the old defaults; hand-tuned gains are untouched. Min/Max Cell Voltage sensors the integration had disabled are re-enabled (user-disabled ones are left alone). - PD control is now event-driven.
pd_max_power_changeis applied per cycle, so the faster cadence raises the effective ramp rate — lower it if the response feels abrupt. - Default PD gains lowered to Kp 0.35 / Kd 0.3 (was 0.65 / 0.5) to curb overshoot. - Household consumption sensor removed from the setup/options flow. Home consumption is now derived from grid + battery + solar; a previously configured sensor is still honoured, but only when no solar production sensor exists. - Per-battery Work mode dropdown removed from the dashboard (the integration manages work mode; theuser_work_modeentity still exists). - Max Contracted Power relocated from each predictive-charging mode to the initial setup step / Sensors section, the "ICP" acronym was dropped from all UI labels, and it now caps grid import in every mode (previously only while predictive grid charging).
Added¶
- PD tuning profiles: New
select.*_pd_tuning_profilewith one-click presets (Very Smooth/Smooth/Balanced/Aggressive) that set all four PD params at once, plus Custom for manual tuning; manual slider moves fall back to Custom automatically.const.py,select.py. - PD Control Quality sensor: New
sensor.marstek_venus_system_pd_control_quality(W, grid-error RMS) withoscillation_per_min, active params/profile, and arecommendationattribute so the effect of tuning is visible.aggregate_sensors.py,__init__.py. - PD Relay Cooldown: New
number.*_pd_relay_cooldown(s, default0= disabled, opt-in). Once the battery engages it stays on at least this long before returning to idle, stopping relay on/off chatter when grid hovers at the deadband edge during solar ramp-up/down. While held it runs at the configured PD min charge/discharge power, or 100 W if that is 0. Large imbalances bypass it.const.py,__init__.py. - PD Min Cycle Interval: New
number.*_pd_min_cycle_interval(s, default1;0= disabled). Caps how often the event-driven control loop runs — grid-sensor updates arriving closer together than this are dropped, so a fast-publishing meter can't flood slow Modbus bridges (e.g. Elfin EW11) with write bursts. The 2 s safety timer is never gated, so control never stalls.const.py,__init__.py. - Predictive Grid Charge Margin: New
number.*_predictive_grid_charge_margin_pct(%, default0= off, opt-in). Inflates the grid-charge amount over the solar-deficit to hedge optimistic solar forecasts or worse-than-expected weather — e.g. a 2 kWh grid need at 50% charges 3 kWh. Applies to the predictive target SOC and the dynamic-pricing evening re-evaluation; capped at the gap to max SOC. Also in the setup wizard and Control tab.const.py,__init__.py,config_flow.py,marstek-panel.js. - SOC recalibration attempt on a stuck top voltage: when the charge tapper pauses at the top cell voltage (3.58 V) but the BMS still reports SOC below 90%, the charge keeps running at the tapered power until the BMS itself cuts off, attempting to make it recalibrate SOC to 100%. Best-effort — whether the cutoff actually resets the SOC depends on the BMS firmware, so it is not guaranteed to fix a drifted reading. Self-limiting: stops at the BMS cutoff and only re-arms after the battery leaves the top zone.
const.py,__init__.py. - Non-responsive reason on the diagnostic sensor: the Non-Responsive Batteries sensor now reports why a battery is flagged —
modbus_write_failed/modbus_exception(write failed),feedback_timeout(write accepted, readback never followed),ack_mismatch,standby_no_delivery(inverter in standby) ornon_delivery— plusretry_attempted/wake_attemptedflags. Modbus/ACK failures now also count toward exclusion, not just non-delivery.coordinator.py,non_responsive_tracker.py,__init__.py,sensor.py. - RS485 wake nudge before excluding: when a battery that ACKs but delivers 0 W is about to be marked non-responsive (final consecutive fail), RS485 control is toggled once — disable (0x55BB), wait 1 s, re-enable (0x55AA) — as a last-ditch attempt to force it out of standby. The toggle (not a plain re-assert) is needed so the battery actually clears its state. One shot per exclusion, not per cycle. Skipped when RS485 control is user-disabled.
__init__.py. - Control tab help tooltips: Each Control-tab section header (ⓘ button) and slider/switch (dotted label) now shows its options-flow explanation on hover and tap.
marstek-panel.js. - Catalan translation: Added
translations/ca.jsonwith full UI translation to Catalan. - Solar Power sensor (Venus D/A): New per-battery
sensor.*_solar_powersumming MPPT inputs into one DC-coupled PV figure (W). vA/vD only, enabled by default.const.py,calculated_sensors.py. - Battery Cell Power sensor (Venus D/A): New per-battery
sensor.*_battery_cell_power=battery_power + solar, the real cell flow after adding back DC PV. vA/vD only, enabled by default. - Home Consumption system sensor: New
sensor.marstek_venus_system_home_consumption(W) exposing the flow diagram's derived home load; the Home node opens it when no household sensor is configured.aggregate_sensors.py.
Changed¶
- PD control is now event-driven (hybrid): The control cycle triggers on each new grid-sensor value (native cadence) instead of only the 2.0 s timer, which stays as a watchdog; overlapping triggers are serialized by a lock. Note:
pd_max_power_changeis per-cycle, so faster cadence raises the effective ramp rate — lower it if the response feels abrupt.__init__.py. - PD made cadence-independent and noise-robust: The P term and rate limiter now scale with real elapsed time (so tuning no longer drifts with the variable event-driven cadence), the grid sensor and derivative are smoothed with time-constant EMAs instead of fixed-sample averages (less PWM/meter-noise injection), and a back-calculation anti-windup re-anchors the control base to measured AC power when batteries can't deliver the commanded power (prevents overshoot/export spikes on recovery). Also fixes a derivative kick when leaving the deadband. The predictive grid-charge loop received the same cadence-scaling and derivative filtering.
__init__.py. - Lowered default PD gains to curb overshoot: Defaults are now Kp 0.35 / Kd 0.3 (was 0.65 / 0.5). Existing installs still on the old defaults are migrated automatically (config entry v3→v4); hand-tuned gains are left untouched.
const.py,__init__.py,config_flow.py. - Max Contracted Power moved to Sensors: now set in the initial setup step and the Sensors options section instead of inside each predictive-charging mode — it is a property of the grid connection, not of predictive charging. Existing values are preserved; default stays
7000. The Spain-specific "ICP" acronym was dropped from all UI labels (every language).config_flow.py, translations. - Contracted power now caps grid import in every mode: previously it only applied while predictive grid charging. The PD loop now clamps battery charging so projected grid import never exceeds the contracted power — a positive target/offset (user setpoint or hourly net balance) can no longer push the breaker past its limit. Charging only; never forces a discharge.
__init__.py. - Min/Max Cell Voltage polled at high priority: scan interval raised from
mediumtohighfor all batteries.const.py. - Predictive charging no longer needs a dedicated household sensor: real-time consumption accumulation, the 23:55 daily capture, and recorder backfill now use the derived home consumption (grid + battery + solar) when none is configured, replacing the cruder battery-discharge+grid estimate. The household sensor stays an optional precision override.
consumption_tracker.py. - Household consumption sensor removed from setup: the field is gone from the config and options flows. Home consumption is derived from grid + battery + solar; a previously configured sensor is still honoured, but only when no solar production sensor exists (with solar, the derived value is exact and preferred).
config_flow.py,const.py,aggregate_sensors.py,__init__.py. - Removed the per-battery Work mode dropdown from the dashboard: It was confusing and the integration manages the work mode itself — users should never set it. The
user_work_modeselect entity still exists, only the panel control is gone.marstek-panel.js.
Fixed¶
- Active balance retry now ratchets down to 3.40 V: a BMS charge rejection below 3.58 V lowered the retry voltage but reset it to default after every escape discharge, so it never stepped down and the run ping-ponged at 3.49 V. The lowered retry now persists across cycles, dropping 0.01 V per rejection to the 3.40 V floor; it resets only on reaching 3.58 V or finishing. A cell delta measurement is now also recorded at the cut, so a run that never reaches 3.58 V no longer logs no reading.
active_balance_mode.py. - Options-flow save raised
list.remove(x): x not in list: the control/refresh/consumption timer unsubs were cancelled twice on unload (manually for early teardown, then again viaentry.async_on_unload), and the state-change unsub raises on a second call. Unsubs are now call-once.__init__.py. - Needed multiple reboots on startup: if the first connect failed (e.g. EW11B hadn't released the previous TCP slot), setup logged a warning and continued with an unconnected coordinator — entities stayed unavailable but HA marked the integration loaded, forcing a second restart. Now retries with escalating delays (2/5/10 s) and raises
ConfigEntryNotReadyif still down so HA retries setup automatically. (#308)__init__.py. - Low-SOC BMS discharge cutoff flagged a battery as non-responsive: when the pack drops below 20% the BMS can refuse discharge on its own (e.g. a weak cell) even above the configured min_soc; the battery ACK'd the command but delivered 0 W and got excluded from the PD pool. Now treated as an expected cutoff, mirroring the high-SOC handling.
const.py,__init__.py. - Disabling a control entity stopped the batteries: The poll skips disabled entities, so turning off Set/Max Charge/Discharge Power (or SOC cutoffs) dropped their registers from
coordinator.dataand the control loop stalled. These control registers are now always polled, regardless of whether their number entity is disabled.coordinator.py. - Cell voltage sensors stayed disabled on existing installs: Min/Max Cell Voltage were switched to enabled-by-default, but entities the integration had already disabled kept
disabled_by=integration. Config entry migration v4→v5 re-enables them; user-disabled entities are left untouched.__init__.py,config_flow.py. - Daily Home Consumption energy (kWh) didn't report without a household sensor:
sensor.*_daily_home_energynow instantiates and integrates the derived home load (grid + battery + solar) when no dedicated household sensor is configured, matching the Home Consumption power sensor.sensor.py,consumption_tracker.py. - SOC "today" sparkline had a wrong time axis: History samples are now step-hold resampled onto a uniform midnight→now grid using real timestamps, so a flat plateau (few recorder samples) no longer compresses to the right edge and makes an hours-old peak look like "now".
marstek-panel.js. - Inverted grid meter showed import as export (and vice versa): The
meter_invertedflag is now applied to the daily grid import/export accumulator and forwarded to the dashboard, so the energy-flow Grid node, the power-history chart, and the imported/exported energy totals all follow the integration's +import / −export convention.consumption_tracker.py,__init__.py,marstek-panel.js. - Energy-flow diagram battery line animated backwards while charging: Flow now animates into the battery image when charging, outward when discharging.
marstek-panel.js. - Energy-flow diagram double-counted DC solar on Venus D/A: Battery node now derived from
-ac_power - ac_offgrid_power + mppt(thebattery_powerregister is unreliable; the off-grid/backup port counts too) and the Solar node sums external solar plus all units' MPPT, so DC PV charging no longer renders as battery discharge with inflated home load.marstek-panel.js. - Non-printable characters in Modbus string registers broke the recorder (issue #282): Decoded char/string values are now filtered with
str.isprintable(), so embedded NUL/control chars no longer reach HA state and break PostgreSQL recorders.modbus_client.py. - Energy-flow diagram "Excluded devices" leader line crowded its label: Line now routes over the top and ends above the power value (wider gap), and its origin sits at the car's centre.
marstek-panel.js. - "Active batteries" chip showed only the direction word: Chip now appends the active battery name(s), e.g. "Discharging: Battery 1" (long lists ellipsize).
- Dashboard control sliders disagreed with the integration (e.g. 62% vs 60%): Slider
minis floored to a step boundary so the grid matches HA's number slider, and values are clamped to the entity's real[min, max]. - Grid-at-min-SOC daily counter used a hardcoded cycle step:
_daily_grid_at_min_soc_kwhnow integrates over real elapsed time (was a fixed 2.5 s, over-counting ~25%) and resets across gaps >10 s.__init__.py. - Daily solar/home/grid energy totals used left-Riemann integration: Now trapezoidal, and the grid total splits at the import↔export zero-crossing so a sign flip between samples is no longer misclassified onto one side.
consumption_tracker.py. - Solar Charge Delay permanently unlocked by a transient forecast blip: A momentarily unavailable forecast now holds the delay through a 300 s grace window instead of unlocking for the day; toggling the switch off→on re-evaluates from scratch.
__init__.py. - TCP keepalive caused v3 batteries to permanently lose connection: On keepalive timeout the OS closes the socket locally without sending FIN/RST to the peer, leaving the battery's single TCP slot occupied by a zombie — new connections were refused until manual restart. Keepalive removed; the existing
async_connect()close-before-reconnect already handles dead-socket recovery.modbus_client.py. - Cycle-count timers drifted under event-driven cadence: The hourly-balance external-sensor detection window and the throttled state saves (hourly balance, grid-at-min-SOC) counted control cycles assuming a fixed 2.5 s step; with the event-driven loop (~1 s) the detection window shrank (risking an early give-up at startup) and saves fired ~2.5× more often. All three now use elapsed monotonic time.
hourly_balance.py,consumption_tracker.py.
[2.0.0] - 2026-05-29¶
⚠️ Breaking Change — Config entry version bumped to 3 (automatic migration)¶
Time slots (no_discharge_time_slots) gained a richer per-direction schema. async_migrate_entry converts every existing slot on first start, preserving legacy whitelist behavior — no manual action required. Legacy apply_to_charge=False → allow_discharge=True, allow_charge=False; apply_to_charge=True → both directions enabled. Scope defaults to all, mode to pd, no SOC/power overrides.
Added¶
- Built-in control dashboard (sidebar panel): Auto-installs as a HA sidebar panel — no HACS, no YAML. Registers a
marstek-venus-panelweb component, cache-busted per version, follows the active HA theme, and matches entities bytranslation_key(works regardless of UI language or renames). Three tabs: Overview (animated SOC ring, Grid↔Home↔Battery↔Solar energy-flow diagram, diagnostics, 2×2 chart grid), Batteries (per-battery SOC/power, health & cells, daily energy, optional MPPT, firmware info, controls), and Control (system-wide settings grouped by feature, each with its switch + config parameters). - Balancing Mode binary sensor (v3 only): Diagnostic sensor reading holding register
34009(4= balancing,0= idle) as arunningdevice-class entity showing Running / Not running. Exposed asbinary_sensor.*_balancing_mode, enabled by default; v3-only (BINARY_SENSOR_DEFINITIONS_V3). - Per-battery, per-direction time slots: Each slot exposes independent
allow_charge/allow_dischargeticks instead of a singleapply_to_charge. Charge and discharge whitelists are evaluated independently; a direction is restricted only when at least one slot opts in, otherwise it stays unrestricted. - Slot SOC override: Optional per-slot replacement of the battery's max/min SOC inside its window (clamped
[12, 100]), reported asslot_soc_overridein the blockers. - Slot power override: Optional per-slot cap on
max_charge_power/max_discharge_power, narrowing the PD envelope without touching global limits. - Manual-power slot mode:
mode = manualforces a fixed charge or discharge power for the slot, bypassing the PD algorithm. Requires the power tick and exactly one direction enabled. Safety blockers (min/max SOC, EV pause, active balance) still apply. - Per-battery slot scope:
battery_scopetargetsallor a specificbattery_N; overlap validation only fires when both slots target the same battery. - Midnight crossing supported natively: Slot evaluation handles
start_time > end_timeranges (e.g. 22:00–06:00) without two separate slots. - Slot diagnostic attributes:
binary_sensor.predictive_charging_activeexposesactive_slot_per_batteryandmanual_slot_owned. - New integration status states:
sensor.*_integration_statusgainedtime_slot_manualandno_charge_slot(alongsideno_discharge_slot). The manual state is checked first to avoid misreporting a manual discharge slot. Translations added acrossstrings.jsonand the 5 locales. - Configuration Summary exposes the full slot schema: Dumps every persisted field per slot (
mode,battery_scope,allow_charge,allow_discharge, override flags,battery_limits) instead of the legacy fields. TheDischarge Windowsensor's per-slot attributes were updated to match.
Changed¶
- TCP keepalive on the Modbus connection:
SO_KEEPALIVEenabled on every socket (TCP_KEEPIDLE=60,TCP_KEEPINTVL=10,TCP_KEEPCNT=3where available), so a stale socket is detected and torn down within ~90 s and the next poll reconnects.async_close()now also nullsself.clientso the nextasync_connect()builds a clean client. Best-effort, debug-logged. - System aggregate entities now created for single-battery setups: Removed the
len(coordinators) > 1gating that left System SOC/Power/Energy andActive Batteriespermanentlyunavailableon one-battery installs. Each aggregate now mirrors the single battery's value (calculations already summed/weighted across coordinators); orphaned entries rebind to the sameunique_idon restart.System Alarm(gated by version, not count) is unchanged. - Time slot blockers are per-battery:
_refresh_time_slot_blockssetstime_slot_charge/time_slot_dischargeper coordinator instead of globally, so a slot targeting one battery no longer affects the rest. - Time slot limit raised from 4 to 8 in the config and options flows.
- Config flow split into two slot steps: Step A captures times/days/scope/ticks/mode; Step B (override values) is shown only when SOC or power overrides are enabled.
- Documentation updated:
site-docs/configuration/time-slots.mdand.es.mddescribe the new model, scope, modes, migration mapping, and diagnostics. - Cell balance notifications condensed and rate-limited: The three separate top-charge notifications (imbalance, degraded-cell, rising-trend) are merged into one bullet-list message per battery, throttled to one per battery every 7 days (
BALANCE_NOTIFY_COOLDOWN_DAYS). Severe alerts flip the title icon to 🔴 instead of firing a second notification. The cooldown timestamp persists across restarts. - Cell-imbalance thresholds raised to match factory baseline: Marstek cells ship with a ~170–180 mV top-of-charge spread that is normal on the steep LiFePO4 curve, so the old 50/100/150 mV bands flagged new packs as unhealthy. Thresholds are now 200/230/250 mV (yellow/orange/red); a
BALANCE_BASELINE_OFFSET_MV = 180correction is subtracted only in the rising-trend gate. Status/notifications still use the raw absolute delta, which is stored and shown unchanged. No migration needed. - Sustained-imbalance notification reworded to point at the fix: The consecutive-red alert now names the Active Balance Mode switch to rebalance the cells instead of implying hardware failure.
- Active Balance Mode start/finish notifications simplified: Start shows just the initial delta vs target and the PD-exclusion note; finish shows the result, initial→final delta with improvement, and duration. Dropped per-line clutter (source labels, per-cell voltages, SOC at cutoff, measurement timestamp). Numeric values unchanged.
- Active balance discharge power raised from 25 W to 200 W (
ACTIVE_BALANCE_DISCHARGE_POWER_W): each charge→discharge micro-cycle completes faster, closing the cell delta in fewer, shorter cycles. Charge leg stays 95 W; voltage thresholds unchanged. - Active balance phase labels renamed:
CHARGE_50W/DISCHARGE_25W/FINAL_DISCHARGE_25W→CHARGE/DISCHARGE/FINAL_DISCHARGE(the wattage moved/changed). Persisted*_50W/*_25W(and olderHOLD) labels are mapped on load, so an in-progress run survives the upgrade.
Fixed¶
- Active balance stuck in PRE_TOP_CHARGE at full charge power on a near-full pack: When enabled at ~100% SOC with resting
max_cell_voltagejust belowACTIVE_BALANCE_CHARGE_RESUME_CELL_VOLTAGE(3.49 V),PRE_TOP_CHARGEcommandedmax_charge_power, which the BMS refused on the full pack (~0 W), sovmaxnever reached the trigger and it hammered max power indefinitely.top_reachednow also flips atbattery_soc ≥ 99%or when charge is rejected atbattery_soc ≥ 95%, handing off to the low-power CHARGE phase (95 W) the BMS accepts. Thevmax ≥ 3.49 Vtrigger is unchanged. - Active balance escape discharge never ran when the BMS rejected charge at a low resting voltage: The discharge target sat at or above
vmaxduring an SOC-100 full lockout (vmax ~3.48 V), so no discharge ran and the run ping-ponged CHARGE↔DISCHARGE forever._lower_active_balance_charge_resume_targetnow bounds the retry voltage strictly below the rejectionvmax(floored atACTIVE_BALANCE_ADAPTIVE_MIN_RESUME_CELL_VOLTAGE= 3.40 V), so the 200 W escape discharge runs and walks SOC off the lockout 10 mV per cycle. The high-vmaxOVP case is unchanged. - Daily energy sensors stepped backwards after a reload (
total_increasingwarnings): The fiveTOTAL_INCREASINGsystem energy totals are only flushed to theirStoreon a ~5 min throttle, andasync_unload_entrynever forced a final save — so a reload reverted each to its last snapshot, below the recorder's pre-reload state, producing noisy warnings (Energy dashboard was not corrupted). The unload path now awaits a newConsumptionTracker.async_save_all()that flushes all three stores before teardown; throttled writers were split into fire-and-forget wrappers plus awaitableasync_save_accumulators/async_save_daily_energyvariants. A hard crash can still lose the unsaved interval, bounded by the ~5 min throttle. - Battery still received PD commands after the user disabled RS485 control:
rs485_user_disabledwas only consulted at setup/reconnect to skip re-enabling RS485, so the PD loop kept issuing power writes the battery silently ignored. The flag is now checked at the three power chokepoints —_get_available_batteries,_set_battery_power, and the manual time-slot writer loop — so the battery is fully excluded; re-enabling the switch re-arms it next cycle. - Active balance completion notification used instant voltage when disabled mid-run: When the switch was turned off before any WAIT_MEASURE completed, the "final delta" fell back to instantaneous cell voltages, distorting the comparison. It now falls back to the last official 3.58 V measurement (or shows n/a if none exists).
- Charge hysteresis not activating without charge tapper at max SOC = 100%: The 1.8.4 fix only applied when the software charge tapper was enabled, so
max_soc = 100%with the tapper off had the BMS cut at 3.58 V while hysteresis never engaged. The check now directly comparesmax_cell_voltage ≥ 3.58 VagainstNORMAL_BALANCE_PAUSE_CELL_VOLTAGEwheneffective_max_soc ≥ 100%, independent of the tapper. Same fix applied inside_get_available_batteries. - Top-voltage hysteresis ignored slot/predictive SOC overrides in
_get_available_batteries: The availability path gated on the raw configuredmax_soc ≥ 100while its sibling usedeffective_max_soc ≥ 100, so a slot/predictive override to 100% (withmax_soc < 100) never armed the 3.58 V trigger. Both now useeffective_max_soc, computed once and reused. - Active balance stuck in CHARGE_50W when the BMS has a latched OVP cut: Two failure modes prevented
_active_balance_charge_rejected_detectedfrom firing and transitioning to a DISCHARGE cycle: the BMS keptinverter_state = 2while delivering 0 W (so theinv_state == 1guard failed), and on some BMS the OVP revertsforce_mode/set_charge_powerto 0 (socharge_was_requestedwas False). The guard now acceptsinv_state ∈ {1, 2}, and a_ab_charge_cmd_activeflag preserves the prior-cycle charge intent across register reversion. - Unavailable device blocks config entry deletion:
async_unload_entrywrote shutdown registers to each battery (~40 s per unreachable battery attimeout=10 s× 4 registers), making the entry appear undeletable. Shutdown writes are now skipped when_is_connectedisFalse; the disconnect call still runs to clean up any open socket. - No reconfigure path when the device IP changes:
async_step_reconfigure/async_step_reconfigure_batteryare now implemented — the Reconfigure button prompts for new IP/port/model (pre-filled), tests the connection, preserves all power/SOC limits, and reloads. Entity unique IDs ({host}_{port}_{key}) and the device identifier are rewritten in place via the registries, so statistics, history, and Energy dashboard data survive with no loss. - Stale battery devices could not be removed from the HA UI:
async_remove_config_entry_deviceis now implemented, allowing removal when a device's identifiers no longer match any configured battery; the system-level device ((DOMAIN, "marstek_venus_system")) is always protected. - PD control starved by polling on slow batteries (v3): The poll loop never yielded the event loop between register reads, so on v3 (~3 s/cycle) the PD writer waiting on the lock was repeatedly bypassed. Added
await asyncio.sleep(0)after each read so asyncio can hand the lock to the PD coroutine.
[1.8.4] - 2026-05-27¶
Changed¶
- Weekly full charge completion decoupled from delta-V measurement: Weekly full charge now declares completion as soon as every battery has reached the pause voltage (
max_cell_voltage ≥ 3.58 V), restoring registers and arming hysteresis immediately. Previously, completion required the 60-second diagnostic delta-V measurement to finish first, which could fail if voltage sagged below 3.58 V under the reduced 95 W taper load before the timer elapsed. The 60-second measurement still runs as a best-effort diagnostic; if it did not finish before completion, a one-shot snapshot is captured at completion time under phasetop_charge_taper_complete.
Fixed¶
- Charge hysteresis not activating when max SOC = 100% with voltage taper: When the charge tapper blocked charging at 3.58 V, the BMS never reported 100% SOC, so the hysteresis activation condition (
current_soc ≥ max_soc) was never met and the tapper cycled indefinitely without engaging hysteresis. Hysteresis now also activates when the tapper is paused at top voltage and the charge target is 100% (either configuredmax_soc = 100%or weekly full charge active). Active cell balance mode is explicitly excluded.
[1.8.3] - 2026-05-26¶
Added¶
- Active balance mode: Added a per-battery active balancing mode that charges at 95 W to 3.58 V, waits 60 s to measure
delta_V, cycles with 25 W discharge to 3.48 V, repeats untildelta_V <= 0.03 V, persists its phase across restarts, and performs a final 25 W discharge to 3.42 V before completing. - Dynamic Pricing: EPEX Spot support (e.g. aWATTar): New price integration option for sensors that expose hourly prices under a
dataattribute as a list of{start_time, end_time, price_per_kwh}entries (€/kWh). Reuses the same cheap-hour scheduling and price-based discharge gating as the existing Nordpool/PVPC/CKW integrations. - Dynamic Pricing: ENTSO-e Transparency Platform support: New price integration option for sensors that expose prices under
prices_today/prices_tomorrowattributes as a list of{time, price}entries (€/kWh, ISO 8601 timestamps with timezone). Supports both hourly and 15-minute slots — slot end times are inferred from the next entry's start. Reuses the same cheap-hour scheduling and price-based discharge gating as the other price integrations.
Changed¶
- Charge 100% revamped: Normal 100% charging now uses a voltage-only profile. It throttles to 95 W from
max_cell_voltage >= 3.48 V, then stops charging at 3.58 V and waits 60 s to record thedelta_Vreading. Charging is left stopped at that voltage and the normal SOC/charge logic decides when charging is allowed again — no forced discharge in this path. - Dynamic Pricing: integration selector switched to dropdown: The price integration picker in the setup and options flows now renders as a compact dropdown instead of a vertical radio list, keeping the form short as more providers are added.
- Balance sensor names regrouped under a
Balance - …prefix: The five cell-balance diagnostic sensors (Cell Delta,Balance Status,Balance Trend,Last Balance Reading,Delta Average (4 readings)) were renamed across all six translation files so they sort together in the HA UI:Balance - Cell Delta (at 100%),Balance - Status,Balance - Trend,Balance - Last Reading, andBalance - Delta Average (4 readings).Cell Deltaalso gained the(at 100%)qualifier to make explicit that the reading is captured at full charge. Translation keys andentity_ids are unchanged.
Fixed¶
- Venus A manual-mode power entities still capped at 1200 W:
MAX_POWER_BY_VERSION["vA"]was already raised to 1500 W for the config flow and PD calculations, but theset_charge_power,set_discharge_power,max_charge_power, andmax_discharge_powernumber entities inNUMBER_DEFINITIONS_VAwere still hard-capped at 1200 W. Raised to 1500 W so the manual-mode sliders match the configured hardware limit. - System SOC weighted by battery capacity: The
System SOCaggregate sensor now weights each battery's SOC by itsbattery_total_energy, so mixed-capacity systems report the real stored-energy percentage instead of a simple average across batteries. - Disconnected batteries reported in manual mode: The Non-responsive Batteries diagnostic sensor now also reports batteries that become unreachable through Modbus polling failures, so unplugging a battery's Ethernet/Modbus adapter is surfaced even while Manual Mode skips automatic power commands.
- Multi-battery split-load hold uses wall-clock time: The split-load minimum runtime now expires after 120 seconds of real time instead of 48 PD write cycles. If the PD controller remains inside deadband when the hold expires, it now performs a one-time rebalance to release the extra battery instead of leaving it latched until the next correction outside deadband.
- Single-battery idle state after zero-power selection: The load-sharing selector now clears active battery state and split-load hold counters before the single-battery fast path when requested power is 0 W. This prevents one-battery systems from retaining load-sharing state intended for multi-battery split holds while the controller is intentionally idle.
[1.8.2] - 2026-05-15¶
Added¶
- High-SOC charge taper (Still under testing): During any charge, the controller now caps each battery's charge allocation to 500 W from 95% SOC and 100 W from 98% SOC, while still respecting the existing per-battery and system power limits.
Changed¶
- Cleaner debug logging for polling and Modbus reads: Normal debug logs no longer emit one line per skipped sensor, disabled entity, raw Modbus register read, or unchanged aggregate power calculation. Coordinator polling now produces compact per-cycle summaries, while maintainer-level raw Modbus and per-sensor detail remain available behind internal debug switches.
- More useful power-control debug summaries: The PD controller now logs a single
Power planline with the control mode, grid reading, target, error, previous/requested/allocated power, selected batteries, allocation, active charge/discharge blockers, and active setpoint offsets/overrides. Per-battery writes now include explicit requested-vs-readback ACK details for force mode, charge power, discharge power, and reported battery power. - Reduced repeated rate-limiter noise: The PD rate-limiter message is now emitted only when limiting first becomes active, changes direction, or the requested change materially changes, instead of repeating every stable control cycle under sustained surplus or demand.
[1.8.1] - 2026-05-12¶
⚠️ Breaking Change — Dynamic Pricing discharge threshold priority¶
When price-based discharge control is enabled, a configured max_price_threshold now acts as the discharge threshold. If it is empty, Dynamic Pricing falls back to the automatically calculated daily average. The setup/options help text and documentation were updated to describe this priority clearly.
Added¶
- Solar forecast reserve discharge blueprint: Added an optional blueprint that controls the per-battery Allow Discharge switches so the system keeps a configurable night reserve (for example 50 % SOC) unless the remaining solar forecast is high enough to recharge from minimum SOC back to that reserve. This lets users dump energy in the morning when solar production is expected while preserving backup capacity and a healthier overnight SOC when the forecast is weak.
Fixed¶
- Peak shaving conserving-mode discharge loop: Peak shaving now evaluates conserving decisions against the base PD target without reusing its previous capacity-protection override. This prevents below-limit household load from being misclassified as solar surplus and causing short discharge/stop cycles.
- Charge delay SOC setpoint latch survives integration reloads: The charge-delay setpoint hysteresis state is now persisted and restored for the same day. Reloading the integration no longer resets
_delay_setpoint_reachedtoFalseand re-enters "Charging to setpoint" when SOC is still above the hysteresis resume threshold. The Charge Delay diagnostic sensor also restores its same-day latch/unlock state as a fallback.
[1.8.0] - 2026-05-11¶
Added¶
- Unified charge/discharge blocker registry: The PD controller now uses explicit runtime blocker registries for charge and discharge decisions. Blockers can be global (system-wide) or scoped to an individual battery, and are exposed on the Integration Status diagnostic sensor via
charge_blockers,discharge_blockers,battery_charge_blockers, andbattery_discharge_blockers. Existing restrictions such as charge delay, time slots, price-based discharge control, EV charger no-telemetry handling, and per-battery user blocks now share the same decision path. - Per-battery Allow Charge / Allow Discharge switches: Each battery now exposes two software controls to include or exclude it from automatic PD charging or discharging independently. The switches persist in the config entry as
allow_chargeandallow_discharge, default to enabled for existing installations, and do not write Modbus registers directly. Disabling a direction stops that battery if it is currently active and lets the next PD cycle reallocate power to the remaining eligible batteries. - System-wide charge/discharge power caps: Advanced PD Controller settings now include an
Enable system power limitscheckbox plus optionalSystem Max Charge PowerandSystem Max Discharge Powersliders. With the checkbox off, the previous behavior is kept and the runtime slider entities are not created; with it on, any positive cap limits the combined power across active batteries while still allowing an individual battery to use its own full limit when the rest of the system is idle. The Configuration Summary sensor reports whether the feature is enabled plus both configured totals and effective capped totals for support diagnostics.
Changed¶
- Peak shaving peak limit range extended: The peak limit selector in setup/options and the runtime number entity now accepts
500 Wto10000 Win100 Wsteps. - PD blocker enforcement before deadband and stale-sensor early returns: Active charge/discharge commands are now stopped immediately when a matching global or per-battery blocker appears, even if the grid sensor is inside deadband or has not updated. This prevents stale commands from continuing after a feature or user switch has blocked that direction.
- SOC limits and charge hysteresis are now visible in the blocker registry: Per-battery charge blocking caused by configured maximum SOC or active charge hysteresis is now recorded in
battery_charge_blockers, and discharge blocking caused by minimum SOC is recorded inbattery_discharge_blockers. The top-levelcharge_blockedanddischarge_blockedattributes on Integration Status report the effective system state, so they becometruewhen every known battery is blocked in that direction, even if the reason is per-battery rather than global. - Hourly net balance uses the charge blocker registry for block reasons: Positive hourly-balance compensation now reads the unified charge blockers for reasons such as solar charge delay, charge time-slot restriction, or EV pause, while keeping its existing local checks for charge hysteresis and max SOC.
Fixed¶
- Peak shaving conserving mode no longer sends discharge commands below the peak limit: When SOC is below the peak-shaving threshold but the estimated house load is still below the configured peak limit, the controller now targets the current grid level and immediately stops any existing discharge command. This prevents the battery from trying to discharge during normal consumption and being incorrectly marked as non-responsive.
- Dynamic-price unit guidance and CKW parser compatibility: Setup/options help text now asks for thresholds in the real sensor scale (
€/kWhfor Nordpool/PVPC,CHF/kWhfor CKW), avoiding the old cent/Rappen guidance. The CKW slot parser also accepts CKW-derived entries that expose the total price asvalueorintegratedin addition toprice, without converting from Rappen. - CKW price-based discharge block in Dynamic Pricing: The discharge blocker no longer reads CKW's all-prices sensor state as the current price. That sensor may expose the number of slots (for example
96) as its state and the real 15-minute prices in thepricesattribute, so the blocker now derives the active slot price fromprices. Dynamic Pricing uses the configured max price threshold for discharge blocking when present, otherwise it uses the daily slot average.
[1.7.6] - 2026-05-09¶
Changed¶
- Integration Status diagnostic sensor refreshed: The status sensor now surfaces newer runtime features and blockers instead of falling back only to generic charging/discharging states. It can now report price-based discharge blocking, hourly net balance compensation/caps/blocks, peak-shaving actions, EV charger no-telemetry pauses, cell-balance holds, and backup/offgrid cooldowns. Diagnostic attributes were expanded with the active setpoint, price mode state, hourly balance snapshot, capacity-protection details, EV pause state, backup cooldowns, and non-responsive batteries.
- Configuration Summary diagnostic sensor refreshed: The hidden support sensor now omits battery IP addresses and ports, and adds the configuration that matters for issue triage: household consumption sensor, manual/predictive override state, total configured battery power, backup off-grid thresholds, predictive safety margin, balance monitor, charge-delay SOC setpoint, hourly balance parameters, PD target grid power, and EV charger no-telemetry flags on excluded devices.
- Dynamic pricing: discharge block evaluated reactively per cycle, like real-time price mode:
_apply_price_discharge_block()no longer uses the pre-scheduledselected_slotsto decide whether to block discharge. The slot list still governs grid charging (when to actively pull energy from the grid), but the discharge decision now comparescurrent_priceagainst the threshold (average_price_sensorif configured, otherwisemax_price_threshold) on every control cycle, identical to real-time price mode. This eliminates two blind windows where the previous slot short-circuit could not fire because_dynamic_pricing_schedulewasNone: the ~15 s gap after a Home Assistant restart inside a cheap slot (the schedule lives only in memory and is rebuilt by the startup evaluation), and the 5-minute gap each midnight between Phase 3 daily reset and the 00:05 evaluation. In both gaps the slot-based block silently went unset and a transient sensor reading abovemax_price_threshold(slot-boundary lag, float precision) was enough to let the PD controller initiate discharge against an actually-cheap price. With the change, DP and RT now share the exact same per-cycle discharge logic; the only behavioural difference between the two modes is how they decide when to grid-charge (DP: pre-scheduled cheap slots; RT: reactive price crossing). - Hourly net balance: added smart meter requirement notice: The feature description now includes a warning that hourly net balance control is only beneficial when using electricity contracts with smart meters that perform hourly net balance calculation. Without hourly netting at the meter level, battery setpoint adjustments do not translate to cost savings. Notice added to all language translations (EN, ES, DE, FR, NL).
- PD Target Grid Power range extended to ±2500 W: The slider in the setup and options flows (Advanced PD Controller) and the number entity limits now accept values from −2500 W to +2500 W (previously −500 W to +500 W).
Fixed¶
- Charge delay estimated unlock time drifted minute by minute: The Charge Delay sensor's estimated unlock time used a legacy consumption projection based on daylight hours, while the real unlock condition used the configured consumption window. When the estimate fell into the past but the real condition still kept the delay active, the displayed time followed the current clock minute by minute (e.g. 12:52, 12:53, 12:54) until the actual unlock. The estimator now uses the same consumption-window proration as the live delay logic, and stale "unlock now" estimates are ignored unless the real energy-balance condition is also met.
- Multi-battery split-load minimum runtime added: The symmetric activation/deactivation deadband prevented threshold chatter, but the PD controller could still react strongly immediately after another battery joined and push the next requested power below the deactivation side of the band. That made the extra battery bounce between idle and a large share of the load under high, bursty household demand. When the selector decides that more than one battery should participate, the whole split is now held for ~2 minutes and the timer is refreshed whenever the split-load condition is met again. The hold is cleared only when the requested power goes to 0 W or the controller switches direction.
- Multi-battery selection oscillates near the activation threshold: The previous hysteresis logic only guarded deactivation (removing the last added battery if power dropped slightly below threshold), but had no guard on activation. In practice this caused batteries to rapidly turn on and off when load hovered near the crossover point. Replaced with a symmetric deadband: Case A (deactivation) — a previously-active battery dropped by the greedy loop is re-added unless power has fallen clearly below
activation_threshold − hysteresis_gap; Case B (activation) — a newly-added battery is removed again unless power has risen clearly aboveactivation_threshold + hysteresis_gap. Both cases cover all batteries inprevious_active, not just the last one selected. - Charge delay SOC setpoint oscillation: When the SOC setpoint was enabled and the delay was active, any brief drop below the setpoint (due to home consumption) caused the system to immediately re-enable charging back to the setpoint, creating a charge/block oscillation loop. Fixed by adding a 3 % hysteresis band: once the setpoint has been reached and the delay becomes active, charging to the setpoint only resumes if the SOC falls at least 3 % below the configured setpoint threshold (
_delay_setpoint_reachedflag, reset daily). - Hourly net balance: remove closed-hour history: The Balance Neto sensor no longer exposes the
historyattribute and the manager no longer persists closed-hour entries. Only the current-hour accumulator is kept across restarts. - Hourly net balance: clear offset state when disabled: Turning off the Hourly Net Balance switch now clears both the controller setpoint offset and the manager's internal visible offset state (
offset_w,theoretical_offset_w, and block reason). The same cleanup is used when the feature is disabled through options, manual mode, or outside active slots. - Peak shaving takes priority over hourly net balance: The PD controller now refreshes hourly net balance and peak shaving setpoint overrides before deadband and first-execution handling. This prevents an hourly net balance discharge command from being kept alive after peak shaving becomes SOC-limited; stale consumption readings also force a recalculation instead of maintaining an existing discharge while peak shaving is active.
- Hourly net balance: restore import compensation discharge: Net import above the hourly target now produces a negative setpoint offset again, allowing the battery to discharge/export during the remaining minutes of the hour to bring the net balance back toward target. This fixes the regression where the offset was clamped to zero and the PD controller could keep charging despite accumulated hourly import.
[1.7.5] - 2026-05-08¶
Added¶
- Hourly Net Balance (
feature/hourly-net-balance): New feature that tracks grid import and export within each civil hour and adjusts the PD setpoint offset in real time to drive the net energy toward a configurable target (default 0 Wh). The offset is calculated as-(deficit_Wh / remaining_h), saturated at a configurable maximum, with optional ramp-in during the first minutes of each hour and a hysteresis band to prevent micro-adjustments every cycle. Applies only within configured discharge time slots (or 24/7 when none are defined); the offset is cleared automatically outside slots and in manual mode. Capacity Protection overrides take priority automatically via the existing setpoint registry (priority=10). State is persisted to Home Assistant storage and restored after a restart (mid-hour accumulator restored only if still in the same civil hour). When the feature is enabled a single diagnostic sensor Balance Neto is added (see below). Configuration is available in both the initial setup flow and the options flow under the new "Hourly net balance" section. - Balance Neto sensor: Single diagnostic sensor that consolidates all hourly balance information. Its state is the net energy for the current hour in kWh (positive = net export to grid, negative = net import). Attributes:
status(compensating_import / compensating_export / capped / compensation_stopped / out_of_slot / idle),offset_w(active setpoint correction),imp_wh/exp_wh(import and export breakdown),target_net_wh,remaining_min,source(see below),hour_iso, andcharge_block_reasonwhen compensation is blocked. - External net balance sensor autodiscovery: On startup the integration looks for
sensor.balance_neto(configurable list inconst.py → EXTERNAL_NET_BALANCE_CANDIDATES). If found, it uses that sensor as the source for hourly net tracking instead of the built-in trapezoidal integration of the grid power sensor, which is subject to polling gaps. The sensor type is detected automatically from itsunit_of_measurementandstate_class: cumulative sensors (total/total_increasing) use a snapshot-at-hour-start approach; measurement sensors are read directly; power sensors (W/kW) fall back to trapezoidal integration. If the external sensor is not present or goes unavailable, the integration falls back to the trapezoidal method transparently. The active source is visible in thesourceattribute of the Balance Neto sensor. Sign convention: positive external sensor value = net export to grid. - Setpoint offset registry: New two-layer system for dynamic PD target control with a fixed reference of 0 W (zero grid flow). Features that need to adjust the PD target can now register either additive offsets (summed together to form the default target) or absolute overrides (highest priority wins, replaces the additive sum entirely). The user's
target_grid_powerpreference is registered as an additive offset (user_target), and capacity protection uses an absolute override with priority 10. Public API:set_setpoint_offset(),remove_setpoint_offset(),set_setpoint_override(),remove_setpoint_override(),compute_active_target(). This provides a clean extension point for future features like hourly net balance or price-based arbitrage. - Setpoint diagnostics on Integration Status sensor: The
Integration Statussensor now exposessetpoint_active,setpoint_offsets, andsetpoint_overridesas extra state attributes, showing the effective PD target and which features are contributing to it.
Changed¶
- Capacity protection migrated to setpoint overrides: Capacity protection no longer directly modifies the internal
active_targetvariable. It registers an absolute override via the new setpoint registry, allowing proper composition with other features. Behaviour is unchanged — when capacity protection is active, its override (priority 10) takes full control of the PD target.
Fixed¶
- Dynamic pricing: discharge blocked unconditionally inside selected cheap slots: Previously
_apply_price_discharge_block()comparedcurrent_price(from the sensor entity state) against_dp_daily_avg_price(averaged from thepricesattribute). When the provider only exposes end-of-day slots all at the same price (e.g. CKW at 23:00 with 4 × 0.2167 CHF/kWh slots), these two values are nominally equal but can diverge due to float precision differences between the sensor state string and the attribute entries, makingcurrent_price > thresholdTrue and leaving the block unset. Fixed by short-circuiting the price comparison when_is_in_dynamic_pricing_slot()is True: the slot was already identified as cheap during evaluation, so discharge is blocked unconditionally (unless override is active). - Dynamic pricing: discharge threshold outside cheap slots ignored
average_price_sensor: Outside selected cheap slots,_apply_price_discharge_block()usedmax_price_thresholddirectly, while real-time price mode usedaverage_price_sensor(if configured) and fell back tomax_price_threshold. This made the effective discharge threshold differ between modes even though the intended behaviour is identical — both should allow discharge only when the current price exceeds the threshold. Fixed by applying the same threshold resolution in DP outside-slot path:average_price_sensorvalue if available, otherwisemax_price_threshold. - CKW price unit in notifications showed "Rp/kWh" instead of "CHF/kWh":
_get_price_unit()returned"Rp/kWh"for the CKW integration but prices are stored in CHF/kWh. Corrected to"CHF/kWh"so slot prices and costs in persistent notifications display the correct unit. - Maximum price threshold cannot be cleared in the options flow: The
max_price_thresholdfield in both the dynamic pricing and real-time price options steps usedvol.Optional(key, default=old_value). When a user cleared the field and submitted, the HA frontend omitted the key and voluptuous restored the old value via itsdefault, making the field impossible to blank. Fixed by switching todescription={"suggested_value": old_value}, which pre-fills the field visually without affecting validation when the field is cleared. - Battery falsely excluded as non-responsive at min-SOC: When the polling SOC reported a value just above
min_soc(e.g. 30.1 % with min_soc = 30 %), the battery passed the_get_available_batteries()filter and received a discharge command. The internal BMS, however, had already reached its discharge cutoff and blocked power delivery (0 W). The feedback check interpreted this as a non-delivery and, after 3 consecutive cycles, excluded the battery. Fixed by adding a guard beforerecord_non_delivery: ifcurrent_soc ≤ min_soc + 1 %, the 0 W output is treated as expected BMS protection behaviour rather than a fault, and the non-delivery counter is not incremented.
[1.7.4] - 2026-05-02¶
Added¶
- Live charge hysteresis control per battery: A new number entity (
Charge Hysteresis Percent) is now exposed for each battery that has charge hysteresis enabled. The value can be changed at runtime from the battery's configuration menu without reloading the integration. Range expanded from 5-20% to 5-50%.
Changed¶
- Charge hysteresis maximum increased to 50%: The config flow slider now allows values up to 50% (previously 20%), giving more flexibility for batteries with aggressive BMS cutoff behavior.
- Centralized price-based discharge block computation: The
_price_based_discharge_blockedflag is now computed once per cycle in a dedicated_apply_price_discharge_block()method before mode dispatch, instead of being set inside each price handler (dynamic pricing and real-time price). This guarantees the flag is always set even when a handler returns early (override active, cheap-slot active, max_soc transition), preventing PD discharge under cheap prices in those edge cases. - More specific blocking reason in logs: When charging is not allowed, the log message now distinguishes between "charge delay active" and "time slot configuration" so users can immediately understand why charging was blocked.
- Venus A maximum charge/discharge power updated to 1500 W:
MAX_POWER_BY_VERSION["vA"]was set to 1200 W, which underestimated the hardware limit. Updated to 1500 W so the config flow slider and all power calculations reflect the correct physical maximum.
[1.7.3] - 2026-04-29¶
Fixed¶
- Real-time price mode charges outside the configured charge time slot:
_handle_realtime_price_predictive_chargingactivated grid charging based solely on price, ignoringapply_to_chargeslot restrictions. Fixed by checking_is_operation_allowed(is_charging=True)before starting a session and every cycle while charging is active. - Real-time price mode discharges during cheap-price periods after grid charging completes: When predictive grid charging finished it set
first_execution=True, causing the PD controller's first-execution path to start discharging without checking_price_based_discharge_blocked. Fixed by adding the price-based discharge block check to the first-execution path. - BMS cuts off charging at 99 % during weekly full charge (or with max_soc = 100 %): Some cells stop accepting charge before the SOC register reaches 100 %, leaving the integration in forced-charge mode indefinitely. Fixed by detecting BMS cutoff via
battery_power ≤ 10 Wandinverter_state == Standbyat SOC ≥ 99 %. After five consecutive cycles (~10 s) the battery is treated as full, triggering normal completion (register restore, hysteresis re-enable, state persist). The same check also prevents useless charge allocations in_get_available_batteries()for the general max_soc = 100 % case. - Charge delay sensor showed
target_soc: nullwhen charging was already allowed:_is_charge_delayed()computedtarget_socafter several early-return paths, so the attribute was never set when the delay was skipped. Moved the computation before the early returns so the value is always present. - No indication in the charge delay sensor that the delay was intentionally skipped on the weekly full charge day: When the balance monitor bypasses the delay on the full charge day, the sensor showed
"Charging allowed"— indistinguishable from a normal unlock. A new stateskipped_full_charge_dayhas been added with translations for EN, ES, DE, FR and NL. - Weekly full charge does not complete after HA restart or integration update: On restart,
async_setup_entrywritesmax_socback to the hardware cutoff register before the persisted state is loaded, sohandle_registers()incorrectly assumed the 100 % register was already in place and skipped the write. Fixed by using the in-memory_weekly_charge_saved_max_socdict as a session proxy — empty on every restart — to force a re-apply of the 100 % cutoff. The status field is now also persisted and restored so the sensor shows the correct state immediately after restart.
Changed¶
- Multi-battery activation threshold is now dynamic based on configured power: The second battery was always activated at 60 % of the configured capacity, which was only correct at the 2500 W default. The threshold is now derived from efficiency crossover points (1500 W discharge / 1750 W charge) expressed as a fraction of the configured max, clamped to [50 %, 95 %]. Users at the 2500 W default see no change in discharge and a slightly later charge activation (70 % instead of 60 %). Five new constants added to
const.py.
[1.7.2] - 2026-04-28¶
⚠️ Breaking Change — Target Grid Power¶
The per-timeslot "Target Grid Power" field has been removed.
Previously each discharge time slot had its own independent Target Grid Power offset (the watt setpoint the PD controller regulated the grid to during that slot). That per-slot field no longer exists.
It has been replaced by a single global "PD Target Grid Power" setting that applies for the entire integration, regardless of which slot is active. The new control is:
- Configurable in the Options → Advanced PD Controller flow.
- Exposed as a live number entity (
number.marstek_venus_system_pd_target_grid_power) that can be changed at runtime without reloading the integration. - Default: 0 W (net-zero grid regulation, same as the previous default).
Action required on upgrade: If any of your timeslots had a non-zero Target Grid Power, the value has been reset to 0 W. Open Options → Advanced PD Controller and set the new global value to your desired setpoint.
Fixed¶
- Total Charging/Discharging Energy sensor periodically resets to near-zero: The battery firmware occasionally returns a corrupt partial value when a Modbus read of the 32-bit energy counter coincides with an internal firmware update of that register. The read technically succeeds (no error logged), but the decoded value can be a fraction of the real counter (e.g. ~50 kWh instead of ~491 kWh). Because the sensor has
state_class: total_increasing, Home Assistant interprets the drop as a meter reset and permanently corrupts Energy Dashboard statistics. Fixed by adding a monotonic guard in the coordinator: for anytotal_increasingsensor, a new reading that is positive but less than 90 % of the last known value is silently discarded. Drops to exactly 0 (daily counter midnight reset, factory reset) are still accepted. Applies tototal_charging_energy,total_discharging_energy,total_daily_charging_energy, andtotal_daily_discharging_energy.
Changed¶
- Entity unique IDs and device identifiers now include the Modbus port: Until now entity unique IDs followed the pattern
{host}_{key}and device identifiers were(DOMAIN, host). This made it impossible to register two batteries reachable at the same IP on different Modbus ports — a setup that exists when several Venus units share a single bridge or NAT mapping. Both the unique ID and device identifier now include the port:{host}_{port}_{key}and(DOMAIN, "{host}_{port}"). The change is applied acrosssensor,binary_sensor,switch,number,select,button,balance_sensorsandcalculated_sensors. - Config entry version bumped to 2 with automatic migration: An
async_migrate_entryhandler renames existing entity unique IDs from{host}_{key}to{host}_{port}_{key}and updates the device identifier in the device registry. Home Assistant automatically populates theprevious_unique_idfield on each migrated entity, so long-term statistics (energy dashboard, history graphs) keep linking to the same entity without interruption. The migration is idempotent and runs once per config entry on first load with the new version.
[1.7.1] - 2026-04-26¶
Added¶
- Live max/min SOC controls for v3/vA/vD batteries: Batteries on firmware v3, vA and vD do not expose a Modbus register for the charging/discharging cutoff capacity, so until now
max_socandmin_soccould only be edited through the options flow (with an integration reload). Each affected battery now gets two number entities —Charging Cutoff CapacityandDischarging Cutoff Capacity— that update the software-enforced limits read by the PD controller in real time and persist the new value in the config entry. v2 batteries are unaffected: they keep using the existing hardware-backed entities. The translation keys and unique IDs match the v2 ones, so the entity names and entity IDs remain consistent across hardware variants.
Fixed¶
- Charge delay unlocked prematurely by
energy_balancedue to consumption being prorated against daylight hours: The energy-balance check in_should_delay_chargecomputedremaining_consumption_kwh = (avg_consumption / daylight_hours) * hours_to_t_end, dividing the daily total byt_end − t_start. This implicitly assumed the entire day's consumption happens during daylight, which inflated the projected midday consumption by roughly 24 / daylight_hours (≈1.7× in summer) and madenet_solar_for_batterylook much smaller than it really is. With a high household consumption (~38 kWh/day in the reported case), the inflated estimate pushednet_solarbelow the safety threshold (energy_needed × DELAY_SAFETY_FACTOR) even when the remaining solar production was clearly enough to top off the battery — triggering a spurious unlock at midday and allowing grid charging the rest of the day. Theavg_consumptionvalue is in fact already scoped to the consumption window for both data sources: whenhousehold_consumption_sensoris configured,accumulate_household_consumptiononly integrates whileis_in_consumption_window()is true (24h if nocharging_time_slotis set, otherwise the hours outside the slot, only on slot days); without that sensor, the captured value istotal_daily_discharging_energy + _daily_grid_at_min_soc_kwh, both of which only grow during the same window. Fixed by prorating against the actual consumption-window duration: two new helpersget_consumption_window_hours_per_day()andconsumption_window_hours_in_range(from_h, to_h)were added toConsumptionTracker, and the formula is nowavg_consumption × remaining_window_hours / window_hours_per_day. With no slot configured this collapses to a uniform 24h distribution; with a slot it correctly excludes the grid-charging window from both numerator and denominator. The displayedremaining_consumption_kwhattribute is consequently lower and more realistic, and the energy-balance unlock now triggers only when the remaining solar genuinely cannot cover the gap. - Spurious warning on v3 weekly full charge: Each time a weekly full charge activated on a v3 battery, a
WARNING-level log entry appeared stating that software enforcement would be used (instead of the hardware cutoff register). This is expected behaviour for v3 hardware and does not indicate a problem. The message has been downgraded toDEBUGso it only appears when debug logging is explicitly enabled. - Alarming
WARNINGwhen the daily consumption capture had nothing to record: On days not covered bycharging_time_slot.daysthe household accumulator legitimately stays at 0 — the integration only accumulates outside the charging slot, and skips entire days that aren't in the slot's day list. The 23:55 capture would then logWARNING: household accumulator too low (0.00 kWh), skipping, which sounded like data loss. In realityget_dynamic_base_consumption()already self-heals: on the next predictive-charging cycle it runs an opportunistic recorder backfill that replaces the day's default sentinel (DEFAULT_BASE_CONSUMPTION_KWH = 5.0) with the real value from the household sensor history. The message has been downgraded toINFOand reworded to make the self-healing behaviour explicit, so the log entry no longer suggests something is broken.
Changed (internal)¶
__init__.pyandcoordinator.pysplit into focused modules: Pure code-movement refactor with no behavioural change. Roughly 1,500 lines were extracted from__init__.py(which had grown to ~5,660 LOC) and ~95 lines fromcoordinator.py, replicating the existingbalance_monitor.pyextraction template — the controller still owns the public attributes that sensors and switches read, while the new modules own the logic, persistentStores, and lifecycle. New files:non_responsive_tracker.py— non-responsive battery detection and 5-minute exclusion windows.alarm_notifier.py— alarm/fault bit-delta detection and HA persistent-notification formatting (extracted from the coordinator; the previous-bitmask state lives with the notifier now).weekly_full_charge.py— weekly full charge state, persistence and register-write orchestration; bundled persistence format (weekly + delay_unlocked + solar_t_start) preserved for backward compatibility.consumption_tracker.py— consumption history, daily energy accumulators (household + solar production), solar-timing detection (t_start/t_end/solar-noon), recorder backfill, and the 23:55 local-time daily capture. TheDEFAULT_BASE_CONSUMPTION_KWHsentinel, timezone-aware recorder queries, and the rule that_capture_from_historyreplaces default entries instead of appending are all preserved.
[1.7.0] - 2026-04-25¶
Added¶
- Per-device enable/disable switch for excluded devices: Each excluded device now gets a dedicated
{Device} – Enabledswitch entity. Turning it off removes the device from all power calculations (charge offset, adjustment, and EV-charger state checks) without deleting it from the configuration. The state is persisted in config entry data and survives HA restarts. Useful for temporarily pausing a device (e.g. a car charger that is unplugged for days) without having to re-enter the options flow. - Cell balance monitor: New optional feature (enabled in the weekly full charge configuration step) that tracks the voltage spread between the strongest and weakest cell after each weekly full charge. When enabled, the battery is held at rest for 15 minutes after reaching 100 % SOC so the cell voltages can settle to their true open-circuit values; then a formal reading is taken. Thresholds are fixed at 50 / 100 / 150 mV (green / yellow → orange / red). An orange reading triggers an additional 2.5-hour passive balancing hold before a follow-up reading; red on two consecutive full charges fires a "possible degraded cell" alert.
- Opportunistic readings: Outside the weekly full charge day, if the battery reaches 100 % SOC and power is already below 50 W, a lightweight reading is taken without holding discharge — useful on days with heavy solar generation. Limited to once every 24 hours.
- Five new sensor entities per battery:
Cell Voltage Delta (mV),Balance Status(green / yellow / orange / red),Delta Trend(rising / stable / falling),Last Balance Read(timestamp), and4-Week Average Delta (mV). Values are restored from persistent storage on HA restart. - Rising-trend notification: If the 4-week rolling average of formal readings exceeds 75 mV and the trend is rising, a persistent notification is sent to prompt the user to monitor battery health.
- Discharge blocking during OCV wait: While waiting for the cell voltages to settle, the battery is prevented from discharging so that the reading reflects true open-circuit conditions. Discharge resumes automatically once the reading is complete (or after the 2.5-hour orange hold).
- Balance history persistence: All readings (formal, follow-up, opportunistic) are stored in a per-entry JSON store and survive HA restarts and reloads.
- WiFi Signal Strength sensor: New diagnostic sensor (register 30303, dBm) available for all battery versions. Disabled by default.
- User Work Mode select: New select entity (register 43000) available for all battery versions, with options
Manual,Self Consumption(anti feed-in), andAI Optimization(trade mode). Disabled by default. Fully translated into EN, ES, DE, FR, and NL.
Removed¶
- Excluded Devices diagnostic sensor: The
Excluded Devicessensor (which reported the count and details of excluded devices as attributes) has been removed. The same information is now directly visible through the per-device{Device} – Enabledand{Device} – Solar Surplusswitch entities added in this release.
Changed¶
- Predictive charging: grid-only SOC target: When predictive charging activates, the battery is no longer charged all the way to
max_socfrom the grid. Instead, the integration calculates how much solar will remain after covering expected household consumption (solar surplus) and charges from the grid only the portion solar cannot cover:
solar_surplus = max(0, solar_forecast − estimated_consumption)
grid_charge = max(0, gap_to_max − solar_surplus)
target_soc = current_soc + grid_charge / capacity × 100
where gap_to_max is the kWh distance from the current SOC to max_soc. Solar output in excess of household demand charges the battery the rest of the way during the day. If solar surplus equals or exceeds the gap, no grid charging is needed and the trigger condition (energy_deficit > 0) already prevents it. Applies to all three modes (time slot, dynamic pricing, real-time price).
In systems with multiple batteries at different SOC levels, the grid charge is distributed proportionally to each battery's individual gap to max_soc. A battery further from full receives a larger share of the grid charge; a battery already close to full relies mostly on solar for its remainder. This avoids overloading any single battery from the grid and minimises total grid import.
- Options menu label: "No-discharge time slots" → "Discharge time slots": The menu entry in the options flow was misleading — it read as a window where the battery is prevented from discharging, but the feature actually defines the only windows where discharging is allowed. The label now matches the step title and description already used inside the step. Updated in all six translation files (
en,es,de,fr,nl,strings). - WiFi Status and Cloud Status moved to diagnostic category: Both binary sensors now appear under Diagnostics in the HA device page. The
category: diagnosticfield was already present in their definitions but was not being read by the binary sensor platform — fixed by applying the same pattern already used for regular sensors. - Version and connectivity sensors disabled by default: Software Version, BMS Version, EMS Version, Comm Module Firmware, WiFi Signal Strength, WiFi Status, and Cloud Status are now disabled by default across all hardware variants. They can be enabled individually from the entity registry if needed.
- Charge Hysteresis binary sensor now translated: The sensor was previously named
{Battery} Charge Hysteresis Activein hard-coded English. It now uses a translation key (charge_hysteresis) andhas_entity_name, so the name is rendered in the user's language without redundant "Active" suffix. Updated in all six translation files. - Backup Offgrid Threshold moved from Configuration to Controls: The number entity is no longer grouped under Configuration in the HA device page and appears directly in the main controls section.
- Cell voltage sensors always display 3 decimal places: Max Cell Voltage and Min Cell Voltage previously dropped trailing zeros (e.g.
3.2 Vinstead of3.200 V). The display precision is now set to 3 decimal places, matching the measurement resolution. - Several per-battery sensors moved to diagnostic category: The following sensors are now grouped under Diagnostics in the HA device page: Fault Status, Alarm Status, Round-Trip Efficiency, Battery Cycle Count, Cell Delta, Delta Average (4-week), Balance Status, Delta Trend, and Last Balance Read.
- Integration Status sensor moved to diagnostic category: The sensor is now grouped under the Diagnostic section in the HA device page, keeping the main entity list focused on operational entities.
- Time Slot switch now translated: The switch was previously named
Time Slot {N}in hard-coded English. It now uses a translation key (time_slot) with a{slot_number}placeholder, so the name is rendered in the user's language. Updated in all six translation files (en,es,de,fr,nl,strings). - Solar Surplus switch renamed and translated: The switch was previously named
Solar Surplus – {device}(hard-coded English). It is now{device} – Solar Surplus, using a translation key (excluded_device_solar_surplus) with a device placeholder. The name inversion ensures all switches for the same excluded device sort together in the HA entity list —{device} – Enabledimmediately followed by{device} – Solar Surplus— instead of all Solar Surplus switches grouping away from the Enabled ones. All six translation files (en,es,de,fr,nl,strings) have been updated. - Number entity names aligned with feature prefixes: Several number entities were renamed so that related entities sort together in the HA entity list, examples:
delay_safety_margin_min:Charge Delay Margin→Charge Delay Safety Margindelay_soc_setpoint:Charge Delay SOC→Charge Delay SOC Setpoint
Fixed¶
- Price-based discharge control ignored while battery is in steady-state discharge: Even after the handler correctly set
_price_based_discharge_blocked, the enforcement at line 5121 could be skipped by the deadband or stale-sensor early-returns in the main control loop. When the system was in equilibrium (grid ≈ 0 W, battery actively discharging) and the price dropped below the daily average, the flag was set but the function exited through the deadband path before applying it — leaving the battery discharging until the load changed enough to exit the deadband. Fixed by inserting a dedicated enforcement block immediately after the predictive-charging dispatcher, before any sensor-dependent early-returns. - AC Offgrid Power wraps to ~65000 W when solar panels are connected to the backup port: On firmware 148+, the battery reports negative values on the AC Offgrid Power register when solar panels feed power through the backup port. The register was decoded as
uint16, causing a 16-bit wraparound (e.g. −100 W → 65436 W). This falsely triggered the backup-active guard, stopping PD control entirely. Fixed by decoding the register asint16for v2 and v3 hardware variants. - Price-based discharge control bypassed in dynamic pricing mode: When the system was inside a cheap-slot window but decided not to activate grid charging (informational schedule with no deficit, charge delay active, or pre-evaluation skip), the handler returned early before setting
_price_based_discharge_blocked. The battery would then discharge even with "Discharge only when price exceeds daily average" enabled. Fixed by converting the three early-return paths into fall-through branches so the discharge control block always runs. enabled_by_default: falsewas ignored: Modbus register entities (sensor, select, binary sensor, switch, number, button) did not propagate theenabled_by_defaultflag from their definition to the HA entity registry. All Modbus entity classes now set_attr_entity_registry_enabled_defaultfrom the definition, so entities markedenabled_by_default: falseare correctly disabled in the registry for new installations.- User Work Mode displayed wrong option due to battery firmware bug: The battery's Modbus register for user work mode returns an incorrect value on readback. The integration now uses a persistent shadow state — the last value written is stored in config entry data and used for display instead of the polled register value. The shadow survives HA restarts. The register is still written correctly so the battery operates in the selected mode.
- Predictive charging starts despite switch being off (time slot mode): When the predictive charging switch was turned off during an active time slot, charging stopped correctly. However, when the slot ended, the override flag was silently reset to
Falsein memory — so on the next slot cycle the switch appeared on again and charging started. The auto-reset on slot exit has been removed; the override now persists until the user explicitly turns the switch back on. - Predictive charging starts despite switch being off (real-time price mode): The real-time price handler never consulted
predictive_charging_overriddenbefore activating charging. If the price dropped below the threshold while the switch was off, charging would start regardless. The handler now checks the override at the top of every cycle and stops any active charging immediately if it is set. - v3 batteries: weekly full charge interrupted after HA restart when charge delay was enabled: After a Home Assistant restart on the weekly full charge day,
_charge_delay_unlockedwas correctly restored from persistent storage but was then immediately overwritten toFalseby the daily-reset block, because_charge_delay_last_dateis an in-memory variable that always starts asNoneafter a restart. For v2 batteries this had no visible effect (the hardware cutoff register at 100 % remained set regardless of software state), but for v3 batteries the software-enforcedeffective_max_socdropped back to the user's configured limit, stopping the charge mid-way. Fixed by only resetting_charge_delay_unlockedon a genuine day change (_charge_delay_last_date is not None); on the first cycle after a restart the restored value from storage is preserved. - Dynamic pricing: Nordpool prices off by factor 100: The Nordpool price parser was dividing sensor values by 100, based on a wrong assumption that the integration reports in ct/kWh. The Nordpool integration reports directly in €/kWh (e.g. 0.072), so the stored slot prices were 100× too small (0.00072). This caused cheapest-slot selection to be correct (relative order unchanged) but made the
max_price_thresholdfilter never trigger, and broke the price-based discharge control — which compares the live sensor price (€/kWh scale) against the daily average computed from slots (was ct/kWh scale). Fixed by removing the division. - Excluded devices not subtracted from household consumption history: The daily household energy accumulator (used by predictive grid charging to estimate typical consumption) integrated the raw household consumption sensor reading without accounting for excluded devices. Devices marked as included in consumption sensor (i.e. the home sensor sees them, but the battery is configured to ignore them) were counted toward the consumption the battery was expected to cover, causing predictive charging to overestimate demand and charge from the grid unnecessarily. Fixed by applying the same per-device correction at accumulation time: power from
included_in_consumption=Truedevices is subtracted from the reading and power fromincluded_in_consumption=Falsedevices (not visible to the home sensor but covered by the battery) is added. The same correction is now applied in the historical backfill path (_backfill_household_from_history), which queries each excluded device's recorder history for past days and adjusts the integrated value accordingly — so consumption history populated after a restart is also accurate.
[1.6.6] - 2026-04-16¶
Added¶
- PD controller advanced step in initial config flow: The setup wizard now includes a final step for advanced PD controller tuning, matching what was already available in the options flow. After the peak shaving step, users are asked whether they want to configure the PD parameters (Kp, Kd, deadband, max power change, direction hysteresis, minimum charge/discharge power). Choosing "No" applies the defaults automatically; choosing "Yes" opens the parameter form. This ensures expert users can tune the controller at first installation without needing to re-enter the options flow afterwards.
- Options flow menu: The options flow now uses a menu instead of a linear wizard, allowing users to jump directly to any section — sensors, batteries, time slots, excluded devices, predictive charging, weekly full charge, solar charge delay, peak shaving, or PD controller — without stepping through unrelated screens.
- Solar forecast safety margin: A configurable kWh buffer can now be added to the consumption forecast when deciding whether to charge from the grid. Useful when your solar forecast integration (e.g. forecast.solar with multiple string arrays) tends to be optimistic. Set in the predictive charging config step for all three modes (time slot, dynamic pricing, real-time price) and also adjustable at runtime via the new Solar Forecast Safety Margin number entity under Marstek Venus System — changes take effect immediately without restarting. Defaults to 0 kWh (no change to existing behaviour). Capped at total battery capacity as a guardrail.
Changed¶
- Peak shaving SOC threshold minimum lowered to 20 %: The minimum selectable value for the peak shaving SOC threshold has been reduced from 30 % to 20 %, both in the setup/options flow slider and in the corresponding number entity (
number.marstek_venus_system_capacity_protection_soc_threshold). This allows configuring peak shaving to activate at lower SOC levels than previously permitted.
Fixed¶
- Weekly full charge mid-charge abort restored 100% instead of original max SOC: When the user changed the weekly full charge day (or disabled the feature) while a v2 battery's cutoff register was already set to 100 %, the hardware restore wrote 100 % back instead of the user's configured limit. Root cause: each coordinator data refresh reads back the
charging_cutoff_capacityregister and updatescoordinator.max_socfrom it — so once the register was set to 100 %,max_socbecame 100 % and the restore code read that same 100 % value. Fixed by saving each coordinator's originalmax_socinto_weekly_charge_saved_max_socimmediately before writing the 100 % register, and using that saved value (with a fallback tocoordinator.max_soc) in both the mid-charge abort path and the normal completion restore path. - Weekly full charge day change via options flow had no immediate effect: Changing the weekly full charge day (or enabling/disabling it) in the options flow updated the stored config but not the in-memory controller variables (
weekly_full_charge_day,weekly_full_charge_enabled), because those were only read at startup. The hot-reload listener (update_pd_parameters) was missing these fields. As a result,_is_weekly_full_charge_active()kept checking against the old day and never returned True until HA restarted — meaning charge hysteresis was not overridden and batteries would not start charging. Fixed by addingweekly_full_charge_enabledandweekly_full_charge_dayto the hot-reload path. When the day changes, the completion flags are also reset so the new day starts fresh. - Weekly full charge not stopped when day changed mid-charge: If the weekly full charge was already running (hardware cutoff register written to 100 % for v2 batteries) and the user changed the charge day (or disabled the feature) via the options flow, the hardware register was never restored. The battery would continue charging toward 100 % via hardware even though the software no longer considered a weekly charge active. Fixed by setting a
_weekly_charge_needs_restoreflag when a mid-charge abort is detected (registers written, charge not yet complete, day changed or feature disabled). On the next controller cycle_handle_weekly_full_charge_registers()detects the flag, restores the cutoff register tomax_socon v2 batteries, and clears the flag. Additionally,_save_weekly_charge_state()is now called immediately after the registers are set to 100 %, so that an HA restart mid-charge still exposesregisters_written = Truefrom storage, allowing the abort logic to trigger correctly when the day is subsequently changed. - Charge hysteresis threshold still incorrect after weekly full charge (regression from 1.6.1 fix): The 1.6.1 fix introduced
_hysteresis_base_socto track the actual SOC that triggered hysteresis, but the weekly-charge completion handler re-enabled hysteresis (_hysteresis_active = True) without setting_hysteresis_base_soc. If the battery SOC had already dropped belowmax_socby the time the next controller cycle ran, the normal capture logic (if current_soc >= max_soc) did not fire, leaving_hysteresis_base_soc = Noneand falling back tomax_socas the base — reproducing the original bug (threshold = 70 % instead of 90 %). Fixed by setting_hysteresis_base_socto the current SOC (~100 % after a full charge) at the same point the completion handler re-enables hysteresis. - Clearing optional entity selector fields in options flow had no effect: Clicking the × button to remove a configured sensor (e.g. Daily average price sensor, Solar forecast sensor, Household consumption sensor) and saving appeared to clear the field in the UI, but the old value was silently restored. Root cause: HA validates
user_inputagainstdata_schemausing voluptuous, andvol.Optional(key, default=current_value)refills any absent key with the default — so a cleared entity selector (which sends the key as absent) was treated as "unchanged". Fixed by replacingdefault=withdescription={"suggested_value": ...}for all clearable entity selector fields in the options flow. The UI still pre-fills with the existing value, but clearing it now correctly persistsNoneto the config.
[1.6.5] - 2026-04-15¶
Fixed¶
- Predictive charging slot blocked normal battery discharge: When the predictive grid charging time slot overlapped with the normal discharge window, the integration sent conflicting commands to batteries every cycle — first idle (0 W) from the predictive handler, then discharge from the PD controller. Batteries could not ramp up, triggering the non-responsive detection after 3 consecutive failures and excluding them from the pool for 5 minutes. This affected three scenarios: (1) the 5-minute entry wait before evaluation, (2) the user override, and (3) after all batteries reached max_soc. Fixed by never sending idle commands from the predictive charging handler when
grid_charging_activeis False — PD control handles normal operation instead. When charging completes (max_soc reached) or the user overrides,grid_charging_activeis deactivated and PD re-initializes cleanly on the same cycle with no idle gap. The same fix was applied to the dynamic pricing override path.
[1.6.4] - 2026-04-14¶
Added¶
-
Integration Status sensor: New sensor (
sensor.marstek_venus_system_integration_status) showing at a glance what the integration is currently doing. It reflects the highest-priority active mode and updates every poll cycle. Possible states:Charging from Grid(predictive grid charging active),Weekly Full Charge(charging to 100 %),Charge Delayed,Waiting for Solar,Charging to Setpoint,Capacity Protection,No-Discharge Window(inside a configured time slot),Charging,Discharging,Standby(within deadband, no action needed),Manual Mode, andInitializing. Fully translated into EN, ES, DE, FR, and NL. -
Dynamic pricing — evening re-evaluation: A new late-day check activates once per day when solar production is winding down (1.5 h before the estimated T_end, or at 16:00 on days with no detected solar start). If the batteries have not reached their target SOC and the remaining solar is insufficient to cover the gap, the integration searches for cheap price slots between now and midnight and schedules them for grid charging. New slots are merged into the existing morning schedule if one exists, or a new schedule is created. A dedicated persistent notification ("Predictive Charging: Evening re-evaluation") is sent listing the slots added and the estimated deficit. This catches the common scenario where the morning forecast was optimistic (more clouds or more household consumption than expected) and the batteries end up under-charged by end of day.
-
Solar production accumulator: When the household consumption sensor is configured, the integration now integrates real-time solar production throughout the day (
Solar_W = House_W + Battery_Net_W − Grid_W). The accumulated value (solar_production_today_kwh) is exposed as a diagnostic attribute onbinary_sensor.marstek_venus_system_predictive_charging_activeand is used by the charge delay logic to compute remaining solar more accurately: instead of the sinusoidal fraction-of-day estimate, it subtracts actual production from the forecast (remaining = forecast − produced_so_far). The accumulator survives restarts and reloads via the same persistent storage used by the household consumption accumulator. -
Household consumption sensor: New optional power sensor (W or kW) that can be configured at the main sensor step. When set, the integration integrates the sensor's power reading over time to compute daily household energy consumption (kWh) during the solar+battery window (i.e. outside the configured charging time slot). This replaces the previous estimation method — which derived consumption from battery discharge + grid import at min SOC — with real measured data. The improvement is most visible in weeks with high solar production, where the old method systematically underestimated demand and could skip necessary grid charging.
-
Predictive charging and charge delay automatically use the real consumption data when the sensor is configured. No additional setup is needed — the switch between sources is transparent.
- New diagnostic attributes on
binary_sensor.marstek_venus_system_predictive_charging_active:consumption_source(household_sensororbattery_discharge),household_consumption_sensor(entity ID),household_consumption_battery_window_kwh, andhousehold_accumulator_date. - The accumulator survives HA restarts and reloads via persistent storage (previously used binary sensor attributes, which were lost on config entry reload).
- Backfill on startup queries the recorder for the configured sensor's history to fill in consumption data for past days, using the same time-window filter.
- Documentation updated with a step-by-step guide on how to create the household consumption sensor as a Template helper (combining grid, solar, and battery power readings).
Fixed¶
- Dynamic pricing notification showed wrong "needed" value: When no grid charging was required, the "Available ≥ X kWh needed" line always displayed the same value on both sides due to a broken formula (
abs(deficit) + consumptionalgebraically equalstotal_availablewhen no deficit exists). Fixed to show the actual average consumption. A second notification path (no price slots found) also omitted the numeric consumption value entirely ("≥ needed"); fixed to include it.
[1.6.3] - 2026-04-12¶
Fixed¶
- Peak shaving ignored excluded devices with "included in consumption": When a special device was configured with "consumption is included in the household consumption sensor", its power was subtracted from the grid reading before peak shaving evaluated the house load. This meant peak shaving never saw the device's consumption and would not discharge the battery to shave peaks caused by it. Peak shaving now calculates the estimated house load using the real grid reading (including excluded devices) and overrides the device exclusion when actively shaving or conserving, so the battery discharges to keep total grid import below the configured limit regardless of device exclusion settings.
- Max price threshold "expected str" error on reconfigure: Reopening the predictive charging config (dynamic pricing or real-time price) after a threshold was already saved failed with "expected str". The stored value was a
float, but theTextSelectorschema expected astrdefault. Fixed by converting the stored value tostr()before passing it as the schema default.
[1.6.2] - 2026-04-10¶
Fixed¶
- Max price threshold rejected small decimal values: The
NumberSelectorinput for the max price threshold silently rounded values like0.0008to0due to browser-level precision loss in the HTML number input. Replaced with aTextSelector(free-text field) that preserves the exact value entered. Applies to both dynamic pricing and real-time price modes in the setup and options flows. Existing installations that stored a rounded value will need to re-enter the correct threshold in the options flow. Note: HA's attribute display in entity cards may round the value visually (e.g.0.008shown as0.01), but the full-precision value is stored correctly and can be verified in Developer Tools → States. - Dynamic pricing config flow did not advance: The config flow for dynamic pricing mode failed to proceed past the pricing step because the
NumberSelectorcould not handle comma-separated decimals used in European locales. TheTextSelectorreplacement accepts both comma and dot as decimal separators.
[1.6.1] - 2026-04-10¶
Changed¶
- Peak shaving renamed from "Capacity Protection": The peak shaving feature has been renamed across all UI strings and translations (EN, ES, DE, FR, NL) to better reflect its actual function — limiting grid import peaks by discharging the battery only when consumption exceeds a configured threshold. The previous name "Capacity Protection" was misleading, suggesting the feature protects the battery's physical capacity. Internal configuration keys are unchanged, so existing installations are not affected.
Fixed¶
- Charge hysteresis threshold incorrect after weekly full charge: After a weekly full charge day completed (battery reaching 100 %), the hysteresis logic switched back to the configured max SOC (e.g. 80 %) as its reference instead of the 100 % target actually reached. This caused the re-charge threshold to be set at 70 % (80 % − 10 % hysteresis) rather than the expected 90 % (100 % − 10 %), blocking the battery from recharging even when well below the intended threshold. Fixed by tracking the SOC level that activated the hysteresis (
_hysteresis_base_soc) and using it — rather thanmax_soc— as the base for the threshold calculation. The tracked value is cleared when the hysteresis deactivates, so normal charging days are unaffected. - Excessive state updates in HA database (#96): Removed
force_update: Truefrombattery_powerandac_powersensor definitions across all battery versions (v2, v3, vA, vD). With this flag enabled, HA recorded a new database entry on every poll cycle (~every 2 seconds) even when the value hadn't changed, generating ~43,000 state entries per sensor per day and causing significant database growth. Now HA only records a state change when the value actually differs from the previous one. - Aggregate sensors double-triggering state writes: Aggregate sensors (multi-battery setups) had both
should_poll = Trueand coordinator listener callbacks, causing redundant state writes on every poll cycle. Changed toshould_poll = Falsesince the listener callbacks are sufficient.
[1.6.0] - 2026-04-07¶
[!IMPORTANT] Breaking change — solar forecast sensor: The integration now uses today's solar forecast instead of tomorrow's. If you have the Solar Charge Delay or Predictive Grid Charging features configured, you must update the forecast sensor field to point to your integration's today sensor (e.g.
sensor.solcast_pv_forecast_forecast_today) instead of the tomorrow sensor. The stored overnight forecast is no longer used and will be ignored on upgrade.
Added¶
- Solar charge delay SOC setpoint: New optional feature that splits the morning charge into two phases. A dedicated checkbox enables it; when enabled, a slider (12–90 %, default 50 %) sets the target SOC. Below the setpoint the battery charges freely without any delay applied; once all batteries reach the setpoint the solar delay logic activates as usual. This guarantees a minimum charge level on deeply discharged batteries before the solar energy decision is made, while still maximising self-consumption for the remaining charge. The minimum is 12 % — the Venus battery minimum discharge SOC. Configurable during setup and from the options flow; the setpoint value is also exposed as a number entity (Charge Delay SOC Setpoint) on the system device card for runtime adjustment.
Changed¶
- Solar forecast now uses today's live value: The integration no longer captures and stores tomorrow's forecast at 23:00. Instead, it reads the configured sensor live on every evaluation. Today's forecast is updated multiple times throughout the day by most solar forecast integrations (Solcast, Forecast.Solar, etc.), becoming progressively more accurate as actual weather conditions develop. This removes the 23:00 nightly capture task and eliminates the stale-forecast problem that occurred after an HA restart or when the nightly window was missed.
- Charge delay threshold is now a dynamic energy balance: The previous fixed threshold (forecast < 1.5 × battery capacity → unlock) has been replaced with the same energy-balance calculation used by predictive charging:
(usable_energy + forecast) < avg_daily_consumption. This takes the current battery SOC into account — on a partly-charged morning the threshold is effectively lower — and uses real historical consumption data instead of an arbitrary capacity multiplier. The balance is only recalculated when the forecast value changes by more than 0.05 kWh, avoiding unnecessary computation on every controller cycle. - Charge delay re-evaluates forecast changes in real time: Because the forecast sensor is now read live, any intra-day update is automatically picked up. If the forecast degrades to the point where the energy balance turns negative (grid charging needed), the delay unlocks immediately and morning charging starts. If the forecast improves while the delay is still active, the system keeps waiting for solar. Once the delay is unlocked, it stays unlocked for the day.
- Predictive charging (Time Slot mode) — initial evaluation delayed 5 minutes: The energy balance evaluation no longer runs at the exact instant the slot boundary is crossed. Instead the controller waits 5 minutes with the battery in idle before evaluating. This avoids a race condition when the slot starts at 00:00: at that moment the forecast sensor may still be reporting yesterday's final value before the integration resets it for the new day. The 5-minute hold ensures the sensor has updated before the charging decision is made.
- Predictive charging (Time Slot mode) — pre-evaluation notification removed: The notification sent 1 hour before the slot start has been removed. A single notification is now sent at the moment the evaluation fires (5 minutes into the slot), reporting the actual decision: charge or no charge needed.
[1.5.4] - 2026-04-06¶
Added¶
- Configurable backup offgrid load threshold: Each battery now has a user-configurable threshold (0–500 W, default 50 W) that determines when an offgrid load is treated as an active backup event. Batteries with small permanent loads on the offgrid port (e.g. a PoE switch, router, or cameras) were previously excluded from PD control indefinitely because any non-zero offgrid reading triggered backup mode. The threshold can be set during initial setup (per-battery limits page in the config flow) and adjusted at any time via a Backup Offgrid Threshold number entity on the battery device card — no reconfiguration required. Changes take effect immediately and survive restarts.
- EV charger without power telemetry (new excluded-device option): A new checkbox — EV charger without power telemetry — is available when configuring an excluded device. When checked, the selected sensor is treated as a state sensor rather than a power sensor. The controller monitors it and reacts when the state changes to any recognised charging string (
Charging,Cargando,Cargando VE,Cargando Vehículo, and case-insensitive variants). On detection, the battery enters a 5-minute full pause (both charge and discharge set to 0 W, PD state frozen) so the vehicle receives the maximum available current without competition. Once the pause expires the battery may still charge from solar surplus but will never discharge while the EV remains in a charging state. Normal operation resumes automatically when the sensor leaves the charging state.
Fixed¶
Active Batteriessensor always showingIdlewhen only one battery is available:_select_batteries_for_operationreturned early for the single-battery case without updating the_active_charge_batteries/_active_discharge_batteriestracking lists, so the diagnostic sensor always saw empty lists and displayedIdleregardless of actual charge/discharge state.- Backup exclusion logic not applied consistently during shutdown: The
async_unload_entryshutdown handler used a hardcoded!= 0check for the offgrid power sensor instead of the per-battery threshold, meaning batteries with small permanent offgrid loads would incorrectly skip shutdown register writes and be left in an uncontrolled state on integration unload.
Thanks to @hdcasey for reporting and fixing the first two issues.
[1.5.3] - 2026-04-05¶
Added¶
- Proactive battery alarm notifications (v2 only): The integration now monitors the
Alarm Status(register 36000) andFault Status(register 36100) registers every 5 seconds and sends a Home Assistant persistent notification the moment a new alarm or fault bit is set. The notification is titled "Battery Fault/Warning: \<name>" and lists both the newly triggered conditions and all currently active ones. When all alarms and faults clear, the notification is automatically dismissed. Notifications are scoped per battery (separate notification ID per battery name) so multi-battery setups report each device independently. v3, vA and vD batteries do not expose these registers via Modbus and are not affected. - System Alarm Status sensor (
sensor.marstek_venus_system_alarm_status, v2 only): New sensor on the Marstek Venus System device that aggregates the alarm state across all batteries. State isOKwhen no conditions are active,Warningwhen one or more alarm bits are set but no fault bits, andFaultwhen at least one fault bit is active on any battery. Theextra_state_attributesdictionary exposes per-battery detail — each key is the battery name and the value is a list of active condition labels (e.g.[Fault] BAT Overvoltage,[Alarm] Fan Abnormal Warning) — so the exact source and nature of each event is visible without opening individual battery sensors. The sensor is only populated for v2 batteries. - Shared alarm/fault bit-description constants (
FAULT_BIT_DESCRIPTIONS,ALARM_BIT_DESCRIPTIONSinconst.py): The 32-bit description maps previously duplicated inline insideSENSOR_DEFINITIONSare now standalone module-level dicts. The existing per-batteryAlarm StatusandFault Statussensors reference these constants, eliminating the duplication and making future updates to alarm labels a single-point change. - Solar Surplus switch per excluded device: Each excluded device now gets a dedicated switch entity (
Solar Surplus – <device name>) that toggles theallow_solar_surplusflag at runtime without entering the options flow. When ON, the battery yields solar surplus to that device (e.g. EV charger) instead of charging itself — solar goes to the device first and the battery will not discharge to power it either. When OFF, the battery charges normally with any available solar surplus. The switch is controllable from HA automations, enabling priority changes based on schedules, battery SOC, EV connection state, or any other condition.
Changed¶
- strings.json now in English: The base translation file (
strings.json) has been converted to English to serve as the proper fallback language for Home Assistant installations using a language without a dedicated translation file.
Fixed¶
- German translation fix: Corrected
round_trip_efficiency_totallabel from "Gesamte Hin- und Rückfahreffizienz" to "Gesamtwirkungsgrad".
[1.5.2] - 2026-04-04¶
Added¶
- Configuration Summary diagnostic sensor: New hidden sensor (
Configuration Summary) on the Marstek Venus System device that exposes the complete integration configuration as entity attributes. Intended for support purposes — enable it from the entity registry, then share the state card to provide a full picture of the system setup at a glance. Attributes are organised in sections: general (grid sensor, meter inversion, solar forecast sensor), per-battery settings (name, IP, version, power limits, SOC thresholds, hysteresis), time slots, predictive charging (mode, time slot, contracted power, price sensor and thresholds), weekly full charge, charge delay, capacity protection, PD controller parameters, and excluded devices. The sensor is disabled by default and categorised as diagnostic. - Backup function exclusion from PD control: When the Backup Function switch is enabled on a battery and the AC Offgrid Power sensor reports a non-zero value, that battery is automatically excluded from PD controller writes. Having the switch on alone is not sufficient — the battery must actually be providing offgrid power. No power commands, force mode changes, or configuration register writes are sent while both conditions are met. A 5-minute cooldown is applied after the offgrid load drops back to 0 W, keeping the battery excluded until the window expires to avoid sending commands immediately after a backup event ends. Turning the switch off clears the cooldown immediately. The battery continues to be polled normally so all read-only sensors remain up to date. This applies to all write paths: normal PD control, predictive grid charging, weekly full charge register writes, and the shutdown sequence. The AC Offgrid Power sensor (register 32302) has been added to v3, vA, and vD battery definitions so that the two-condition check works across all versions.
[1.5.1] - 2026-04-01¶
Added¶
- Price-based discharge control for Dynamic Pricing and Real-Time Price modes: New optional checkbox in both pricing mode configurations that restricts battery discharge to periods when the current electricity price exceeds a configurable threshold. When enabled, the battery only discharges if the live price is strictly above the threshold (fixed
max_price_thresholdor a daily average price sensor). If discharge time slots are also configured, both conditions must be met — the price check acts as an additional gate on top of the existing time window restriction. Dynamic Pricing mode gains a new optionaldp_average_price_sensorfield (equivalent to the existingaverage_price_sensorin Real-Time Price mode) to support a dynamic discharge threshold. - Grid meter kW auto-detection and inverted sign support: The controller now automatically detects if the grid meter sensor reports in kW (via its
unit_of_measurementattribute) and converts to Watts internally — no user action required. A new "Inverted meter sign" toggle has been added to the initial setup and options flow for meters that use the opposite sign convention (positive = export, negative = import).
[1.5.0] - 2026-04-01¶
Added¶
- New sensors for all battery versions (v2/v3/vA/vD): Added device information sensors (
device_name,sn_code,software_version,bms_version,vms_version,ems_version,comm_module_firmware,mac_address) where supported by each battery version. Added cell voltage sensors (max_cell_voltage,min_cell_voltage) for v3/vA/vD. Added WiFi and Cloud connectivity binary sensors (wifi_status,cloud_status) for all versions. - Battery Cycle Count sensor (v3/vA/vD): Direct register-based cycle count sensor reading from the battery firmware for v3, vA, and vD batteries.
- Calculated Battery Cycle Count sensor (all versions): New derived sensor (
battery_cycle_count_calc) available for all battery versions, calculated as(total_discharge + total_charge) / 2 / battery_capacity. Provides cycle count estimation for v2 batteries that lack a direct register, and a cross-check for other versions. - Dynamic Pricing Mode for Predictive Grid Charging: New charging mode that automatically selects the cheapest hours of the day to charge the batteries from the grid. Supports Nordpool, PVPC (ESIOS REE, Spain), and CKW (Switzerland). Configured through a new
predictive_charging_modestep (choose between Time Slot or Dynamic Pricing) and adynamic_pricing_configstep (price integration type, price sensor, optional max price threshold, and ICP contracted power). - Automatic cheapest-hour selection: The controller calculates the energy deficit each day at 00:05 using the effective charge power (
min(ICP, total battery charge capacity)), then picks the cheapest hours from the price forecast. When no deficit exists, the cheapest equivalent hours are still selected as informational reference. - Max price threshold filter: Optional ceiling that prevents charging even during cheap hours if prices exceed the configured limit. Unit matches the sensor (€/kWh for Nordpool/PVPC, CHF for CKW).
- Daily 00:05 evaluation with retry logic: Evaluation runs just after midnight when price data for the day is already available. If price data is still unavailable at 00:05, the controller retries every 15 min within the first hour of the day. A
no_price_dataerror is surfaced in the config flow if the selected sensor lacks the expected attributes. - Startup evaluation on integration restart: If Home Assistant restarts after the 00:05 window and no evaluation has been done yet for the current day, the controller runs a one-time evaluation automatically during startup (after a 15-second delay to allow the data coordinator to complete its first poll). This ensures the daily schedule is always built, even when HA is restarted mid-morning. The evaluation only considers slots up to 23:59 of the current day — tomorrow's slots are left to the normal 00:05 evaluation.
price_data_statusdiagnostic attribute: Thepredictive_charging_activebinary sensor exposes aprice_data_statusattribute showing whether the price sensor is being read correctly:ok (N slots),sensor_unavailable,no_slots, ornot_evaluated.predictive_charging_activebinary sensor — dynamic pricing attributes: Always exposes the full evaluation result —charging_neededflag, selected hours with individual prices, average price, estimated cost, and evaluation timestamp. The schedule persists throughout the day it applies to (not cleared at midnight).- Real-Time Price mode for predictive grid charging: New third charging mode that reads the current electricity price every controller cycle (~2.5 s) and activates or deactivates grid charging immediately when the price crosses a configured threshold. Unlike Dynamic Pricing (which pre-selects the cheapest hours at 00:05), this mode requires no overnight evaluation and no price forecast — it reacts purely to the live price. Supports any HA sensor that exposes the current period price as its state (PVPC, Nordpool, CKW, or any other integration). Optionally accepts a daily average price sensor as a dynamic threshold instead of a fixed value, and evaluates the same solar/battery energy balance as other modes before starting to charge.
- Solar forecast sensor optional in predictive charging: The solar forecast sensor field in both Time Slot and Dynamic Pricing configuration steps is now optional. Users without solar panels can leave it empty — the system will activate grid charging whenever the battery's usable energy is insufficient to cover expected daily consumption (same conservative logic already used when the sensor is unavailable or reports an error). Users with solar panels should still configure it so the system only charges when the forecast is insufficient.
- Improved daily consumption estimate when battery reaches min SOC: The system now tracks grid energy imported during periods when all batteries are at minimum SOC and the battery would otherwise be discharging (within a configured discharge slot, or always if no slots are defined). This unmet demand is accumulated in a new sensor (
Grid at Min SOC, kWh, resets at midnight) and added to the battery discharge when capturing the daily consumption figure used by predictive charging. This prevents the 7-day rolling average from underestimating consumption on days where the battery ran out before midnight, which previously caused the system to charge less than needed the following day. Grid import during intentional grid charging (predictive/dynamic pricing) is excluded from the accumulator. - Mid-day re-evaluation for dynamic pricing slots: When multiple cheap slots are selected at 00:05, the system now re-evaluates 1 hour before each subsequent slot whether charging is still needed. If the battery is sufficiently charged (solar + current SOC covers expected consumption), the slot is silently skipped. If charging is still needed, a notification is sent confirming the slot will activate. Re-evaluations are skipped automatically when a previous slot is still actively charging (back-to-back slots), and the per-day state is reset at midnight alongside the main schedule.
Fixed¶
- SOC and power limit sliders not persisted across restarts (complete fix): Changes to
Min SOC,Max SOC,Max Charge Power, andMax Discharge Powersliders were written to the battery hardware registers and updated in memory, butconfig_entry.datawas never updated. On every HA restart,async_setup_entryoverwrote the hardware registers with the original setup values, discarding any runtime changes. The partial fix in 1.4.0 (syncing coordinator attributes from polled register values) was ineffective because the startup sequence writes the stale config values to hardware before the first poll completes. Values are now persisted toconfig_entry.dataimmediately when a slider is changed, using the same pattern as the RS485 switch preference. - RS485 switch state not persisted across restarts: Disabling the RS485 Control Mode switch and restarting Home Assistant would re-enable it automatically. The user's preference is now saved to the config entry and restored on startup. The initial RS485 enable during integration load and the reconnection re-enable are both skipped when the user has explicitly disabled the switch.
- v3/vA/vD batteries not accepting write commands: Inter-register write delay in the atomic power write sequence was hardcoded to 50 ms (v2 timing). v3/vA/vD firmware requires a minimum of 150 ms between consecutive Modbus messages; writes now use the version-specific timing from
MESSAGE_WAIT_MS, matching the delay already applied to polling reads. - Non-responsive battery cooldown capped at 30 min: The exponential backoff for excluded batteries could grow to 30 minutes after repeated failures. The cap is now 5 minutes so a temporarily unresponsive battery is retried more frequently.
- Dynamic pricing schedule cleared at midnight: The
_dynamic_pricing_evaluated_datewas stored as the evaluation date (day N), causing the schedule to be wiped at midnight — before any of the selected slots (day N+1 morning) could activate. The evaluated date is now stored as the date the slots belong to, so the schedule persists until the end of that day. - Charging hours underestimated when ICP > battery charge capacity:
_calculate_charging_hours_neededused onlymax_contracted_power(ICP) as the effective charge power. If the total battery charge capacity is lower than the ICP, the actual charge rate is limited by the batteries, not the ICP — resulting in too few hours being selected and the battery not reaching the target SOC. The calculation now usesmin(ICP, total battery charge capacity). Estimated cost in the schedule and notifications is also corrected accordingly. - Consumption history always showing 6 days instead of 7: The cleanup filter used a strict
>comparison (d > today − 7), which excluded the entry for exactly 7 days ago. Replaced date-based cutoff with a trim to the 7 most recent entries ([-7:]) so the window is always exactly 7 entries. - Consumption history gaps filled with real-data average: When the startup backfill cannot find recorder data for a day (HA was down, or daily discharge was below the 1.5 kWh representativeness threshold), the missing entry is now filled with the average of the real entries already in the history instead of the fixed 5.0 kWh default. This prevents artificial inflation of the consumption estimate. On first run when no real data exists yet, 5.0 kWh is still used as a conservative bootstrap.
- Modbus read/write operations now time out on half-open sockets:
async_read_registerandasync_write_registernow wrap their underlying pymodbus calls inasyncio.wait_for(timeout=...)using the client's configured timeout (default 10 s). Previously, a hung write or read on a half-open socket would block indefinitely, preventing the coordinator from recovering after a TCP connection drop.asyncio.TimeoutErroris now treated the same asConnectionException, triggering the existing reconnect-and-retry path. - Battery SOC hidden by efficiency sensor in device card: The Round-Trip Efficiency sensor shared the
batterydevice class with the Battery SOC sensor, causing Home Assistant to display the efficiency percentage instead of the SOC in the top-right corner of the device card. The device class has been removed from the efficiency sensor.
Changed¶
- Predictive charging notifications reformatted: Both time-slot and dynamic pricing notifications now use a consistent emoji-based layout (🔋 battery, ☀️ solar, 📊 consumption, ⚡ deficit, ⏰ timing). All notifications show the effective charge power as
min(ICP, battery capacity)W (ICP: XW, batteries: YW). When dynamic pricing finds no deficit, the notification is clearly labelled as informational ("No charging will activate") and shows the cheapest reference hours without implying grid charging will occur. - Clarifying note on solar forecast sensor in secondary configuration steps: In the Time Slot, Dynamic Pricing, and Charge Delay configuration steps, the solar forecast sensor field now includes a note explaining that it is not required if the sensor was already configured in the initial setup step — it will be used automatically. The German, French, and Dutch translations of the Dynamic Pricing step were also missing this field entirely; it has been added.
Removed¶
- Unused
too_lowtranslation error key: Removed the orphanedtoo_lowerror string (previously defined in all translation files but never raised by the Python code) from EN, ES, DE, FR, and NL translations. user_work_moderemoved from vA/vD batteries: Theuser_work_modeselect entity (register 43000) is not supported on vA and vD hardware and has been removed from their definitions.
[1.5.1] - 2026-04-01¶
Added¶
- Grid meter kW auto-detection and inverted sign support: The controller now automatically detects if the grid meter sensor reports in kW (via its
unit_of_measurementattribute) and converts to Watts internally — no user action required. A new "Inverted meter sign" toggle has been added to the initial setup and options flow for meters that use the opposite sign convention (positive = export, negative = import).
[1.4.1] - 2026-03-24¶
Added¶
- Support for up to 6 batteries: The battery count slider in both the initial setup and options flow now allows selecting 1–6 batteries (previously capped at 4). No architectural changes were required as the control loop and power distribution are fully dynamic.
- Non-responsive battery detection: The control loop now detects when a battery acknowledges a discharge command (registers written correctly) but fails to deliver power. After 3 consecutive cycles with actual output below 10% of the commanded value, the battery is excluded from the active pool with a warning log entry. It is automatically retried after a cooldown period that doubles on each repeated failure (5 → 10 → 20 → 30 min cap) and resets to 5 min after a successful delivery cycle. This prevents a single non-responsive battery from destabilising the PD controller and causing the remaining batteries to oscillate.
- Non-Responsive Batteries diagnostic sensor: New sensor on the Marstek Venus System device showing which batteries are currently excluded due to non-responsive behaviour. State is
Nonewhen all batteries are healthy, or a comma-separated list of excluded battery names. Attributes expose per-battery details: exclusion status, cooldown duration, and remaining cooldown minutes. Available in EN, ES, DE, FR, NL.
[1.4.0] - 2026-03-21¶
Added¶
- Unified Solar Charge Delay: New dedicated config step (
Charge Delay) that replaces the previous weekly-charge-specific delay flag. When enabled, the delay applies every day: charging is held back while solar production is forecast to cover the required energy, and unlocked automatically once the energy balance tips. The target SOC is 100% on the configured weekly full charge day, or the configuredmax_socon all other days. Discharge in configured time slots is unaffected. - Unified charge delay config step: Two new steps in both ConfigFlow and OptionsFlow — a gate step (
Configure charge delay) and a configuration step with aSafety margin (hours)slider (1–5 h, step 0.5 h, default 1 h) and an optionalSolar forecast sensorfield (only shown if not already configured in the predictive charging step). - Charge Delay Status diagnostic sensor: New
Charge Delay Statussensor on the Marstek Venus System device, replacing the separate weekly and solar delay sensors. State reportsIdle,Waiting for solar,Delayed (~HH:MM est.),Charging allowed, orDisabled. Attributes exposetarget_soc,safety_margin_min,forecast_kwh,solar_t_start,solar_t_end,energy_needed_kwh,remaining_solar_kwh,remaining_consumption_kwh,net_solar_kwh,charge_time_h,estimated_unlock_time, andunlock_reason. Populated on every control cycle, not only when a charge attempt is gated. - Accurate estimated unlock time: The
estimated_unlock_timeattribute is now calculated as the earliest of two triggers — the time-backup threshold (T_end − charge_time − safety_margin) and the energy-balance crossing point, found via a binary search (40-iteration bisection, <1 s precision) on the sinusoidal solar production model. On good solar days, this reflects the energy-balance unlock ~1–2 hours earlier than the conservative time-backup estimate. - Capacity Protection Mode (Peak Shaving): New feature that conserves battery energy when SOC drops below a configurable threshold (30–100%). Instead of discharging to cover all household consumption, the battery only discharges to offset consumption that exceeds a configurable peak limit (2500–8000W). When house load is below the limit, the battery stays idle; when it exceeds the limit, the battery discharges only the excess. Solar charging continues unaffected. Configurable in both initial setup and options flow, with a runtime toggle switch and adjustable number entities for SOC threshold and peak limit.
- Charge Delay switch: New
Charge Delayswitch on the Marstek Venus System device to enable/disable the charge delay feature at runtime without reconfiguring the integration. Only visible when charge delay is configured. - Capacity Protection switch: New
Capacity Protectionswitch on the Marstek Venus System device to enable/disable the feature at runtime without reconfiguring the integration. State persists across restarts. - Capacity Protection Active diagnostic sensor: New
Capacity Protection Activebinary sensor (diagnostic) that turns ON when the protection is actively intervening (SOC below threshold). Attributes expose real-time diagnostic data:avg_soc,soc_threshold,peak_limit_w,estimated_house_load_w,action(shaving/conserving/charging/idle/disabled),original_target_w, andadjusted_target_w. - Capacity Protection number entities:
Capacity Protection SOC ThresholdandPeak Limit Protectionnumber entities on the system device for runtime tuning without reconfiguration. Only visible when the feature is enabled. - Charge Delay Margin number entity: New
Charge Delay Marginslider on the Marstek Venus System device to adjust the safety margin at runtime without reconfiguring the integration. Displayed in hours (1–5 h, step 0.5 h); stored internally in minutes. - Entity name and state translations: All system-level entities (switches, sensors, binary sensors) now use Home Assistant's translation system. Entity names (
Manual Mode,Charge Delay,Discharge Window, etc.) and sensor state values (Idle,Disabled,Charging allowed,Waiting for solar,Delayed,Active,Inactive, etc.) are now displayed in the user's configured HA language. Supported languages: English, Spanish, German, French, Dutch.
Removed¶
- Force Full Charge button removed: The
Force Full Chargebutton has been replaced by the newCharge Delayswitch (see above).
Changed¶
- Solar T_start detection rewritten: The mechanism that detects when solar production begins (used by the Charge Delay feature) has been replaced. The previous approach accumulated daily battery charging energy and triggered at a 0.1 kWh threshold — unreliable because grid charging energy was included in the counter. The new primary mechanism triggers when grid power ≤ 0 W and total battery power ≤ 0 W simultaneously (solar is covering at least the full house load with no battery contribution). A new astronomical fallback kicks in 30 minutes after the estimated sunrise (calculated from HA latitude, longitude, and day of year) if the primary condition has not fired, handling high-consumption days where grid power never reaches zero.
- Solar forecast corrected by 15 % before use: A conservative 15 % reduction is applied internally to the captured solar forecast before it is used by the Charge Delay and Predictive Charging algorithms. The raw sensor value is still shown in the
forecast_kwhdiagnostic attribute; only the internal calculation uses the adjusted value. - Weekly full charge config simplified: The
weekly_full_charge_configstep now contains only the day-of-week selector. The delay toggle, safety margin, and solar forecast sensor have been moved to the new dedicatedCharge Delaysteps, which apply to both the weekly 100% charge and the daily max_soc charge. - Solar forecast captured every night: The forecast capture at 23:00 now runs every night (previously only the night before the weekly charge day). The stored value is used the next morning by the delay logic, ensuring the forecast is always from the previous evening — before the sensor resets to the next day's data at midnight.
- Delay uses stored forecast: The charge delay algorithm no longer reads the solar forecast sensor live. It uses the value captured the previous night, which corresponds to the current day's production. Live reads would return the next day's forecast after midnight.
Fixed¶
- Feature entities disappearing after disabling switch: The
Charge Delaysensor andCapacity Protectionswitch and status sensor were only registered at startup when their respective feature was enabled. Disabling the switch persisted the disabled state to config, so after a restart those entities would no longer appear. Registration now checks whether the feature is configured (key exists in config entry) rather than whether it is currently enabled, matching the pattern already used by theCharge Delayswitch. Affected entities:Charge Delaysensor,Capacity Protectionswitch,Capacity Protection Activebinary sensor. - Charge Delay sensor renamed: The
Charge Delay Statussensor has been renamed toCharge Delayacross all supported languages (EN, ES, DE, FR, NL) for brevity. - Configuration changes not surviving restart: Changes to
Min SOC,Max SOC,Max Charge Power, andMax Discharge Powervia the UI were written to the battery's Modbus registers but not persisted toconfig_entry.data. After a restart, the coordinator re-initialised from the original setup values. The coordinator now syncs these attributes from the polled register values after every data refresh, treating the hardware as the source of truth. Separately, the enabled/disabled state of theCharge DelayandCapacity Protectionswitches was being saved correctly, but the related entities were not registered when the feature was disabled — making the saved state effectively invisible after a restart. This is resolved by the entity registration fix described above. - Solar forecast sensor not shown in initial setup: The solar forecast sensor field was missing from the first configuration step. It is now included as an optional field alongside the consumption sensor, making it available to both Predictive Grid Charging and Charge Delay without requiring re-entry in each feature's step.
- Safety Margin description and defaults incorrect in README: The documented range (10–120 min) and default (40 min) did not match the actual implementation (30–180 min, default 180 min). The description incorrectly stated it was an extra buffer for underperformance; the correct meaning is "minutes before sunset by which charging must be complete — higher values unlock charging earlier".
- V3 Battery SOC register reverted: Register 34002 (scale 0.1, introduced in v1.3.0) is not supported on Venus C v3 batteries and caused incorrect readings. SOC is now read again from register 37005 (scale 1, precision 1 decimal) as in versions prior to 1.3.0.
- Delay evaluation too infrequent: The charge delay status was only computed when the PD controller attempted to charge the battery. On days where the battery stayed in equilibrium (deadband), the
Charge Delay Statussensor remained stale and showedtarget_soc: Unknown. Delay evaluation now runs proactively on every 2-second control cycle regardless of charging activity.
[1.3.4] - 2026-03-17¶
Improved¶
- Stale sensor detection for PD controller: The control loop now detects when the consumption sensor hasn't updated between cycles (common with sensors reporting every 5s or slower) and skips PD recalculation to avoid acting on stale data. Sensor history is only populated with real readings, preventing duplicate values from diluting the moving average. The derivative term now uses the actual elapsed time between sensor updates instead of a fixed 2s, eliminating derivative spikes that caused oscillation with slow sensors. A safety valve forces recalculation (proportional only, derivative suppressed) if the sensor stops updating for ~30 seconds.
[1.3.3] - 2026-03-17¶
Fixed¶
- Hassfest manifest validation: Removed unsupported
iconfield frommanifest.jsonand addedrecordertoafter_dependenciesto declare the integration's usage of the recorder component.
[1.3.2] - 2026-03-17¶
Changed¶
- Min charge/discharge power slider range increased: The maximum value for
Min Charge PowerandMin Discharge Powerin both the PD Advanced options flow and the number entities has been raised from 500W to 2000W, allowing higher idle thresholds for systems with large PV Systems.
[1.3.1] - 2026-03-12¶
Fixed¶
- System SOC decimal precision for v3 batteries: The
System SOCaggregate sensor now displays one decimal place when any battery in the system is a v3/vA/vD model, matching the higher-resolution SOC readings provided by those batteries.
[1.3.0] - 2026-03-12¶
Added¶
- Venus A and Venus D battery support: New battery models
A(Venus A, max 1200W) andD(Venus D, max 2200W) for hybrid inverter setups. Both models share the same Modbus register map and include MPPT power sensors (mppt1–mppt4, enabled by default) for monitoring solar input channels. - Dynamic power slider limits in config flow: The battery configuration wizard now adapts the charge/discharge power sliders to the selected model's maximum (Ev2/Ev3: 2500W, A: 1200W, D: 2200W). The battery setup step has been split into two screens: connection details (name, IP, port, model) and power limits.
- Battery model version labels updated: Version labels in the configuration flow now read
Ev2,Ev3,A, andDfor clarity. - Weekly Full Charge Delay (Solar-Aware): New optional feature that delays the weekly 100% charge until solar production is forecast to be insufficient. Instead of charging to 100% from midnight, the system evaluates the solar forecast and only unlocks the full charge when remaining solar energy won't cover household consumption plus the energy needed to reach 100%. Uses a sinusoidal solar production model with T_start detection from actual battery charging data and solar noon calculated from Home Assistant's configured longitude. Includes configurable safety margins and automatic fallback for days with no forecast data.
- Solar forecast capture for delay feature: When the delay feature is enabled, the integration captures the next-day solar forecast at 23:00 and persists it across restarts using HA Store, ensuring the forecast is available on the target day.
- Weekly Full Charge diagnostic sensor: New
Weekly Full Chargediagnostic sensor on the Marstek Venus System device showing the current charge status (Idle,Waiting for solar,Delayed (HH:MM est.),Charging to 100%,Complete). Attributes expose full calculation details: forecast kWh, solar T_start/T_end, energy needed, remaining solar/consumption, net solar, charge time estimate, estimated unlock time, and unlock reason. - Force Full Charge button: New button on the Marstek Venus System device to trigger an immediate 100% charge on any day, bypassing the weekly schedule and delay logic. Resets automatically on day change.
- Configurable safety margin for delay feature: The delay safety margin (time buffer before estimated end of solar production) is now configurable in both config and options flow (10-120 minutes, default 40 min). Previously hardcoded at 40 minutes.
Changed¶
- System Charge/Discharge Power uses AC power: The
System Charge PowerandSystem Discharge Poweraggregate sensors now read from each battery'sAC Powerregister instead ofBattery Power, reflecting the actual AC-side power flow. - V3 Battery SOC register upgraded: V3 batteries now read SOC from register 34002 (scale 0.1, precision 2 decimals) instead of 37005 (scale 1, precision 1 decimal), providing higher resolution readings.
- Removed unused Charge to SOC entity: The
charge_to_socnumber entity (register 42011) was not used by any integration logic and has been removed from both V2 and V3 definitions. - Translation files completed: Added missing
apply_to_chargefield translations to EN, DE, FR and NL. Added missingenable_weekly_full_charge_delay,solar_forecast_sensoranddelay_safety_margin_mintranslations to DE, FR and NL (both config and options flow).
Fixed¶
- RS485 control mode not re-enabled after reconnection: When a battery's TCP connection was lost and re-established (e.g., WiFi drop, options flow reload), RS485 control mode was not re-enabled. The battery silently ignored all power commands until a manual restart.
async_reconnect_fresh()now automatically re-enables RS485 after every successful reconnection, with a user-override flag to respect manual RS485 disabling via the switch entity. - First battery RS485 disabled after options flow reload: On reload, the first battery attempted to reconnect before the V3 firmware released the previous TCP slot, causing the initial connection to fail. RS485 was only enabled on successful connection, leaving the first battery uncontrolled until health monitoring reconnected (without re-enabling RS485). Added a 1-second retry delay for failed initial connections.
- Individual battery Stored Energy sensor not visible: The
MarstekVenusStoredEnergySensorentities were created but immediately discarded due to alambda entities: Nonecallback. Sensors are now properly registered through the sensor platform setup. - Consumption history not populated without predictive charging: The daily consumption capture (needed for the delay feature's average calculation) was only scheduled when predictive charging was enabled. Now also scheduled when the weekly full charge delay is enabled.
- Grid charging falsely triggering solar T_start detection:
total_daily_charging_energyincludes grid charging energy, which could falsely indicate solar production start. T_start detection now only activates after 07:00 to avoid overnight grid charging interference.
[1.2.1] - 2026-03-06¶
Fixed¶
- Max charge/discharge power changes from UI ignored by control loop: Changing
Max Charge PowerorMax Discharge Powernumber entities wrote to the Modbus register but did not update the coordinator attributes used by the PD controller. The initial config flow values were used forever. Changes now take effect immediately.
[1.2.0] - 2026-03-04¶
Added¶
- Solar surplus mode for excluded devices: New
allow_solar_surplusoption in excluded device configuration. When the battery is charging, no adjustment is applied — the PD controller sees real grid power and naturally reduces charging to leave solar for the device. When the battery is discharging, full exclusion applies so the battery won't drain to power the device. Recommended for high-consumption devices like EV chargers. - Native config entities: Exposed key configuration parameters as Home Assistant entities, eliminating the need to run the full Options Flow wizard for routine adjustments:
- PD controller number entities: Kp, Kd, deadband, max power change, direction hysteresis, min charge/discharge power — all hot-reloadable without integration restart.
- Max Contracted Power number entity: Editable from the UI when predictive charging is enabled.
- Weekly Full Charge Day select entity: Pick the balancing day directly from the UI.
- Time Slot switches: Enable/disable individual no-discharge time slots on the fly.
- Excluded Devices Config sensor: Read-only diagnostic showing the number of excluded devices, with per-device details (sensor entity, included_in_consumption, allow_solar_surplus) as attributes.
- Discharge Window diagnostic sensor: Real-time sensor showing whether the system is currently inside an allowed discharge time slot. Displays "Active (Slot N)", "Inactive", or "No slots". Attributes include all slot configuration details (schedule, days, enabled, apply_to_charge, target_grid_power). Replaces the per-slot Time Slot Info sensors.
- Battery load sharing: Intelligent battery selection that uses the minimum number of batteries needed to keep each one operating in its optimal efficiency zone. Based on the Venus efficiency curve, batteries activate when total power exceeds 60% of combined capacity (peak efficiency ~91% at 1000-1500W). Features:
- Discharge priority: Highest SOC first (drain fullest battery).
- Charge priority: Lowest SOC first (fill emptiest battery).
- SOC hysteresis (5%): Active battery stays selected until another exceeds it by 5% SOC.
- Energy hysteresis (2.5 kWh): Tiebreaker uses lifetime energy with 2.5 kWh advantage for active battery, balancing long-term wear.
- Power hysteresis (±100W): Activates 2nd battery at 60% capacity threshold, deactivates at 50% to prevent ping-pong with fluctuating loads.
- Applies to all modes: normal PD control, solar charging, and predictive grid charging.
- Active Batteries diagnostic sensor: Real-time sensor showing which batteries are currently active in load sharing. Displays "Discharging: Venus 1", "Charging: Venus 2", or "Idle". Attributes include per-battery SOC, lifetime discharged/charged energy, and active battery counts. Only created for multi-battery setups.
Improved¶
- Modbus TCP connection management: Overhauled the Modbus connection lifecycle to prevent permanent battery disconnection (especially on V3, which only accepts one TCP connection). Reconnection now creates a fresh pymodbus client instance every time — closing the old socket first (sending TCP FIN to release the battery's connection slot), then connecting with
reconnect_delay=0to disable pymodbus's internal auto-reconnect which grew exponentially up to 300 seconds. Added coordinator-level connection health monitoring: after 3 consecutive failed poll cycles a fresh reconnection is triggered; after 5 failures, polling is suspended for 2 minutes to avoid flooding unreachable batteries. Normal 1.5s polling resumes automatically on recovery. The PD control loop now skips unreachable batteries viacoordinator.is_availableinstead of writing to dead connections. - Automatic reconnection in Modbus retry loops: When a
ConnectionExceptionorModbusIOExceptionoccurs during a read or write operation, the client now immediately attempts to create a fresh TCP connection instead of retrying on the dead socket. If reconnection succeeds, the operation is retried once; if it fails, retries are aborted immediately. This dramatically reduces recovery time after WiFi drops — the integration reconnects on the first failed operation instead of waiting for 3 poll cycles. - Immediate unavailability detection: The coordinator now marks a battery as unavailable (
_is_connected = False) on the first poll cycle where all reads fail, instead of waiting for 5 consecutive failures. The control loop stops sending writes to unreachable batteries immediately, preventing log noise and wasted Modbus operations. - Connection error log reduction:
ConnectionExceptionandModbusIOExceptionerrors during read/write operations are now logged at DEBUG level instead of ERROR with full traceback. "Failed after N attempts" messages also downgraded to DEBUG. This eliminates the 60,000+ error log entries that occurred during a WiFi disconnection event.
Changed¶
- Minimum charge/discharge power moved to PD controller settings:
min_charge_powerandmin_discharge_powerare now global PD controller parameters instead of per-time-slot settings. They apply uniformly regardless of the active time slot. Existing installations will use the default (0 = disabled) until reconfigured via Options → PD Advanced. - Predictive Charging switch logic inverted: The switch is now ON when predictive charging is enabled (default) and OFF when overridden/paused. Previously, it was an "Override" switch with inverted semantics. New unique_id (
_predictive_charging) — the old_override_predictive_chargingentity should be manually deleted from HA. - Entity reorganization: Restructured the Marstek Venus System device page for clearer HA UI layout:
- Controls: Manual Mode, Predictive Charging (inverted logic), Time Slot switches, Weekly Full Charge Day select — all without
EntityCategoryso they appear in the Controls section. - Diagnostic: Discharge Window sensor (new), Predictive Charging Active binary sensor.
- Configuration: PD controller parameters only (Kp, Kd, deadband, etc.).
- Removed per-slot Time Slot Info sensors and Predictive Charging Config sensor: Replaced by the single Discharge Window diagnostic sensor. Old entities (
*_time_slot_N_info,*_config_predictive_charging_slot) should be manually deleted from HA. - Max charge/discharge power config flow selector: Replaced the dropdown with only two options (800W / 2500W) with a slider ranging from 800W to 2500W in 50W increments, both in initial setup and options flow.
Fixed¶
write_register()refresh never executed: Theasync_request_refresh()call after a successful register write was dead code — it sat afterreturn Trueinside theasync with self.lockblock and was never reached. Restructured the method so the refresh executes outside the lock after a successful write.- First execution ignores time slot restrictions: After integration reload/reconfiguration, the first control cycle sent power to batteries without checking time slot restrictions. This caused a brief discharge pulse (~2.5s) even when the current day/time was outside any configured slot, which was then corrected to 0W on the next cycle. The first execution now checks
_is_operation_allowed()before sending any power commands. - Charge/discharge power limits swapped: The PD controller clamped charging power using
max_discharge_powerand vice versa. This caused charging to be limited to the discharge limit (e.g., 800W instead of 2500W) and discharge to use the charge limit. Both clamp conditions now use the correct limit for their direction.
[1.1.1] - 2026-02-27¶
Fixed¶
- Incorrect time slot translations: Fixed descriptions in English, German, French, and Dutch that incorrectly stated batteries "will NOT discharge" during slots. The correct behavior is the opposite — batteries are ALLOWED to discharge during configured slots and blocked outside them. Spanish translations were already correct.
[1.1.0] - 2026-02-27¶
Added¶
- Configurable target grid power per time slot: The PD controller can now regulate toward a user-defined grid power target instead of the fixed 0W. Each time slot includes a
target_grid_powerfield (range: -500W to +500W, default: 0W). Negative values target slight export (e.g. -150W), positive values allow slight import. Outside of active time slots, the controller defaults to 0W. This enables economic optimization for tariff setups where feed-in is more valuable than self-consumption. - Minimum charge/discharge power per time slot: Each time slot can now define
min_charge_powerandmin_discharge_powerthresholds (range: 0-500W, default: 0W = disabled). When the PD controller output is below the configured minimum, the controller stays idle instead of operating at inefficient low power levels. This reduces micro-cycling, unnecessary battery wear, and improves roundtrip efficiency. - Time slot overlap validation: The config flow now rejects time slots that overlap with existing ones on shared days. Also prevents midnight-crossing slots (start >= end) to avoid day-ambiguity — users must create two separate slots for overnight periods instead.
- Midnight-crossing slot runtime logic removed: Simplified
_is_operation_allowed()and_get_active_slot()to remove dead midnight-crossing code, since midnight-crossing slots are now rejected at configuration time.
[1.0.4] - 2026-02-26¶
Added¶
- V3 battery support: Version-specific Modbus register maps, entity definitions, and timing for V3 firmware.
- V3 packet correction: Automatically fixes malformed MBAP length bytes in V3 exception responses that caused pymodbus timeouts.
- Automatic reconnection in Modbus retry loops: Both read and write operations now reconnect if the TCP connection is lost mid-retry (skipped during shutdown to avoid occupying the single TCP slot).
Changed¶
- Platform files (
button.py,number.py,select.py,switch.py) now use coordinator's version-specific entity definitions instead of importing hardcoded V2 lists. ManualModeSwitchusescoordinator.get_register()instead of hardcoded register addresses, making it version-aware.- Bumped
pymodbusrequirement from>=3.0.0to>=3.5.0. - Version-specific Modbus timing: V2 uses 50ms, V3 uses 150ms between messages.
- RS485 control mode disable now writes the correct
command_offvalue (0x55BB) instead of0, which V3 firmware rejects with Modbus Exception 3.
Fixed¶
- Race condition during reload: Control loop and coordinator refresh continued running during
async_unload_entry, causing "Not connected" write errors. Fixed by cancelling timers at the start of unload, adding a shutdown flag to suppress expected errors and skip operations, and reordering the unload sequence to: cancel timers → set shutdown flag → wait for in-flight ops → unload platforms → write shutdown registers → disconnect. - Reconfiguration fails randomly with connection error: In-flight coordinator polls could survive the shutdown
disconnect()and automatically reconnect via Modbus retry logic, occupying the battery's single TCP connection slot. The newasync_setup_entrywould then fail with[Errno 111] Connect call failed. Fixed by exiting Modbus read/write retries immediately during shutdown and adding an early exit check in the coordinator poll cycle. - Options flow connection validation: Reconfiguration now temporarily closes the coordinator's active connection under lock, tests with a fresh connection, and reconnects the coordinator, instead of opening a second Modbus TCP connection (which the firmware rejects since it only supports one simultaneous connection).
- V3 Modbus serialization: Polling reads now acquire the coordinator lock, preventing interleaving with control loop writes on the same TCP connection. V3 firmware mishandled concurrent requests, causing transaction ID mismatches ("extra data") and written values not being applied. New
write_power_atomic()method writes all power registers and reads feedback under a single lock acquisition.
[1.0.3] - 2026-02-22¶
Fixed¶
- Fix
KeyErrorforforce_modewhendata_typeis missing (PR #3 by @openschwall).
[1.0.2] - 2026-02-20¶
Fixed¶
- Remove redundant
_write_config_to_batteries()call during options flow. The function opened a second Modbus TCP connection while the coordinator was still holding the first one, causing "Not connected" errors on V3 batteries. The reload already applies all configuration values viaasync_setup_entry(). - Fix
async_close()in Modbus client attempting toawaitthe synchronousclose()method, which caused "object NoneType can't be used in 'await' expression" errors on every reload. - Fix "Unable to remove unknown job listener" error on reload by switching
homeassistant_startedlistener fromasync_listen_oncetoasync_listen. The one-time listener auto-removed itself after firing, causingasync_on_unloadto fail when trying to cancel it during reload. - Run startup consumption backfill immediately on reload instead of waiting for
homeassistant_started(which never fires again after boot).
[1.0.1] - 2026-02-18¶
Changed¶
- Remove V3-exclusive entity definitions to match V2 register footprint.
- Deleted 20 entity definitions from the V3 definition lists (sensors, binary sensors, selects, buttons) that had no equivalent in V2.
- This reduces V3 Modbus-polled registers from ~38 to ~22, which should significantly cut options flow reload time for V3 users.