UX-13 P2: the Installer settings view
- Settings tab: the Installer view (app/static/settings.js), drawn from
app/uilayout.py through the new app/uipage.py: search, six collapsible
groups with "n to fix / n to check / n changed" badges and an "applies
after a restart" chip, radio cards, segmented choices, sliders with units
(the peak limit in kW), entity pickers with search and the live reading,
inverter cards, solar rows, locked measured values behind "Change", and
the add-on's refusals, warnings and info beside the field they name.
- Save path: the page posts its model to the existing config/validate and
config/save; uimodel turns it into options before the shipped validator,
so nothing it refuses is saved. No route added. A value equal to the
stored one is written as stored, so a save that changed nothing writes an
identical blob. Save bar, then the restart dialog that names 0 W.
- The region choice shows its preset year; Wallonia and Brussels are listed
as having no preset yet (TARIFF-22). "Classic settings" keeps /config.
- P1 test findings: (a) "Going to 0 W" until the last accepted power request
is 0 W; (b) the trade blocks and the price line name the feed's own points
("Lowest published price"), the hour bars say "hour average"; (c) the entity
list's WARNING gives a reason on a timeout.
- test_settings.py; test_home, test_haentities, test_uilayout extended.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LyR88rXETvwhTgx6yHE39p
This commit is contained in:
@@ -21,11 +21,23 @@ from .price import FLEMISH_TAXES_COMPONENTS_EUR_KWH
|
||||
from .static_tariff import ProfileInvalid, parse_static_profile
|
||||
|
||||
__all__ = ["options_to_ui", "ui_to_options", "REGION_PRESETS",
|
||||
"arbitrage_from_batteries", "DeriveError"]
|
||||
"arbitrage_from_batteries", "DeriveError", "COUNT", "GROWN"]
|
||||
|
||||
|
||||
class DeriveError(ValueError):
|
||||
"""The page model asks for something no options blob can say."""
|
||||
"""The page model asks for something no options blob can say. `code` names
|
||||
the sentence the page shows (app/ui_text `derive.<code>`), and `field` the
|
||||
page field it is shown beside."""
|
||||
|
||||
def __init__(self, text, code="", field=""):
|
||||
super().__init__(text)
|
||||
self.code, self.field = code, field
|
||||
|
||||
|
||||
# The page model's inverter count, which show conditions read, and what adding
|
||||
# a card sets: a second inverter turns the one flat inverter into a list.
|
||||
COUNT = "inverter_count"
|
||||
GROWN = {"inverter_shape": "list"}
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
@@ -80,13 +92,16 @@ def _roster_encode(m, keep):
|
||||
out = {}
|
||||
shape, cards = m.get("inverter_shape"), m.get("inverters")
|
||||
if shape == "raw":
|
||||
raise DeriveError("the inverter list is edited in the Configuration tab")
|
||||
raise DeriveError("the inverter list is edited in the Configuration tab",
|
||||
"raw_roster", "inverters")
|
||||
if not cards:
|
||||
raise DeriveError("at least one inverter is needed")
|
||||
raise DeriveError("at least one inverter is needed", "no_inverter",
|
||||
"inverters")
|
||||
if shape == "flat" and len(cards) == 1:
|
||||
card = cards[0]
|
||||
if not _blank(card.get("name")) or not _blank(card.get("capacity_kwh")):
|
||||
raise DeriveError("one inverter stored flat has no name or capacity")
|
||||
raise DeriveError("one inverter stored flat has no name or "
|
||||
"capacity", "flat_named", "inverters")
|
||||
for k in _ROSTER_FLAT:
|
||||
if card.get(k) is not None or k in keep:
|
||||
out[k] = card.get(k)
|
||||
@@ -96,8 +111,9 @@ def _roster_encode(m, keep):
|
||||
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.
|
||||
if shape == "flat" or _roster_decode(keep)["inverter_shape"] == "flat":
|
||||
# CFG-04 rule 9e: a flat power request entity beside a list is refused,
|
||||
# so a list that replaces the flat inverter clears it.
|
||||
out["setpoint_entity"] = ""
|
||||
return out
|
||||
|
||||
@@ -210,7 +226,8 @@ def _tariff_encode(m, keep):
|
||||
contract = m.get("ui.contract")
|
||||
if contract == "dynamic":
|
||||
if _blank(m.get("price_provider")) or m.get("price_provider") == "off":
|
||||
raise DeriveError("a dynamic contract needs a price source")
|
||||
raise DeriveError("a dynamic contract needs a price source",
|
||||
"no_source", "price_provider")
|
||||
out["price_provider"] = m["price_provider"]
|
||||
kind = m.get("ui.fallback")
|
||||
else:
|
||||
@@ -226,17 +243,19 @@ def _tariff_encode(m, keep):
|
||||
elif kind == "custom":
|
||||
if "static_profile" not in keep:
|
||||
raise DeriveError("odd tariff windows are edited in the "
|
||||
"Configuration tab")
|
||||
"Configuration tab", "custom_profile",
|
||||
"ui.contract")
|
||||
out["static_profile"] = keep["static_profile"]
|
||||
else:
|
||||
raise DeriveError(f"unknown contract {kind!r}")
|
||||
raise DeriveError(f"unknown contract {kind!r}", "unknown",
|
||||
"ui.contract")
|
||||
return out
|
||||
|
||||
|
||||
def _price_of(m, name):
|
||||
value = _num(m.get(name))
|
||||
if value is None:
|
||||
raise DeriveError(f"{name} needs a price")
|
||||
raise DeriveError(f"{name} needs a price", "no_price", name)
|
||||
return value
|
||||
|
||||
|
||||
@@ -259,7 +278,7 @@ def _region_encode(m, keep):
|
||||
out["price_taxes_profile"] = keep["price_taxes_profile"]
|
||||
return out
|
||||
if region not in REGION_PRESETS:
|
||||
raise DeriveError(f"unknown region {region!r}")
|
||||
raise DeriveError(f"unknown region {region!r}", "unknown", "ui.region")
|
||||
p = REGION_PRESETS[region]
|
||||
out = {"price_taxes_eur_kwh": p["taxes_eur_kwh"]}
|
||||
if p["taxes_profile"] or "price_taxes_profile" in keep:
|
||||
|
||||
Reference in New Issue
Block a user