- flow/nodes/device.py: `device` node with params `device` and `config`;
provides the device's `in` ports, requires its `out` ports without
waking anything, never run (Node.passive), down "Offline" from presence
- DeviceDef.io and DevicePort go; GET /devices/{id} computes `io` and
`bindings` from the device nodes of published flows, in a fixed order,
cached by FlowStore.revision; the credential's scope comes from them
- issues `unknown_device` and `device_port` (wired twice; against a report
the wrong direction or dtype, or unlisted as advisory), rechecked when a
device is created or deleted; ValidationIssue.advisory is a field
- a device publishing on a bound port is credited to its node by the
server; the panel endpoint kind is gone from `_endpoints` and the canvas
- a rebuild that moves a device's bindings sends `dashboard_changed` with
an empty name, which the firmware refetches on
- startup moves each device's `io` rows into device nodes in the flows
declaring their messages, draft and published, one commit; rows no
flow declares are logged and left out
- frontend: Device node settings (device, Publishes, Follows, a port
picker from a report), palette shortcuts per reported port, DeviceDetail
lists bindings with "Use in flow" in place of the I/O editor
- docs: devices, flow editor, node types, issues, API
95 lines
3.2 KiB
Python
95 lines
3.2 KiB
Python
"""A device node: never run by the engine, down 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_down_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
|
|
|
|
assert asyncio.run(scenario()) == [
|
|
("down", "Offline"),
|
|
("ok", None),
|
|
("down", "Offline"),
|
|
("down", "Offline"),
|
|
]
|
|
presence.forget("wall")
|