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_widgets(known)
|
||||||
problems += _check_sources(flows)
|
problems += _check_sources(flows)
|
||||||
problems += _check_schemas(flows)
|
problems += _check_schemas(flows)
|
||||||
|
problems += _check_injects(flows)
|
||||||
return problems
|
return problems
|
||||||
|
|
||||||
|
|
||||||
@@ -308,6 +309,36 @@ def _rejects(spec: dict[str, Any], sample: Any) -> str:
|
|||||||
return ""
|
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]:
|
def _check_schemas(flows: list[Flow]) -> list[str]:
|
||||||
"""Hand every node to the engine's own models before the API sees them.
|
"""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",
|
"id": "keepalive_tick",
|
||||||
"type": "inject",
|
"type": "inject",
|
||||||
"title": "Every 30 seconds",
|
"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"}],
|
"provides": [{"name": "keepalive", "dtype": "str"}],
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user