Connection nodes speak of listener and sender, and of send_queue_size
mqtt and http take websocket's vocabulary: their Mode is LISTENER/SENDER, default names read mqtt_listener_… and http_listener_…, and mqtt's publish_queue_size is send_queue_size. No alias stays behind, so the flow store rewrites the old setting in every published and draft mqtt node when it opens, in the same migration as trigger → debounce. Every mqtt and http setting carries a one-line description for the editor, and the node-types reference uses one wording for direction and says that an http sender publishes a JSON object response on its outputs.
This commit is contained in:
@@ -19,7 +19,7 @@ from fluksio.flow.store import FlowStore
|
||||
BROKER = {"broker_host": "mosquitto", "broker_port": 1883, "topic": "sensors/temp"}
|
||||
|
||||
|
||||
def subscriber(node_id: str) -> NodeDef:
|
||||
def listener(node_id: str) -> NodeDef:
|
||||
return NodeDef(
|
||||
id=node_id,
|
||||
type="mqtt",
|
||||
@@ -35,7 +35,7 @@ def controller(tmp_path: Path) -> FlowController:
|
||||
FlowDef(
|
||||
name="house",
|
||||
nodes=[
|
||||
subscriber("sensor"),
|
||||
listener("sensor"),
|
||||
NodeDef(
|
||||
id="scale",
|
||||
type="change",
|
||||
@@ -45,7 +45,7 @@ def controller(tmp_path: Path) -> FlowController:
|
||||
],
|
||||
)
|
||||
)
|
||||
store.write_flow(FlowDef(name="shed", nodes=[subscriber("sensor")]))
|
||||
store.write_flow(FlowDef(name="shed", nodes=[listener("sensor")]))
|
||||
return FlowController(store)
|
||||
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ def test_a_filter_does_not_cover_a_neighbouring_topic():
|
||||
assert not topic_matches("sensors/living/temp", "sensors/living")
|
||||
|
||||
|
||||
def _subscriber(topic, **params) -> MqttNode:
|
||||
def _listener(topic, **params) -> MqttNode:
|
||||
node = MqttNode(
|
||||
provides=[MessageSpec(name="reading", port="reading", dtype=DType.FLOAT)],
|
||||
params={"topic": {"reading": topic}, **params},
|
||||
@@ -40,7 +40,7 @@ def _subscriber(topic, **params) -> MqttNode:
|
||||
|
||||
def test_a_wildcard_subscription_routes_to_its_port():
|
||||
"""It used to subscribe and then drop every message it was sent."""
|
||||
node = _subscriber("shellypm/status/#")
|
||||
node = _listener("shellypm/status/#")
|
||||
assert node._ports_for("shellypm/status/pm1:0") == ["reading"]
|
||||
assert node._ports_for("shellypmc/status/pm1:0") == []
|
||||
|
||||
@@ -63,12 +63,12 @@ def test_an_exact_topic_still_wins_over_a_filter():
|
||||
|
||||
def test_json_key_lifts_the_value_a_device_wraps():
|
||||
"""Victron publishes {"value": 47} on every one of its topics."""
|
||||
node = _subscriber("N/x/battery/Soc", json_key="value")
|
||||
node = _listener("N/x/battery/Soc", json_key="value")
|
||||
assert node.json_keys == {"reading": "value"}
|
||||
|
||||
|
||||
def test_without_json_key_the_port_name_is_the_key_as_before():
|
||||
node = _subscriber("sensors/temp")
|
||||
node = _listener("sensors/temp")
|
||||
assert node.json_keys == {}
|
||||
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ def test_http_senders_share_one_pooled_client():
|
||||
close_shared_client()
|
||||
|
||||
|
||||
def _publisher() -> MqttNode:
|
||||
def _sender() -> MqttNode:
|
||||
node = MqttNode(
|
||||
requires=[MessageSpec(name="setpoint", port="setpoint", dtype=DType.FLOAT)],
|
||||
params={"topic": {"setpoint": "heating/setpoint"}},
|
||||
@@ -29,27 +29,27 @@ def _publisher() -> MqttNode:
|
||||
return node
|
||||
|
||||
|
||||
def test_a_started_publisher_queues_instead_of_connecting():
|
||||
def test_a_started_sender_queues_instead_of_connecting():
|
||||
"""The handler runs on a worker thread; it must not block on the broker."""
|
||||
node = _publisher()
|
||||
node = _sender()
|
||||
|
||||
async def scenario() -> None:
|
||||
node._publish_queue = asyncio.Queue(maxsize=4)
|
||||
node._send_queue = asyncio.Queue(maxsize=4)
|
||||
node._loop = asyncio.get_running_loop()
|
||||
|
||||
await asyncio.to_thread(node._publisher_handler, {}, setpoint=21.0)
|
||||
await asyncio.to_thread(node._sender_handler, {}, setpoint=21.0)
|
||||
# call_soon_threadsafe lands on the next loop pass.
|
||||
await asyncio.sleep(0)
|
||||
|
||||
assert node._publish_queue.qsize() == 1
|
||||
assert node._publish_queue.get_nowait() == {"setpoint": 21.0}
|
||||
assert node._send_queue.qsize() == 1
|
||||
assert node._send_queue.get_nowait() == {"setpoint": 21.0}
|
||||
|
||||
asyncio.run(scenario())
|
||||
|
||||
|
||||
def test_a_full_publish_queue_drops_the_oldest():
|
||||
def test_a_full_send_queue_drops_the_oldest():
|
||||
"""A broker that cannot keep up must not grow the queue without bound."""
|
||||
node = _publisher()
|
||||
node = _sender()
|
||||
health: list[tuple[str, str | None]] = []
|
||||
node._on_health = lambda _n, status, detail: health.append((status, detail))
|
||||
|
||||
@@ -61,7 +61,7 @@ def test_a_full_publish_queue_drops_the_oldest():
|
||||
assert queue.qsize() == 2
|
||||
assert queue.get_nowait() == {"setpoint": 2.0}
|
||||
assert queue.get_nowait() == {"setpoint": 3.0}
|
||||
assert health == [("degraded", "publish queue full")]
|
||||
assert health == [("degraded", "send queue full")]
|
||||
|
||||
asyncio.run(scenario())
|
||||
|
||||
@@ -121,7 +121,7 @@ def test_the_configured_timeout_reaches_the_broker_client(monkeypatch):
|
||||
assert seen["timeout"] == 2.5
|
||||
|
||||
|
||||
def test_the_configured_backlog_reaches_the_publish_queue(monkeypatch):
|
||||
def test_the_configured_backlog_reaches_the_send_queue(monkeypatch):
|
||||
"""The depth is read when the queue is built, so it has to be per node."""
|
||||
import aiomqtt
|
||||
|
||||
@@ -139,14 +139,14 @@ def test_the_configured_backlog_reaches_the_publish_queue(monkeypatch):
|
||||
|
||||
node = MqttNode(
|
||||
requires=[MessageSpec(name="setpoint", port="setpoint", dtype=DType.FLOAT)],
|
||||
params={"topic": {"setpoint": "heating/setpoint"}, "publish_queue_size": 8},
|
||||
params={"topic": {"setpoint": "heating/setpoint"}, "send_queue_size": 8},
|
||||
)
|
||||
node.assign_flow("heating", "out")
|
||||
|
||||
async def scenario() -> int:
|
||||
await node.start_publisher()
|
||||
assert node._publish_queue is not None
|
||||
size = node._publish_queue.maxsize
|
||||
assert node._send_queue is not None
|
||||
size = node._send_queue.maxsize
|
||||
await node.stop_publisher()
|
||||
return size
|
||||
|
||||
|
||||
@@ -159,3 +159,17 @@ def test_opening_the_store_renames_trigger_nodes_to_debounce(store: FlowStore):
|
||||
|
||||
FlowStore(store.root)
|
||||
assert commit_count(store) == commits + 1
|
||||
|
||||
|
||||
def test_opening_the_store_renames_the_mqtt_publish_queue_size(store: FlowStore):
|
||||
"""The mqtt setting is now `send_queue_size`, with no alias for the old name."""
|
||||
relay = NodeDef(id="relay", type="mqtt", params={"publish_queue_size": 8})
|
||||
store.write_flow(FlowDef(name="hall", nodes=[relay]))
|
||||
commits = commit_count(store)
|
||||
|
||||
reopened = FlowStore(store.root)
|
||||
|
||||
published = reopened.read_flow("hall")
|
||||
assert published.nodes[0].params == {"send_queue_size": 8}
|
||||
assert published.version == 2
|
||||
assert commit_count(store) == commits + 1
|
||||
|
||||
Reference in New Issue
Block a user