history: one Flux script per chart, and rows are json

Two things the installation found that the checks did not.

A script with three `from()` statements in it produces three results all
called `_result`, and InfluxDB refuses that outright — so the measurements go
into one filter and the rows come back tagged with which one they are.

And the answer a database node hands back holds a *list* of rows, which a
record may not: a record is flat scalars. It was declared one, so every chart
failed on the type check the moment a real answer arrived.

The second one is now caught before anything is pushed: the preflight runs
each sample shape past the port that would receive it, which is what turns
"expected record, got dict" from a runtime surprise into a line of output.

Also records the two engine faults this seeding session surfaced.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-22 16:24:38 +02:00
co-authored by Claude Opus 5
parent 8734e51ef1
commit 1b455b5e55
4 changed files with 112 additions and 32 deletions
+28 -23
View File
@@ -18,7 +18,13 @@ from typing import Any
from .api import Flow
from .sensing import _broker
BUILD = '''"""A chart's window into Flux. The database-specific half, and the only one."""
BUILD = '''"""A chart's window into Flux. The database-specific half, and the only one.
One script, not one per line: several `from()` statements in a single script
each produce a result called `_result`, and InfluxDB refuses a script that
names two the same. The measurements go into one filter instead, and the rows
come back tagged with which one they are.
"""
BUCKET = "{bucket}"
@@ -26,23 +32,19 @@ BUCKET = "{bucket}"
def process(chart_request, series=()):
span = int(chart_request["range_s"])
every = max(1, int(chart_request["interval_s"]))
parts = []
for measurement in series:
parts.append(
"\\n".join(
[
'from(bucket: "%s")' % BUCKET,
" |> range(start: -%ds)" % span,
' |> filter(fn: (r) => r["_measurement"] == "%s")' % measurement,
' |> filter(fn: (r) => r["_field"] == "value")',
" |> aggregateWindow(every: %ds, fn: mean, createEmpty: false)"
% every,
]
)
)
wanted = " or ".join('r["_measurement"] == "%s"' % m for m in series)
flux = "\\n".join(
[
'from(bucket: "%s")' % BUCKET,
" |> range(start: -%ds)" % span,
" |> filter(fn: (r) => %s)" % (wanted or "false"),
' |> filter(fn: (r) => r["_field"] == "value")',
" |> aggregateWindow(every: %ds, fn: mean, createEmpty: false)" % every,
]
)
return {
"query": {
"flux": "\\n".join(parts),
"flux": flux,
"range_s": chart_request["range_s"],
"interval_s": chart_request["interval_s"],
}
@@ -135,9 +137,9 @@ def history(h: dict[str, Any]) -> Flow:
"requires": [
{"name": f"{name}_query", "port": "query", "dtype": "record"}
],
"provides": [
{"name": f"{name}_rows", "port": "rows", "dtype": "record"}
],
# The answer holds a list of rows, and a record may only hold
# flat scalars.
"provides": [{"name": f"{name}_rows", "port": "rows", "dtype": "json"}],
}
)
flow.add(
@@ -146,9 +148,7 @@ def history(h: dict[str, Any]) -> Flow:
"type": "python",
"title": f"{name.title()}: rows to a series",
"params": {"labels": labels},
"requires": [
{"name": f"{name}_rows", "port": "rows", "dtype": "record"}
],
"requires": [{"name": f"{name}_rows", "port": "rows", "dtype": "json"}],
"provides": [
{"name": f"{name}_series", "port": "series", "dtype": "series"}
],
@@ -296,7 +296,12 @@ def kiosk(h: dict[str, Any]) -> Flow:
"dtype": "float",
"trigger": False,
},
{"name": "shutters.bed_down", "port": "bed_down", "dtype": "bool"},
{
"name": "shutters.bed_down",
"port": "bed_down",
"dtype": "bool",
"trigger": False,
},
],
"provides": [{"name": "brightness", "dtype": "int"}],
},