Capability report: PUT /devices/{id}/report, a Capabilities section, offline devices degraded
A device reports its board, screen, camera and ports from its own credential (16 KB, schema 1, replacing the last report), and the flows are validated against it again. The node panel turns a reported port's config schema into a form, a reported screen is a canvas preset and a touch screen's first report turns Touch friendly on. An offline device node is degraded: amber on the canvas, a warning on Home, never a fault.
This commit is contained in:
@@ -803,6 +803,98 @@ def test_a_port_wired_twice_or_an_unknown_device_is_an_issue(
|
||||
client.delete(f"{FLOWS}/{flow}", headers=superuser_token_headers)
|
||||
|
||||
|
||||
#: What a P4 panel reports: a GPIO pin either way, its backlight and touch.
|
||||
REPORT = {
|
||||
"schema": 1,
|
||||
"platform": "esp32p4",
|
||||
"board": "p4",
|
||||
"firmware": "0.1.2",
|
||||
"display": {"w": 1024, "h": 600, "shape": "rect", "touch": True},
|
||||
"ports": [
|
||||
{"port": "backlight", "direction": "out", "dtype": "int"},
|
||||
{"port": "touch", "direction": "in", "dtype": "bool"},
|
||||
{
|
||||
"port": "gpio.4",
|
||||
"direction": "in",
|
||||
"modes": ["in", "out"],
|
||||
"dtype": "bool",
|
||||
"config": {"type": "object", "properties": {"invert": {"type": "boolean"}}},
|
||||
},
|
||||
],
|
||||
"features": {"display": True},
|
||||
# A newer firmware's key, which this version drops.
|
||||
"battery": {"level": 80},
|
||||
}
|
||||
|
||||
|
||||
def test_a_device_reports_what_it_has(
|
||||
client: TestClient, superuser_token_headers: dict[str, str]
|
||||
) -> None:
|
||||
"""Only from its own credential, at most 16 KB, the whole report replaced.
|
||||
|
||||
The device node's ports are checked against it from then on.
|
||||
"""
|
||||
_panels(
|
||||
client,
|
||||
superuser_token_headers,
|
||||
{"panels": [{"id": "porch_io"}, {"id": "other"}]},
|
||||
)
|
||||
_publish_flow(client, superuser_token_headers, "porch", [PORCH_NODE])
|
||||
device = _pair(client, superuser_token_headers, "porch_io")
|
||||
other = _pair(client, superuser_token_headers, "other")
|
||||
url = f"{PREFIX}/porch_io/report"
|
||||
|
||||
def issues() -> set[tuple[str | None, bool]]:
|
||||
found = client.get(f"{FLOWS}/porch", headers=superuser_token_headers).json()
|
||||
return {
|
||||
(i["port"], i["advisory"])
|
||||
for i in found["issues"]
|
||||
if i["code"] == "device_port"
|
||||
}
|
||||
|
||||
# Without a report the ports are the device's business.
|
||||
assert issues() == set()
|
||||
assert (
|
||||
client.put(url, headers=superuser_token_headers, json=REPORT).status_code == 403
|
||||
)
|
||||
assert client.put(url, headers=other, json=REPORT).status_code == 403
|
||||
padded = {**REPORT, "firmware": "x" * 20_000}
|
||||
assert client.put(url, headers=device, json=padded).status_code == 413
|
||||
assert (
|
||||
client.put(url, headers=device, json={**REPORT, "schema": 2}).status_code == 422
|
||||
)
|
||||
|
||||
put = client.put(url, headers=device, json=REPORT)
|
||||
assert put.status_code == 200, put.text
|
||||
listed = client.get(f"{PREFIX}/", headers=superuser_token_headers).json()
|
||||
stored = next(d for d in listed["data"] if d["id"] == "porch_io")
|
||||
assert stored["reported_at"]
|
||||
assert "battery" not in stored["report"]
|
||||
assert stored["report"]["ports"][2]["modes"] == ["in", "out"]
|
||||
# A touch screen's first report makes the device touch friendly.
|
||||
assert stored["touch"] is True
|
||||
# What the device reads stays what it was: no report, no presence.
|
||||
read = client.get(f"{PREFIX}/porch_io", headers=device).json()
|
||||
assert "report" not in read and read["touch"] is True
|
||||
# gpio.4 is wired as in, which its modes allow, and the backlight as int.
|
||||
assert issues() == set()
|
||||
|
||||
# A report replaces the one before: the backlight gone, gpio.4 out only.
|
||||
gpio = {"port": "gpio.4", "direction": "out", "dtype": "bool"}
|
||||
assert (
|
||||
client.put(url, headers=device, json={**REPORT, "ports": [gpio]}).status_code
|
||||
== 200
|
||||
)
|
||||
assert issues() == {("gpio.4", False), ("backlight", True)}
|
||||
client.delete(f"{FLOWS}/porch", headers=superuser_token_headers)
|
||||
# The other tests wire porch_io without a report.
|
||||
store = app.state.device_store
|
||||
with store._state_lock:
|
||||
states = store._read_states()
|
||||
states["porch_io"].report = None
|
||||
store._write_states(states)
|
||||
|
||||
|
||||
def test_publishing_a_flow_with_device_nodes_tells_the_devices(
|
||||
client: TestClient, superuser_token_headers: dict[str, str]
|
||||
) -> None:
|
||||
|
||||
Reference in New Issue
Block a user