UX-10: the last plain-language leftovers: log lines, two table rows, 'Why: Other', red at 0 s
- test_messages.py part 6: every dynamic part of a WARNING+ line is classified as data or as a built text, and every built text is produced by the real code and checked. This is the gap the 2d scan had: it counted `%s` as one word, so reasons built by loop_clamp_note() and friends were never seen. - Quoted reasons rewritten to STYLE.md (red-alarm shape, codes kept): OUTPUT_CLAMP, METER_FROZEN, CIRCULATING_POWER, STALE_READING, the peak guard (codes now PEAK_GUARD*), the migration report, price feed, price history and gate reasons. Plurals via messages.times()/count(). - Top table: Price history "96 prices over 24 hours for 26 Sep", Tariff "one every 15 minutes or every hour", "not final yet" dropped. - Levels and noise: per-cycle shadow line DEBUG, shadow on/off INFO, aiohttp access log only at log level debug, settings warnings once per start, one plain line per Home Assistant outage (hass.Outage). - INFO jargon and error texts: no "§8.3", ISO times, "banked", "keys ARE", "NOT_OBSERVED", "did NOT land", "AND". - Display bugs: WHY_WINNER covers every winner and safety reason (main.why_key(), part 7 finds them in the code); the meter row is amber "waiting for the first reading" inside its limit after a start; the missing-reading age is counted when the page asks. - STYLE.md and DECISIONS/UX-08-short-notifications.md (UX-10 section). No change to safety behaviour, alarm levels, notification ids, triggers, cadence or the banner cascade. One row colour changes (meter at start), justified in DECISIONS. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
125b0702a2
commit
23a3a3f5b5
@@ -114,6 +114,8 @@ import sqlite3
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timedelta, timezone, tzinfo
|
||||
|
||||
from . import messages
|
||||
|
||||
_LOG = logging.getLogger("goodwe.tariff")
|
||||
|
||||
QUARTER_S = 900
|
||||
@@ -339,8 +341,8 @@ class PeakStore:
|
||||
# INFO, not ERROR: on a dev host or in a test container there is no
|
||||
# /data and nothing is wrong. A store that OPENED and then broke is
|
||||
# the error case, and it is logged as one below.
|
||||
_LOG.info("no peak store: %s - the monthly peak will read "
|
||||
"NOT_OBSERVED", self.last_error)
|
||||
_LOG.info("No peak store (%s), so the month peak cannot be read "
|
||||
"and only the peak limit is used.", self.last_error)
|
||||
return
|
||||
try:
|
||||
self._db = sqlite3.connect(self.path, timeout=5.0,
|
||||
@@ -360,8 +362,9 @@ class PeakStore:
|
||||
# fault must not stop a controller that is regulating a battery. The
|
||||
# cost of continuing is a peak that reads NOT_OBSERVED, and every
|
||||
# reader is built to treat that as "do not use me", not as 0 W.
|
||||
_LOG.error("peak store unusable at %s (%s) - the monthly peak will "
|
||||
"read NOT_OBSERVED, which is not a zero peak", self.path, err)
|
||||
_LOG.error("The peak store at %s cannot be used (%s), so only the "
|
||||
"peak limit is used. The month peak is unknown, not "
|
||||
"zero.", self.path, err)
|
||||
|
||||
def close(self) -> None:
|
||||
if self._db is not None:
|
||||
@@ -672,12 +675,12 @@ class PeakTracker:
|
||||
now = now or datetime.now(timezone.utc)
|
||||
snap = self.store.load_partial()
|
||||
if not snap:
|
||||
self.restore_state = ("fresh: no checkpoint" if self.store.ok
|
||||
else "fresh: store unreadable")
|
||||
self.restore_state = ("fresh start, nothing saved" if self.store.ok
|
||||
else "fresh start, the store cannot be read")
|
||||
return self.restore_state
|
||||
block = snap.get("block")
|
||||
if block is None:
|
||||
self.restore_state = "fresh: checkpoint had no open block"
|
||||
self.restore_state = "fresh start, no open 15-minute period saved"
|
||||
return self.restore_state
|
||||
|
||||
current = int(now.timestamp() // QUARTER_S) * QUARTER_S
|
||||
@@ -686,11 +689,13 @@ class PeakTracker:
|
||||
# A reshaped accumulator (meter_phases changed) is a different
|
||||
# measurement, not a resumable one. Say so; do not half-resume.
|
||||
self.store.clear_partial()
|
||||
self.restore_state = "fresh: checkpoint does not fit this averager"
|
||||
self.restore_state = ("fresh start, the saved period does not "
|
||||
"fit the number of phases")
|
||||
return self.restore_state
|
||||
self.restore_state = (
|
||||
f"resumed the open block at {snap.get('elapsed', 0.0):.0f} s "
|
||||
f"of {QUARTER_S} s ({snap.get('acc', 0.0) / QUARTER_S:.0f} W banked)")
|
||||
f"resumed the open 15-minute period at "
|
||||
f"{snap.get('elapsed', 0.0):.0f} s of {QUARTER_S} s, with "
|
||||
f"{snap.get('acc', 0.0) / QUARTER_S:.0f} W counted so far")
|
||||
return self.restore_state
|
||||
|
||||
# The block ended while the add-on was down. Its measured part really
|
||||
@@ -704,10 +709,14 @@ class PeakTracker:
|
||||
if snap.get("pp_acc") and snap.get("last_pp") else None))
|
||||
banked = self.store.record_block(closed, self.policy)
|
||||
self.store.clear_partial()
|
||||
# UX-10: plain words and the local time, not ISO and "banked".
|
||||
self.restore_state = (
|
||||
f"closed a stale block from {closed.start.isoformat()} at "
|
||||
f"{closed.offtake_avg_w:.0f} W (partial coverage), "
|
||||
+ ("banked" if banked else "NOT banked - store unwritable"))
|
||||
f"closed the 15-minute period from "
|
||||
f"{messages.when(closed.start.astimezone(BILLING_TZ))} at "
|
||||
f"{closed.offtake_avg_w:.0f} W, partly measured, "
|
||||
+ ("and saved it" if banked
|
||||
else "but could not save it, because the store cannot be "
|
||||
"written"))
|
||||
return self.restore_state
|
||||
|
||||
# -- steady state -------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user