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:
Claude
2026-09-28 02:49:58 +02:00
parent a36dca60dd
commit c9ab48d6fc
18 changed files with 1871 additions and 95 deletions
+21
View File
@@ -10,6 +10,7 @@ Mutating a module this file grades? Read MUTATION.md first (purge __pycache__).
import asyncio
import json
import logging
import sys
import time
@@ -96,9 +97,29 @@ for name, handler in (("502 while HA restarts", bad_gateway),
("a body that is not JSON", garbage),
("JSON that is not a list", not_a_list)):
check(f"{name} -> None", asyncio.run(read_with(handler)) is None)
class _Lines(logging.Handler):
def __init__(self):
super().__init__()
self.lines = []
def emit(self, record):
if record.levelno >= logging.WARNING:
self.lines.append(record.getMessage())
_lines = _Lines()
logging.getLogger().addHandler(_lines)
check("an answer slower than the timeout -> None, at the timeout",
asyncio.run(read_with(slow, timeout_s=0.3)) is None
and read_with.took < 1.0)
# UX-13 P1 test finding (c): a timeout's own text is empty, which left
# "Reading the entity list failed: " with no reason.
check("...and its WARNING line gives the reason, not an empty one "
f"({_lines.lines})", any(
x == "Reading the entity list failed: no answer within 0.3 seconds."
for x in _lines.lines)
and not any(x.rstrip().endswith("failed:") for x in _lines.lines))
logging.getLogger().removeHandler(_lines)
check("no server at all -> None", asyncio.run(closed_port()) is None)
check("the timeout for the list is the same 10 s as every other read",
"STATES_TIMEOUT_S = 10 " in open(H.__file__, encoding="utf-8").read())