Deploy 0.18.0 to master #193

Merged
admin merged 30 commits from deploy-0.18.0 into master 2026-10-16 00:42:55 +02:00
2 changed files with 113 additions and 5 deletions
Showing only changes of commit df6bc0f76b - Show all commits
@@ -51,8 +51,8 @@ Times are seconds after `docker restart` of Home Assistant:
The first write comes 1.0-4.4 s after `RUNNING`, so the whole wait is the The first write comes 1.0-4.4 s after `RUNNING`, so the whole wait is the
proxy's. With a 400 s sleep Core moved on at 300.88 s on its own (bootstrap's proxy's. With a 400 s sleep Core moved on at 300.88 s on its own (bootstrap's
stage timeout), so one slow integration holds the start for about 5 minutes stage timeout), so one slow integration holds a start stage for about 5
at most. The live start took 414 s. minutes. The whole start can take longer: the live one took 414 s.
Before this change the page said "Not ready — Battery level: no usable Before this change the page said "Not ready — Battery level: no usable
reading", which sends the reader to Developer Tools for an entity that is reading", which sends the reader to Developer Tools for an entity that is
+111 -3
View File
@@ -6,18 +6,21 @@ so the add-on cannot control then. This file grades what it says meanwhile:
1. The log: one WARNING when Home Assistant stops answering, one INFO line 1. The log: one WARNING when Home Assistant stops answering, one INFO line
every 5 minutes while it stays down, one INFO line with the wait when it every 5 minutes while it stays down, one INFO line with the wait when it
answers again. Only the API channel counts as "waiting". answers again. Only the API channel counts as "waiting": a 502 or a
refused connection turns it on, the next good read off, a timeout never.
2. The status page, N=1 and N=2: the waiting row is first and red, and is the 2. The status page, N=1 and N=2: the waiting row is first and red, and is the
banner. Home says "Waiting for Home Assistant" in English and Dutch, not banner. Home says "Waiting for Home Assistant" in English and Dutch, not
"Needs you now", unless a safety stop outranks it. "Needs you now", unless a safety stop outranks it. The page script has it.
3. Control is unchanged: the same readings give the same writes with the 3. Control is unchanged: the same readings give the same writes with the
waiting flag on or off. waiting flag on or off, with 502 readings and with live ones.
Mutating a module this file grades? Read MUTATION.md first (purge __pycache__). Mutating a module this file grades? Read MUTATION.md first (purge __pycache__).
""" """
import asyncio import asyncio
import logging import logging
import os
import re
from types import SimpleNamespace from types import SimpleNamespace
from app import hass as _hass from app import hass as _hass
@@ -141,6 +144,66 @@ check("waiting() follows the API channel only, not the meter websocket",
with_clock(channels) == [False, False, True, True, False]) with_clock(channels) == [False, False, True, True, False])
# The flag as `HomeAssistant.state()` sets it: a 502 or a refused connection
# turns it on, the next good read turns it off, and a timeout (a slow Home
# Assistant, not a starting one) leaves it alone.
class _Answer:
def __init__(self, what):
self.what = what
async def __aenter__(self):
if isinstance(self.what, BaseException):
raise self.what
return self
async def __aexit__(self, *_a):
return False
@property
def status(self):
return self.what
def raise_for_status(self):
pass
async def json(self):
return {"state": "1"}
class _Session:
def __init__(self, answers):
self.answers = list(answers)
def get(self, *_a, **_k):
return _Answer(self.answers.pop(0))
def reads(answers):
async def go():
real, _hass.OUTAGE = _hass.OUTAGE, _hass.Outage()
lg = logging.getLogger("goodwe.hass")
lg.disabled = True
try:
h = _hass.HomeAssistant(_Session(answers), token="t")
seen = []
for _ in answers:
await h.state("sensor.x")
seen.append(_hass.OUTAGE.waiting())
return seen
finally:
_hass.OUTAGE = real
lg.disabled = False
return asyncio.run(go())
check("a 502 read turns waiting on and the next good read turns it off",
reads([502, 200, 503, 404]) == [True, False, True, False])
check("a refused connection is waiting too",
reads([_hass.aiohttp.ClientConnectionError(), 200]) == [True, False])
check("a read timeout is not waiting",
reads([TimeoutError(), TimeoutError()]) == [False, False])
# --------------------------------------------------------------------------- # # --------------------------------------------------------------------------- #
print("2. the status page, N=1 and N=2") print("2. the status page, N=1 and N=2")
@@ -295,6 +358,18 @@ check("shadow mode keeps its own state over the wait",
pay["state"]["kind"] == "shadow") pay["state"]["kind"] == "shadow")
# The page script, read as text (the image has no JS engine): an amber dot,
# and the batteries tab carries the banner like the other held states.
JS = open(os.path.join(os.path.dirname(os.path.abspath(__file__)), "app",
"static", "app.js"), encoding="utf-8").read()
check("the page script gives 'waiting' an amber dot",
re.search(r'alarm: " bad", waiting: " warn" \}\[st\.kind\]', JS)
is not None)
check("...and shows it as the batteries-tab banner",
'["shadow", "stopped", "alarm", "waiting"].includes(S.state.kind)'
in JS)
# --------------------------------------------------------------------------- # # --------------------------------------------------------------------------- #
print("3. control is unchanged by the waiting flag") print("3. control is unchanged by the waiting flag")
@@ -323,6 +398,39 @@ for label, opts in (("N=1", ONE), ("N=2", TWO)):
f"asks 0 W", on[1] and all(v == 0.0 for _e, v in on[1])) f"asks 0 W", on[1] and all(v == 0.0 for _e, v in on[1]))
# With 502 readings every write is 0 W whatever the flag says, so the check
# above cannot see a flag that forces 0 W. Live readings can: the flag alone
# must not change a non-zero write.
class _LiveHass(_Hass):
async def number(self, e, invert=False):
v = {"sensor.grid": 900.0}.get(e, 50.0 if "soc" in e else 0.0)
return -v if invert else v
async def set_number(self, e, v):
self.writes.append((e, float(v)))
return True
def run_live(opts, flag):
async def go():
h = _LiveHass()
c = make(opts, h)
(waiting_on if flag else waiting_off)()
for _ in range(4):
await c.read_inputs()
await c.cycle()
await c.send_heartbeats(asyncio.get_running_loop().time(), 10.0)
waiting_off()
return h.writes
return asyncio.run(go())
for label, opts in (("N=1", ONE), ("N=2", TWO)):
off, on = run_live(opts, False), run_live(opts, True)
check(f"{label}: with live readings the flag changes no write "
f"({on[-2:]})", off == on and any(v != 0.0 for _e, v in on))
print() print()
if fails: if fails:
print(f"{len(fails)} FAILED") print(f"{len(fails)} FAILED")