Capability report: PUT /devices/{id}/report, a Capabilities section, offline devices degraded

A device reports its board, screen, camera and ports from its own
credential (16 KB, schema 1, replacing the last report), and the flows
are validated against it again. The node panel turns a reported port's
config schema into a form, a reported screen is a canvas preset and a
touch screen's first report turns Touch friendly on. An offline device
node is degraded: amber on the canvas, a warning on Home, never a fault.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0138N7k5PT4dcR1iEqXaaQ9k
This commit is contained in:
Melvin Strobl
2026-10-16 17:37:49 +02:00
co-authored by Claude Opus 5.5
parent a9d453c9d9
commit ad76200d3d
28 changed files with 820 additions and 129 deletions
+28 -5
View File
@@ -1,4 +1,4 @@
"""A device node: never run by the engine, down while its device is away."""
"""A device node: never run by the engine, degraded while its device is away."""
import asyncio
from pathlib import Path
@@ -67,7 +67,7 @@ def test_the_engine_never_runs_a_device_node(tmp_path: Path) -> None:
asyncio.run(controller._teardown())
def test_a_device_node_is_down_while_its_device_is_away(tmp_path: Path) -> None:
def test_a_device_node_is_degraded_while_its_device_is_away(tmp_path: Path) -> None:
controller = _controller(tmp_path)
async def scenario() -> list[tuple[str, str | None]]:
@@ -85,10 +85,33 @@ def test_a_device_node_is_down_while_its_device_is_away(tmp_path: Path) -> None:
presence.closed("wall")
return seen
# Degraded, not down: a switched-off panel is a warning, never a fault.
assert asyncio.run(scenario()) == [
("down", "Offline"),
("degraded", "Offline"),
("ok", None),
("down", "Offline"),
("down", "Offline"),
("degraded", "Offline"),
("degraded", "Offline"),
]
presence.forget("wall")
def test_an_offline_device_is_a_warning_not_a_fault(tmp_path: Path) -> None:
controller = _controller(tmp_path)
async def scenario() -> list:
await controller.reload()
# Past the floor that holds back a device rebooting.
controller.loaded["hall.panel"].health_since -= 3600
found = (
controller.unhealthy_nodes(),
[e.id for e in controller.unhealthy_nodes(health="degraded")],
[(i.code, i.advisory) for i in controller.flow_issues("hall")],
)
await controller._teardown()
return list(found)
assert asyncio.run(scenario()) == [
[],
["hall.panel"],
[("node_degraded", True)],
]