UX-13 P0 review: keep unread keys, back off a failed entity read
- uimodel: editing a listed inverter keeps the stored flat keys, and a region of "own" keeps a stored price_taxes_profile (both were dropped). - haentities: a failed read also waits CACHE_S, so polls while Home Assistant is down do not queue one 10 s read each. - ui_text: the Flanders preset names its DSO, and the help says Wallonia and Brussels have no figures yet. REGION_PRESETS cites the card. - tests: decode of a reversed solar row, both kept keys, the backoff. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -51,13 +51,17 @@ class EntityCache:
|
||||
self.clock = clock
|
||||
self.rows: dict | None = None
|
||||
self.read_at: float | None = None
|
||||
self.tried_at: float | None = None
|
||||
self._lock = asyncio.Lock()
|
||||
|
||||
async def get(self) -> tuple[dict | None, float | None]:
|
||||
"""(rows by entity id, age in s), or (None, None) if never read."""
|
||||
async with self._lock: # one HA read however many pickers ask
|
||||
now = self.clock()
|
||||
if self.read_at is None or now - self.read_at >= CACHE_S:
|
||||
# A failed read also waits CACHE_S, so polls while HA is down do
|
||||
# not queue one 10 s read each behind this lock.
|
||||
if self.tried_at is None or now - self.tried_at >= CACHE_S:
|
||||
self.tried_at = now
|
||||
states = await self.hass.states()
|
||||
if states is not None:
|
||||
rows = (row(s) for s in states)
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
"fields": {
|
||||
"solar": {"name": "Solar inverters", "description": "Each solar inverter's power sensor, which the add-on records but does not use for control."},
|
||||
"contract": {"name": "Your contract", "description": "How your supplier prices each kWh you take from the grid."},
|
||||
"region": {"name": "Region", "description": "Fills in the grid costs, levies and VAT for the year shown, so check them on your bill."},
|
||||
"region": {"name": "Region", "description": "Fills in grid costs, levies and VAT for the year shown, but not yet for Wallonia or Brussels, so check your bill."},
|
||||
"fallback": {"name": "Price when the feed is down", "description": "Used when the price source does not answer."},
|
||||
"fixed_price": {"name": "Price per kWh, all in", "description": "Energy, supplier costs, grid costs, levies and VAT together."},
|
||||
"day_price": {"name": "Day price, weekdays 07:00 to 22:00", "description": "All in, as on your bill."},
|
||||
@@ -29,7 +29,7 @@
|
||||
"choices": {
|
||||
"contract": {"none": "No prices", "fixed": "Fixed price", "daynight": "Day and night", "dynamic": "Dynamic, hourly", "custom": "Other time windows"},
|
||||
"fallback": {"none": "None", "fixed": "One price", "daynight": "Day and night", "custom": "Other time windows"},
|
||||
"region": {"flanders": "Flanders", "own": "I enter my own figure"}
|
||||
"region": {"flanders": "Flanders, Fluvius Antwerpen", "own": "I enter my own figure"}
|
||||
},
|
||||
"applies": {
|
||||
"restart": {"name": "Applies after a restart"},
|
||||
|
||||
@@ -31,7 +31,8 @@ class DeriveError(ValueError):
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Region presets for "Grid costs, levies and VAT". Year-stamped, updated each
|
||||
# January (owner, 2026-09-27). Only presets with a sourced figure are listed:
|
||||
# Flanders is app/price.py's Fluvius Antwerpen sum (TARIFF-18). Wallonia and
|
||||
# Flanders is app/price.py's Fluvius Antwerpen sum (TARIFF-18: Eneco
|
||||
# residential card, Flanders, September 2026, VAT included). Wallonia and
|
||||
# Brussels have a day and a night band (config.yaml) and join with sourced
|
||||
# figures; until then those sites choose "own".
|
||||
# --------------------------------------------------------------------------- #
|
||||
@@ -93,9 +94,10 @@ def _roster_encode(m, keep):
|
||||
return out
|
||||
out["inverters"] = [{k: v for k, v in c.items() if v is not None}
|
||||
for c in cards]
|
||||
# The flat keys beside a list are not read: they stay as stored.
|
||||
out.update({k: copy.deepcopy(keep[k]) for k in _ROSTER_FLAT if k in keep})
|
||||
if shape == "flat":
|
||||
# CFG-04 rule 9e: a flat power request entity beside a list is refused.
|
||||
# The other flat keys stay as stored and are not read.
|
||||
out["setpoint_entity"] = ""
|
||||
return out
|
||||
|
||||
@@ -252,7 +254,10 @@ def _region_decode(o):
|
||||
def _region_encode(m, keep):
|
||||
region = m.get("ui.region")
|
||||
if region == OWN:
|
||||
return {"price_taxes_eur_kwh": m.get("price_taxes_eur_kwh")}
|
||||
out = {"price_taxes_eur_kwh": m.get("price_taxes_eur_kwh")}
|
||||
if "price_taxes_profile" in keep: # "own" keeps the stored table
|
||||
out["price_taxes_profile"] = keep["price_taxes_profile"]
|
||||
return out
|
||||
if region not in REGION_PRESETS:
|
||||
raise DeriveError(f"unknown region {region!r}")
|
||||
p = REGION_PRESETS[region]
|
||||
|
||||
Reference in New Issue
Block a user