power: the Cerbo keepalive sends an empty payload, not the time
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) <noreply@anthropic.com>
This commit is contained in:
@@ -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.
|
||||
|
||||
|
||||
@@ -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"}],
|
||||
}
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user