"""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")