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
118 lines
4.0 KiB
Python
118 lines
4.0 KiB
Python
"""A device node: never run by the engine, degraded while its device is away."""
|
|
|
|
import asyncio
|
|
from pathlib import Path
|
|
|
|
from fluksio.flow.controller import FlowController
|
|
from fluksio.flow.devices import presence
|
|
from fluksio.flow.messages import DType, MessageSpec
|
|
from fluksio.flow.schemas import FlowDef, FlowInput, NodeDef
|
|
from fluksio.flow.store import FlowStore
|
|
|
|
|
|
def _controller(tmp_path: Path) -> FlowController:
|
|
store = FlowStore(tmp_path / "flows")
|
|
store.write_flow(
|
|
FlowDef(
|
|
name="hall",
|
|
inputs=[
|
|
FlowInput(spec=MessageSpec(name="level", dtype=DType.INT), initial=50)
|
|
],
|
|
nodes=[
|
|
NodeDef(
|
|
id="panel",
|
|
type="device",
|
|
params={"device": "wall"},
|
|
provides=[
|
|
MessageSpec(name="bell", port="gpio.4", dtype=DType.BOOL)
|
|
],
|
|
requires=[
|
|
MessageSpec(
|
|
name="level",
|
|
port="backlight",
|
|
dtype=DType.INT,
|
|
trigger=False,
|
|
)
|
|
],
|
|
),
|
|
NodeDef(
|
|
id="ring",
|
|
type="change",
|
|
requires=[MessageSpec(name="bell", dtype=DType.BOOL)],
|
|
provides=[MessageSpec(name="rung", dtype=DType.BOOL)],
|
|
),
|
|
],
|
|
)
|
|
)
|
|
return FlowController(store)
|
|
|
|
|
|
def test_the_engine_never_runs_a_device_node(tmp_path: Path) -> None:
|
|
controller = _controller(tmp_path)
|
|
asyncio.run(controller.reload())
|
|
assert controller.issues == []
|
|
node = controller.get_node("hall.panel")
|
|
assert node is not None and node.passive
|
|
ran: list[object] = []
|
|
node.f = lambda **kwargs: ran.append(kwargs) # type: ignore[method-assign]
|
|
|
|
# The message its out port follows changes, and a manual run of the flow.
|
|
controller.publish_message("hall.level", 80)
|
|
controller.run_flow("hall")
|
|
assert ran == []
|
|
|
|
# What the device publishes on its in port runs what reads it.
|
|
controller.publish_message("hall.bell", True)
|
|
assert controller.values("hall")["hall.rung"]["value"] is True
|
|
asyncio.run(controller._teardown())
|
|
|
|
|
|
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]]:
|
|
await controller.reload()
|
|
entry = controller.loaded["hall.panel"]
|
|
seen = [(entry.health, entry.health_detail)]
|
|
presence.opened("wall")
|
|
seen.append((entry.health, entry.health_detail))
|
|
presence.closed("wall")
|
|
seen.append((entry.health, entry.health_detail))
|
|
await controller._teardown()
|
|
# Stopped, it no longer follows the device.
|
|
presence.opened("wall")
|
|
seen.append((entry.health, entry.health_detail))
|
|
presence.closed("wall")
|
|
return seen
|
|
|
|
# Degraded, not down: a switched-off panel is a warning, never a fault.
|
|
assert asyncio.run(scenario()) == [
|
|
("degraded", "Offline"),
|
|
("ok", None),
|
|
("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)],
|
|
]
|