From 13cdfa79b999584d0948ab6e982fdf34e4ece92f Mon Sep 17 00:00:00 2001 From: stroblme Date: Sat, 22 Aug 2026 16:40:44 +0200 Subject: [PATCH] power: the Cerbo keepalive sends an empty payload, not the time MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit An inject left to itself emits the current time. That went into a port declared `str`, the node raised on every tick, and after five the supervisor quarantined the whole `power` flow — which is every Victron reading in the house. It was invisible because Node-RED is still sending its own keepalive, so the Cerbo kept publishing anyway; the first thing to notice would have been the data stopping some minutes after Node-RED did. The preflight now checks what an inject emits against the port that receives it. Nothing else could: an inject has no source to run, and both halves of the declaration agreed with each other while disagreeing with reality. Co-Authored-By: Claude Opus 5 (1M context) --- scripts/tinyhouse/__main__.py | 31 +++++++++++++++++++++++++++++++ scripts/tinyhouse/sensing.py | 5 ++++- 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/scripts/tinyhouse/__main__.py b/scripts/tinyhouse/__main__.py index c88ed71..e3d4116 100644 --- a/scripts/tinyhouse/__main__.py +++ b/scripts/tinyhouse/__main__.py @@ -110,6 +110,7 @@ def check(flows: list[Flow]) -> list[str]: problems += _check_widgets(known) problems += _check_sources(flows) problems += _check_schemas(flows) + problems += _check_injects(flows) return problems @@ -308,6 +309,36 @@ def _rejects(spec: dict[str, Any], sample: Any) -> str: return "" +def _check_injects(flows: list[Flow]) -> list[str]: + """What an inject emits has to be what its port declared. + + An inject left to itself emits the current time, so a port expecting + anything but a number gets a float and the node raises — five times, and + the supervisor quarantines the flow. Nothing else catches it: an inject is + not a Python node, so it has no source to run, and both sides of the + declaration agree with each other while disagreeing with reality. + """ + problems = [] + for flow in flows: + for node in flow.nodes: + if node["type"] != "inject": + continue + params = node.get("params", {}) + payloads = dict(params.get("payloads") or {}) + for spec in node.get("provides", []): + port = spec.get("port") or spec["name"] + if port in payloads: + value = payloads[port] + elif "payload" in params: + value = params["payload"] + else: + value = 0.0 # the current time, which is what it defaults to + bad = _rejects(spec, value) + if bad: + problems.append(f"{flow.name}.{node['id']} emits: {bad}") + return problems + + def _check_schemas(flows: list[Flow]) -> list[str]: """Hand every node to the engine's own models before the API sees them. diff --git a/scripts/tinyhouse/sensing.py b/scripts/tinyhouse/sensing.py index 8bb9e79..3503577 100644 --- a/scripts/tinyhouse/sensing.py +++ b/scripts/tinyhouse/sensing.py @@ -165,7 +165,10 @@ def power(h: dict[str, Any]) -> Flow: "id": "keepalive_tick", "type": "inject", "title": "Every 30 seconds", - "params": {"interval": 30, "at_start": True}, + # An empty payload, as the reference sends: the Cerbo only wants + # to know somebody is listening. Left to itself an inject emits + # the time, which is a float going into a string port. + "params": {"interval": 30, "at_start": True, "payload": ""}, "provides": [{"name": "keepalive", "dtype": "str"}], } )