SMA-2: each inverter is reached through a driver; GoodWe behaviour unchanged
app/drivers.py holds one Driver per roster entry (configured, actuator_id, limits, read, write, release, probe, link_stale_s, batt_bound, settle_s, brand, words). GoodWeEsphomeDriver makes the calls the code it replaced made, in the same order. Inverter gets driver; has_writer, is_live and link_note read it. The SoC/power read, the write, the device range (run_control, amain, the settings panel), rule 2 per unit and the SAFETY-28 census go through it. setpoint_entity is optional per entry beside driver, host, port, unit_id and maintenance; only goodwe_esphome is selectable, the rest are checked and unused. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude
parent
23ff9d0920
commit
e56eeef8f0
@@ -71,6 +71,7 @@ from . import messages
|
||||
from .freeze import (
|
||||
FreezeWatchdog, FreezeWatchdogConfig, looks_frozen, rail_unreachable, rail_w,
|
||||
)
|
||||
from .drivers import GoodWeEsphomeDriver
|
||||
from .hass import CORE_API, OUTAGE, HomeAssistant
|
||||
# ⚠️ `LINK_STALE_S` IS RE-EXPORTED DELIBERATELY. `live_units()` reasons about it
|
||||
# and two checks read `app.main.LINK_STALE_S` to build a stale link, so the
|
||||
@@ -3774,6 +3775,18 @@ class Controller:
|
||||
elif self.inputs_bad_since is None:
|
||||
self.inputs_bad_since = datetime.now(timezone.utc)
|
||||
|
||||
def _driver(self, inv):
|
||||
"""`inv`'s driver, wired to this Controller's Home Assistant on first use.
|
||||
|
||||
SMA-2. Bound HERE rather than when the roster is built, so a unit added
|
||||
to `self.inverters` afterwards is wired too, and the driver follows
|
||||
whatever `self.hass` is at the time (a check may swap it).
|
||||
"""
|
||||
drv = inv.driver
|
||||
if not drv.bound():
|
||||
drv.bind_getter(lambda: self.hass)
|
||||
return drv
|
||||
|
||||
async def _read_unit(self, inv, now_mono: float) -> None:
|
||||
"""One inverter's own SoC and battery power, from ITS OWN entities.
|
||||
|
||||
@@ -3793,8 +3806,10 @@ class Controller:
|
||||
second spelling of "what is standing" would let the two disagree about
|
||||
the number they are both reasoning about.
|
||||
"""
|
||||
inv.soc = await self.hass.number(inv.soc_entity)
|
||||
raw = await self.hass.number(inv.batt_entity, inv.batt_invert)
|
||||
# SMA-2. The driver reads, in the order this method always did: SoC,
|
||||
# then power. `extra` is whatever else the driver learned; nothing
|
||||
# consumes it yet.
|
||||
inv.soc, raw, _extra = await self._driver(inv).read()
|
||||
was_stale = inv.batt_fresh.stale
|
||||
was_releases = inv.batt_fresh.releases
|
||||
inv.batt_fresh.submit(raw, inv.standing_w(), now_mono)
|
||||
@@ -4029,9 +4044,9 @@ class Controller:
|
||||
for inv in targets))
|
||||
|
||||
async def _write_unit(self, inv, value: float, *, force: bool = False) -> None:
|
||||
entity = inv.setpoint_entity
|
||||
if not entity:
|
||||
if not inv.driver.configured():
|
||||
return
|
||||
entity = inv.driver.actuator_id() # for the log line below
|
||||
|
||||
# --- SAFETY-03: THE CLAMP, AND IT IS ON THE ONLY PATH TO THE WIRE -----
|
||||
# Every write in this add-on comes through here - the cycle, the
|
||||
@@ -4154,7 +4169,7 @@ class Controller:
|
||||
# tick by itself: `last_write` is updated only on an ACCEPTED write, so
|
||||
# `changed` is still true.
|
||||
try:
|
||||
ok = await asyncio.wait_for(self.hass.set_number(entity, value),
|
||||
ok = await asyncio.wait_for(self._driver(inv).write(value),
|
||||
timeout=WRITE_BUDGET_S)
|
||||
except (asyncio.TimeoutError, TimeoutError):
|
||||
ok = False
|
||||
@@ -6730,9 +6745,8 @@ class Controller:
|
||||
# Concurrently, for `read_inputs()`'s reason: a slow ESPHome on one unit
|
||||
# must not delay the controller's start on every other.
|
||||
async def _limits(inv):
|
||||
if inv.setpoint_entity:
|
||||
inv.dev_min, inv.dev_max, _ = await self.hass.limits(
|
||||
inv.setpoint_entity)
|
||||
if inv.driver.configured():
|
||||
inv.dev_min, inv.dev_max = await self._driver(inv).limits()
|
||||
await asyncio.gather(*(_limits(inv) for inv in self.inverters))
|
||||
entity = self.o.get("setpoint_entity", "")
|
||||
# ⚠️ THIS USED TO WARN AND CARRY ON, and that was the bug CFG-01
|
||||
@@ -7316,9 +7330,10 @@ class Controller:
|
||||
rows = []
|
||||
for inv in self.inverters:
|
||||
unit = None if len(self.inverters) == 1 else inv.name
|
||||
rows.append((messages.LABEL_SOC, unit, inv.soc, inv.soc_entity))
|
||||
rows.append((messages.LABEL_SOC, unit, inv.soc,
|
||||
inv.driver.soc_source()))
|
||||
rows.append((messages.LABEL_BATTERY, unit, inv.batt,
|
||||
inv.batt_entity))
|
||||
inv.driver.batt_source()))
|
||||
if not self.p1_enabled:
|
||||
# In P1 mode the check above replaces this one; leaving both in
|
||||
# would report "no entity configured" for a meter_entity that is
|
||||
@@ -7360,9 +7375,8 @@ class Controller:
|
||||
"text": messages.row_sensor(label, value, unit)})
|
||||
|
||||
for inv in self.inverters:
|
||||
sp = inv.setpoint_entity
|
||||
unit = None if len(self.inverters) == 1 else inv.name
|
||||
if not sp:
|
||||
if not inv.driver.configured():
|
||||
out.append({"ok": False, "warn": False, "id": "control",
|
||||
"text": messages.row_control_unset(unit)})
|
||||
elif inv.dev_max is None:
|
||||
@@ -8205,10 +8219,13 @@ async def amain() -> None:
|
||||
# validator cannot do for itself; (None, None) when ESPHome has not come
|
||||
# up yet is not a refusal - run_control() re-checks once the real rating
|
||||
# is known. See validate.py for where the line is drawn and why.
|
||||
# SMA-2. The flat options are the one-inverter shape, so it is the
|
||||
# GoodWe driver's, asked without a Controller (none exists yet).
|
||||
_sp = str(opts.get("setpoint_entity", "") or "").strip()
|
||||
_drv = GoodWeEsphomeDriver(setpoint_entity=_sp).bind(hass)
|
||||
_dev_min = _dev_max = None
|
||||
if _sp:
|
||||
_dev_min, _dev_max, _ = await hass.limits(_sp)
|
||||
_dev_min, _dev_max = await _drv.limits()
|
||||
_LOG.info("setpoint device %s reports range %s…%s W", _sp, _dev_min, _dev_max)
|
||||
_problems = validate_startup_config(opts, _dev_min, _dev_max)
|
||||
# UX-15: a new install names no inverter. When that is the ONLY
|
||||
|
||||
Reference in New Issue
Block a user