Let a connector write, and publish strings bare
Playwright Tests / test-playwright (1, 2) (push) Canceled after 0s
Playwright Tests / test-playwright (2, 2) (push) Canceled after 0s
pre-commit / pre-commit (push) Canceled after 0s
Compose Smoke Test / test-compose (push) Canceled after 0s
Playwright Tests / merge-reports (push) Canceled after 0s

Two things stopped the engine commanding this house. ConnectorNode hardwired
its node function to a no-op, so an input message reaching a connector was
discarded and Art-Net's packet builder was unreachable; write() now carries
the input ports, which is additive so the contract version holds. And the MQTT
publisher JSON-encoded every payload, so "ON" went on the wire quoted and the
devices on a shared broker, which speak bare values, ignored it.

seed_house_control.py is the rig: a flow that drives the washing machine plug,
a dimmer and a colour fixture over MQTT, carries the same two as DMX on an
Art-Net node with transmit still off, and a dashboard to drive it by hand.
This commit is contained in:
2026-08-20 21:57:27 +02:00
parent 06875a77ac
commit f88dcf81c0
8 changed files with 575 additions and 12 deletions
+28
View File
@@ -100,6 +100,34 @@ def test_a_failing_poll_reports_down_and_keeps_going():
assert health[-1][0] == "ok"
class Actuator(ConnectorNode):
"""A connector that commands something instead of reading it."""
contract = CONTRACT_VERSION
title = "Test actuator"
def __init__(self, **kwargs: Any) -> None:
super().__init__(**kwargs)
self.commands: list[dict[str, Any]] = []
def write(self, **ports: Any) -> None:
self.commands.append(ports)
return None
def test_an_incoming_message_reaches_a_connector_that_writes():
node = Actuator(requires=[MessageSpec(name="level", dtype=DType.INT)])
node.assign_flow("demo", "actuator")
assert node.execute({"demo.level": 255}) is None
assert node.commands == [{"level": 255}]
def test_a_read_only_connector_ignores_what_reaches_it():
node = a_sensor([])
assert node.execute({}) is None
def test_a_credential_param_is_marked_for_the_editor():
schema = Sensor.Params.model_json_schema()
assert schema["properties"]["poll_interval"]["default"] == 0
+25
View File
@@ -64,3 +64,28 @@ def test_a_full_publish_queue_drops_the_oldest():
assert health == [("degraded", "publish queue full")]
asyncio.run(scenario())
def test_a_string_goes_on_the_wire_bare():
"""Devices on a shared broker expect `ON`, not `"ON"`."""
class Recorder:
def __init__(self) -> None:
self.published: list[tuple[str, str]] = []
async def publish(self, topic, payload, **_):
self.published.append((topic, payload))
node = MqttNode(
requires=[
MessageSpec(name="plug", port="plug", dtype=DType.STR),
MessageSpec(name="level", port="level", dtype=DType.INT),
],
params={"topic": {"plug": "actor/plug", "level": "light/level"}},
)
node.assign_flow("house", "out")
client = Recorder()
asyncio.run(node._publish_with(client, {"plug": "ON", "level": 60}))
assert client.published == [("actor/plug", "ON"), ("light/level", "60")]