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
@@ -124,9 +124,12 @@ from datetime import datetime
|
||||
from .clamp import ClampResult
|
||||
from .delivery import DeliveryConfig, DeliveryWatchdog
|
||||
from . import messages
|
||||
# SMA-2. The driver seam: how this unit's inverter is read and steered.
|
||||
from .drivers import (GOODWE_LINK_STALE_S, GoodWeEsphomeDriver,
|
||||
build_driver)
|
||||
# SAFETY-37. The freshness policy, and the ONE place its bounds are written.
|
||||
# freshness.py imports nothing from this package, so the direction is one-way.
|
||||
from .freshness import BOUNDS, Freshness
|
||||
from .freshness import Freshness
|
||||
from .idle import IdleReading
|
||||
# CFG-03's ONE in-code mirror of config.yaml's SAFETY-24 defaults. Imported
|
||||
# rather than retyped: the flat-keys path below needs `bms_soc_floor`'s
|
||||
@@ -149,7 +152,11 @@ from .watchdog import SignWatchdog, SignWatchdogConfig
|
||||
# decided it will not. No config key: CFG-03 tracks three places a default has
|
||||
# to be kept in step and this does not earn a fourth - it is derived from a
|
||||
# constant the firmware and validate.py already share.
|
||||
LINK_STALE_S = 30.0
|
||||
#
|
||||
# SMA-2. This is the GoodWe driver's `link_stale_s` (drivers.py), kept under its
|
||||
# old name for the checks and for main.py. `is_live()` and `link_note()` now ask
|
||||
# each unit's DRIVER for its window; a driver for another make states its own.
|
||||
LINK_STALE_S = GOODWE_LINK_STALE_S
|
||||
|
||||
# The name of the inverter a SINGLE-INVERTER config describes. ⚠️ A NAME, NOT A
|
||||
# CONFIGURATION - it exists so that a per-inverter claim has an actuator to be
|
||||
@@ -195,12 +202,14 @@ class Inverter:
|
||||
def __init__(self, name, *, setpoint_entity="", soc_entity="",
|
||||
batt_entity="", batt_invert=False, max_w=2000.0,
|
||||
bms_soc_floor=None, capacity_kwh=None, store=None,
|
||||
sign_cfg=None, delivery_cfg=None):
|
||||
sign_cfg=None, delivery_cfg=None, driver=None):
|
||||
self.name = str(name)
|
||||
self.setpoint_entity = str(setpoint_entity or "")
|
||||
self.soc_entity = str(soc_entity or "")
|
||||
self.batt_entity = str(batt_entity or "")
|
||||
self.batt_invert = bool(batt_invert)
|
||||
# SMA-2. HOW THIS UNIT IS READ AND STEERED. The entity keywords above
|
||||
# are the GoodWe driver's, kept so every existing caller builds the
|
||||
# same unit; a roster entry that selects another driver passes
|
||||
# `driver` and the entity keywords stay empty.
|
||||
self.driver = driver if driver is not None else GoodWeEsphomeDriver(
|
||||
setpoint_entity, soc_entity, batt_entity, batt_invert)
|
||||
# The supervised cap on what may be written to THIS setpoint entity.
|
||||
# See the module docstring on why this is a number and not a `Tuning`.
|
||||
self.max_w = float(max_w)
|
||||
@@ -256,7 +265,7 @@ class Inverter:
|
||||
# constructed here: SoC's obligation term is energy through the pack, not
|
||||
# command travel, and the constant it needs has not been measured. That
|
||||
# is TEL-08's, and freshness.py's docstring says exactly what it owes.
|
||||
self.batt_fresh = Freshness(BOUNDS["batt"])
|
||||
self.batt_fresh = Freshness(self.driver.batt_bound)
|
||||
|
||||
# --- what this unit was last told, and what it last accepted ---------
|
||||
# `target` is THIS unit's share of the site target from the last
|
||||
@@ -338,6 +347,25 @@ class Inverter:
|
||||
def idle_w(self) -> float:
|
||||
return self.idle.w
|
||||
|
||||
# -- the GoodWe driver's entities, read through for the callers that name
|
||||
# them (the panel's range check, the status rows, the checks). A driver
|
||||
# without such an entity answers "" - the same as "not set".
|
||||
@property
|
||||
def setpoint_entity(self) -> str:
|
||||
return getattr(self.driver, "setpoint_entity", "")
|
||||
|
||||
@property
|
||||
def soc_entity(self) -> str:
|
||||
return getattr(self.driver, "soc_entity", "")
|
||||
|
||||
@property
|
||||
def batt_entity(self) -> str:
|
||||
return getattr(self.driver, "batt_entity", "")
|
||||
|
||||
@property
|
||||
def batt_invert(self) -> bool:
|
||||
return bool(getattr(self.driver, "batt_invert", False))
|
||||
|
||||
# -- store ---------------------------------------------------------------
|
||||
def key(self, name: str) -> str:
|
||||
"""This unit's namespaced Store key for `name`. See store.unit_key()."""
|
||||
@@ -387,7 +415,7 @@ class Inverter:
|
||||
return out
|
||||
|
||||
def has_writer(self) -> bool:
|
||||
return bool(self.setpoint_entity)
|
||||
return self.driver.configured()
|
||||
|
||||
# -- liveness ------------------------------------------------------------
|
||||
def note_write(self, ok: bool, now: datetime) -> None:
|
||||
@@ -398,7 +426,7 @@ class Inverter:
|
||||
elif self.rejected_since is None:
|
||||
self.rejected_since = now
|
||||
|
||||
def is_live(self, now: datetime, stale_s: float = LINK_STALE_S):
|
||||
def is_live(self, now: datetime, stale_s: float | None = None):
|
||||
"""`(live, why_not)` - may this controller command this unit THIS TICK?
|
||||
|
||||
Read the module docstring before widening this - in particular on why a
|
||||
@@ -411,7 +439,11 @@ class Inverter:
|
||||
|
||||
⚠️ ANSWERED PER TICK, NEVER CACHED, for `allocator.plan()`'s reason: a
|
||||
link goes stale between one cycle and the next.
|
||||
|
||||
`stale_s` is the driver's `link_stale_s` unless a caller passes one.
|
||||
"""
|
||||
if stale_s is None:
|
||||
stale_s = self.driver.link_stale_s
|
||||
if self.rejected_since is not None:
|
||||
held = (now - self.rejected_since).total_seconds()
|
||||
if held >= float(stale_s):
|
||||
@@ -451,9 +483,11 @@ class Inverter:
|
||||
held = (0.0 if self.rejected_since is None
|
||||
else (now - self.rejected_since).total_seconds())
|
||||
if not live:
|
||||
return messages.row_not_controllable(self.name, held, LINK_STALE_S)
|
||||
return messages.row_not_controllable(self.name, held,
|
||||
self.driver.link_stale_s)
|
||||
if self.rejected_since is not None:
|
||||
return messages.row_commands_refused(self.name, held, LINK_STALE_S)
|
||||
return messages.row_commands_refused(self.name, held,
|
||||
self.driver.link_stale_s)
|
||||
return ""
|
||||
|
||||
def __repr__(self) -> str:
|
||||
@@ -556,10 +590,8 @@ def build_roster(opts: dict, store, sign_cfg=None,
|
||||
# identically, which is AC 3.
|
||||
return [Inverter(
|
||||
UNIT_A,
|
||||
setpoint_entity=opts.get("setpoint_entity", ""),
|
||||
soc_entity=opts.get("soc_entity", ""),
|
||||
batt_entity=opts.get("batt_entity", ""),
|
||||
batt_invert=bool(opts.get("batt_invert")),
|
||||
# SMA-2. The flat keys are the GoodWe shape and only that.
|
||||
driver=GoodWeEsphomeDriver.from_entry(opts),
|
||||
max_w=_flat_max_w(opts),
|
||||
# SAFETY-32. THE FLAT KEY, because this IS the single-inverter shape
|
||||
# - the one pack the flat options describe is the one pack whose
|
||||
@@ -588,10 +620,9 @@ def build_roster(opts: dict, store, sign_cfg=None,
|
||||
entry = entry if isinstance(entry, dict) else {}
|
||||
roster.append(Inverter(
|
||||
name,
|
||||
setpoint_entity=entry.get("setpoint_entity", ""),
|
||||
soc_entity=entry.get("soc_entity", ""),
|
||||
batt_entity=entry.get("batt_entity", ""),
|
||||
batt_invert=bool(entry.get("batt_invert")),
|
||||
# SMA-2. The entry's `driver` (default goodwe_esphome) picks the
|
||||
# class; the entry itself says where its entities are.
|
||||
driver=build_driver(entry),
|
||||
# ⚠️ NOT `opts["max_w"]`. See the docstring: a rating an entry did
|
||||
# not state is not the flat one, it is a rating rule 9 refuses.
|
||||
max_w=_entry_max_w(entry),
|
||||
|
||||
Reference in New Issue
Block a user