Flow canvas draws panel I/O ports as panel:<id>:<port> endpoints that open their panel in the Panels dialog
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WgBfkm4rCmtd1T4vHWjEPx
This commit is contained in:
@@ -38,7 +38,7 @@ from fluksio.flow.controller import NODE_TYPES, FlowController
|
||||
from fluksio.flow.dashboards import DashboardStore
|
||||
from fluksio.flow.events import event_bus
|
||||
from fluksio.flow.messages import flow_of, qualify
|
||||
from fluksio.flow.panels import messages_for
|
||||
from fluksio.flow.panels import messages_for, read_config
|
||||
from fluksio.flow.pipeline import ValidationIssue
|
||||
from fluksio.flow.runs import RunRejected
|
||||
from fluksio.flow.schemas import (
|
||||
@@ -78,18 +78,19 @@ ws_router = APIRouter(prefix="/flows", tags=["flows"])
|
||||
class Endpoint(BaseModel):
|
||||
"""Something wired into this flow that is not a node in it.
|
||||
|
||||
A dashboard control setting one of its messages, a tile showing one, or a
|
||||
node in another flow on the far side of a dotted name. The canvas draws
|
||||
these so a value never appears to come from nowhere — or worse, appears to
|
||||
come from whichever node happens to be drawn as a producer.
|
||||
A dashboard control setting one of its messages, a tile showing one, a
|
||||
port on a panel's own hardware, or a node in another flow on the far side
|
||||
of a dotted name. The canvas draws these so a value never appears to come
|
||||
from nowhere — or worse, appears to come from whichever node happens to be
|
||||
drawn as a producer.
|
||||
"""
|
||||
|
||||
#: dashboard or flow.
|
||||
#: dashboard, panel or flow.
|
||||
kind: str
|
||||
#: Stable within its kind, and used as the canvas node id.
|
||||
id: str
|
||||
label: str
|
||||
#: What sort of widget, or the node type in the other flow.
|
||||
#: What sort of widget, the panel, or the node type in the other flow.
|
||||
detail: str = ""
|
||||
#: Messages of this flow it publishes, and ones it reads.
|
||||
provides: list[str] = []
|
||||
@@ -155,6 +156,28 @@ def _endpoints(controller: FlowController, definition: FlowDef) -> list[Endpoint
|
||||
requires=binding["requires"],
|
||||
)
|
||||
|
||||
# A panel's own hardware: an ``in`` port publishes like a control, an
|
||||
# ``out`` port reads like a tile. The id is the source id the device
|
||||
# publishes under, so its edges pulse for its values.
|
||||
prefix = f"{flow}."
|
||||
for panel in read_config().panels:
|
||||
for port in panel.io:
|
||||
if not port.message.startswith(prefix):
|
||||
continue
|
||||
key = f"panel:{panel.id}:{port.port}"
|
||||
endpoint = found.setdefault(
|
||||
key,
|
||||
Endpoint(
|
||||
kind="panel",
|
||||
id=key,
|
||||
label=port.port,
|
||||
detail=panel.title or panel.id,
|
||||
),
|
||||
)
|
||||
side = endpoint.provides if port.direction == "in" else endpoint.requires
|
||||
if port.message not in side:
|
||||
side.append(port.message)
|
||||
|
||||
def _flow_endpoint(key: str, detail: str) -> Endpoint:
|
||||
"""One label per node on the far side, however many names reach it."""
|
||||
return found.setdefault(
|
||||
|
||||
@@ -10,8 +10,10 @@ from pathlib import Path
|
||||
import pytest
|
||||
|
||||
from fluksio.api.routes.flows import _endpoints
|
||||
from fluksio.core.config import settings
|
||||
from fluksio.flow.controller import FlowController
|
||||
from fluksio.flow.messages import DType, MessageSpec
|
||||
from fluksio.flow.panels import PanelDef, PanelPort, PanelsConfig, write_config
|
||||
from fluksio.flow.schemas import FlowDef, NodeDef
|
||||
from fluksio.flow.store import FlowStore
|
||||
|
||||
@@ -109,3 +111,35 @@ def test_the_wiring_index_is_read_once_per_store_revision(controller):
|
||||
controller.store.write_flow(FlowDef(name="shed", nodes=[]))
|
||||
_endpoints(controller, controller.store.read_flow("house"))
|
||||
assert reads == 2
|
||||
|
||||
|
||||
def test_a_panel_port_is_drawn_on_the_side_it_wires(controller, tmp_path, monkeypatch):
|
||||
monkeypatch.setattr(settings, "PANELS_FILE", tmp_path / "panels.json")
|
||||
write_config(
|
||||
PanelsConfig(
|
||||
panels=[
|
||||
PanelDef(
|
||||
id="hall",
|
||||
title="Hall",
|
||||
io=[
|
||||
PanelPort(port="gpio.4", message="house.temp", direction="in"),
|
||||
PanelPort(
|
||||
port="backlight", message="house.temp", direction="out"
|
||||
),
|
||||
PanelPort(port="mic", message="garage.open", direction="in"),
|
||||
],
|
||||
)
|
||||
]
|
||||
)
|
||||
)
|
||||
|
||||
endpoints = {
|
||||
one.id: one
|
||||
for one in _endpoints(controller, controller.store.read_flow("house"))
|
||||
}
|
||||
|
||||
# The id is what the device publishes under, so its edges pulse for it.
|
||||
assert endpoints["panel:hall:gpio.4"].provides == ["house.temp"]
|
||||
assert endpoints["panel:hall:backlight"].requires == ["house.temp"]
|
||||
assert endpoints["panel:hall:backlight"].detail == "Hall"
|
||||
assert "panel:hall:mic" not in endpoints
|
||||
|
||||
Reference in New Issue
Block a user