v1.0.0b1
Pre-release
Pre-release
[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):
- 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.
- 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.- Restart Home Assistant, then go to Settings → Devices & Services → Add Integration → Omnibattery.
- 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.
- Once Omnibattery is running, remove the old
marstek-venus-energy-managerintegration and repository from HACS.- 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/.