A driver now says whether its release() hands the inverter back (Driver.hands_back; GoodWe is False, so release() stays write(0) and the Controller never calls it). command_site_release() sits next to command_site_zero(): the Stop button and shutdown() (so SIGTERM, restart, control task death and a late config refusal) hand a hand-back unit back, once, and zero every other unit as before. Safety stops and refusals still hold 0 W for every unit. Shadow mode releases a hand-back unit. After a release nothing is written to the unit (heartbeat and cycle included) until control starts again. release() is bounded by RELEASE_BUDGET_S (= the write budget), runs concurrently per unit, and a raising or hanging release on one unit does not stop the others. The "set to 0 W" sentences for Stop, restart, shadow mode and the stopped banner are built from the roster (app/handback.py, EN and NL, words from the driver); a GoodWe-only roster keeps every old literal. A handed-back unit shows "Back under <its control>" on Home. Existing test edited: test_messages.py (two classification entries for the CRITICAL line that moved into a roster builder, and the new release bound). Co-Authored-By: Claude <noreply@anthropic.com>
79 lines
2.4 KiB
Python
79 lines
2.4 KiB
Python
"""UX-13 P5: the page's words, app/ui_text/<lang>.json, in English or Dutch.
|
|
|
|
One file per language with the same keys. A key missing from a language falls
|
|
back to English, key by key, the way the Supervisor's translations do.
|
|
test_ui_text.py holds both files to STYLE.md and to each other.
|
|
"""
|
|
|
|
import copy
|
|
import io
|
|
import json
|
|
import os
|
|
|
|
DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "ui_text")
|
|
LANGS = ("en", "nl")
|
|
_CACHE: dict = {}
|
|
# SMA-10. The sentences that move with the roster (handback.overlay): empty for
|
|
# every GoodWe-only roster, and then `raw()` is the file as it always was.
|
|
_IDLE: dict = {}
|
|
_GEN = [0]
|
|
_MERGED: dict = {}
|
|
|
|
|
|
def lang_of(value) -> str:
|
|
""""nl", "nl-BE" or "nl_NL" -> "nl"; anything else -> "en"."""
|
|
head = str(value or "").strip().lower().replace("_", "-").split("-")[0]
|
|
return head if head in LANGS else "en"
|
|
|
|
|
|
def _file(lang: str) -> dict:
|
|
if lang not in _CACHE:
|
|
with io.open(os.path.join(DIR, f"{lang}.json"), encoding="utf-8") as f:
|
|
_CACHE[lang] = json.load(f)
|
|
return _CACHE[lang]
|
|
|
|
|
|
def set_idle(overlay: dict) -> None:
|
|
"""SMA-10: the roster-built sentences, `{lang: nested dict}` (or {})."""
|
|
_IDLE.clear()
|
|
_IDLE.update(overlay or {})
|
|
_MERGED.clear()
|
|
_GEN[0] += 1
|
|
|
|
|
|
def generation() -> int:
|
|
"""Changes whenever `set_idle()` does, so a cache of words can tell."""
|
|
return _GEN[0]
|
|
|
|
|
|
def raw(lang: str) -> dict:
|
|
"""One language file as written, without the English fallback (and with the
|
|
roster's own sentences laid over it when a unit is handed back)."""
|
|
if lang not in _IDLE:
|
|
return _file(lang)
|
|
if lang not in _MERGED:
|
|
_MERGED[lang] = _merge(_file(lang), _IDLE[lang])
|
|
return _MERGED[lang]
|
|
|
|
|
|
def _merge(base, over):
|
|
if not (isinstance(base, dict) and isinstance(over, dict)):
|
|
return copy.deepcopy(over)
|
|
out = copy.deepcopy(base)
|
|
for k, v in over.items():
|
|
out[k] = _merge(base.get(k), v) if k in base else copy.deepcopy(v)
|
|
return out
|
|
|
|
|
|
def words(lang="en") -> dict:
|
|
"""Every word in `lang`, English where a key is missing."""
|
|
lang = lang_of(lang)
|
|
return raw("en") if lang == "en" else _merge(raw("en"), raw(lang))
|
|
|
|
|
|
def say(template: str, **values) -> str:
|
|
"""Fill `{name}` placeholders, as settings.js `say()` does."""
|
|
for k, v in values.items():
|
|
template = template.replace("{" + k + "}", str(v))
|
|
return template
|