From e0f5ddc3a41f4565b71e21a965d218cce8136e9b Mon Sep 17 00:00:00 2001 From: Melvin Strobl Date: Thu, 29 Oct 2026 21:58:38 +0100 Subject: [PATCH] Devices may publish to their own out ports' messages (a level set on the device itself) --- backend/fluksio/api/deps.py | 14 +++++++++----- backend/tests/api/routes/test_devices.py | 11 ++++++----- docs/interface/dashboards.md | 4 +++- 3 files changed, 18 insertions(+), 11 deletions(-) diff --git a/backend/fluksio/api/deps.py b/backend/fluksio/api/deps.py index a70f801..69a1f67 100644 --- a/backend/fluksio/api/deps.py +++ b/backend/fluksio/api/deps.py @@ -139,15 +139,19 @@ def _device_writable(device: DeviceDef, request: Request) -> set[str]: one is what entitles the screen to it. Reads are untouched — read-only is not blind, and a locked dashboard has to keep drawing live data. - The device's own ``in`` ports, as flows wire them, are added outside the - lock: it covers the controls on a dashboard, not the sensors on the - device. Its ``out`` ports are read, not written, unless a widget targets - them too. + The device's own ports, as flows wire them, are added outside the lock: + it covers the controls on a dashboard, not the device itself. Its ``in`` + ports are its sensors; its ``out`` ports it publishes when set on the + device (a panel's brightness on its config page), so what follows the + message hears of it. """ store: DashboardStore | None = getattr(request.app.state, "dashboard_store", None) if store is None: return set() - writable = _device_in(device, request) + owned: DeviceStore | None = getattr(request.app.state, "device_store", None) + writable = ( + {b.message for b in owned.bound(device.id)} if owned is not None else set() + ) defns = devices.dashboards_for(device, store) bound = sorted( {s.message for d in defns if (s := d.settings.get("locked")) and s.message} diff --git a/backend/tests/api/routes/test_devices.py b/backend/tests/api/routes/test_devices.py index d8cca87..d2199c8 100644 --- a/backend/tests/api/routes/test_devices.py +++ b/backend/tests/api/routes/test_devices.py @@ -639,10 +639,11 @@ def test_a_devices_own_ports_publish_through_a_lock( ) -> None: """An ``in`` port is the device's own sensor, which a dashboard lock is not about. - An ``out`` port is the device following a message, so it reads and does - not write, and only an ``in`` port's name opens the artifact upload. A - value published on a bound port is the device node's, whatever the device - says it is from. + An ``out`` port is the device following a message, which it publishes too + when set on the device itself (a panel's brightness on its config page). + Only an ``in`` port's name opens the artifact upload. A value published on + a bound ``in`` port is the device node's, whatever the device says it is + from. """ _dashboard_with(client, superuser_token_headers, "panel_io_locked", PANEL_WIDGETS) _publish_settings( @@ -690,7 +691,7 @@ def test_a_devices_own_ports_publish_through_a_lock( client.post( f"{messages}/porch.backlight", headers=panel_headers, json={"value": 50} ).status_code - == 403 + == 200 ) assert ( client.get( diff --git a/docs/interface/dashboards.md b/docs/interface/dashboards.md index 1bbee82..5034a41 100644 --- a/docs/interface/dashboards.md +++ b/docs/interface/dashboards.md @@ -550,7 +550,9 @@ The credential may publish to its `in` messages even when a dashboard is locked, and may upload an artifact named after one (`PUT /artifacts?name=`), which is how a microphone sends a recording. A value it publishes there is credited to its device node, so the node's edge -lights up on the canvas. `out` messages are read only. The node is shown +lights up on the canvas. It may publish to its `out` messages as well, for a +level set on the device itself (a panel's brightness on its config page), so +what follows the message hears of it; no upload there. The node is shown degraded, *Offline*, in amber while the device is not connected: a warning on Home, not a fault, since a panel switched off or asleep breaks nothing.