Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions docs/mqtt-vectors/channels-broadcast.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"contract": "docs/mqtt-spec.md#the-channel-decides-whether-your-writes-survive",
"says": "**Broadcast has no snapshot subscription.**",
"features": ["mqtt"],
"steps": [
{ "send": { "mqtt": { "topic": "patternflow/snapshot", "payload": "{\"param\":[100,200,300,400]}", "retain": true } } },
{ "send": { "http": "POST /api/mqtt?prefix=patternflow&role=subscriber" } },
{ "send": { "wait": 20 }, "expect": { "status": { "mqttConnected": true, "paramActive": [false, false, false, false] } } },
{ "send": { "mqtt": { "topic": "patternflow/param/2", "payload": "800" } } },
{ "send": { "wait": 20 }, "expect": { "status": { "params": ["<integer>", 800, "<integer>", "<integer>"], "paramActive": [false, true, false, false] } } },
{ "send": { "mqtt": { "topic": "patternflow/snapshot", "payload": "{\"param\":[100,200,300,400]}", "retain": true } } },
{ "send": { "wait": 20 }, "expect": { "status": { "params": ["<integer>", 800, "<integer>", "<integer>"], "paramActive": [false, true, false, false] } } }
]
}
20 changes: 20 additions & 0 deletions docs/mqtt-vectors/channels-forced.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"contract": "docs/mqtt-spec.md#transport",
"says": "Channels 1–4 force the Subscriber role.",
"features": ["mqtt"],
"steps": [
{ "send": { "http": "POST /api/mqtt?prefix=patternflow5&role=publisher" } },
{ "send": { "wait": 20 }, "expect": { "status": { "mqttRole": "publisher", "mqttConnected": true } } },
{ "send": { "http": "POST /api/mqtt?prefix=patternflow1" }, "expect": { "status": { "mqttRole": "subscriber" } } },
{ "send": { "http": "POST /api/mqtt?role=publisher" }, "expect": { "status": { "mqttRole": "subscriber" } } },
{ "send": { "http": "POST /api/mqtt?prefix=patternflow2" } },
{ "send": { "http": "POST /api/mqtt?role=publisher" }, "expect": { "status": { "mqttRole": "subscriber" } } },
{ "send": { "http": "POST /api/mqtt?prefix=patternflow3" } },
{ "send": { "http": "POST /api/mqtt?role=publisher" }, "expect": { "status": { "mqttRole": "subscriber" } } },
{ "send": { "http": "POST /api/mqtt?prefix=patternflow4" } },
{ "send": { "http": "POST /api/mqtt?role=publisher" }, "expect": { "status": { "mqttRole": "subscriber" } } },
{ "send": { "wait": 20 }, "expect": { "status": { "mqttRole": "subscriber", "mqttConnected": true } } },
{ "send": { "mqtt": { "topic": "patternflow4/pattern", "payload": "Wave Saw" } } },
{ "send": { "wait": 20 }, "expect": { "status": { "active": "Wave Saw" } } }
]
}
15 changes: 15 additions & 0 deletions docs/mqtt-vectors/channels-heartbeat.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"contract": "docs/mqtt-spec.md#the-channel-decides-whether-your-writes-survive",
"says": "A Publisher on the channel re-sends one every 8 seconds, and the retained copy is redelivered on every reconnect.",
"features": ["mqtt"],
"steps": [
{ "send": { "http": "POST /api/mqtt?prefix=patternflow5&role=publisher" } },
{ "send": { "wait": 20 }, "expect": {
"status": { "mqttRole": "publisher", "mqttConnected": true, "paramActive": [false, false, false, false] },
"out": [{ "mqtt": { "topic": "patternflow5/snapshot", "payload": "<string>", "retain": true } }] } },
{ "send": { "wait": 7900 }, "expect": { "out": [{ "mqtt": "<absent>" }] } },
{ "send": { "wait": 200 }, "expect": { "out": [{ "mqtt": { "topic": "patternflow5/snapshot", "payload": "<string>", "retain": true } }] } },
{ "send": { "http": "POST /api/mqtt?role=subscriber" } },
{ "send": { "wait": 20 }, "expect": { "status": { "mqttRole": "subscriber", "mqttConnected": true, "paramActive": [true, true, true, true] } } }
]
}
19 changes: 19 additions & 0 deletions docs/mqtt-vectors/channels-overwrite.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"contract": "docs/mqtt-spec.md#the-channel-decides-whether-your-writes-survive",
"says": "So on those five channels an external controller is not the only writer: its value is overwritten a moment later by whatever the snapshot last said, with every write succeeding and nothing reporting an error.",
"features": ["mqtt"],
"steps": [
{ "send": { "http": "POST /api/mqtt?prefix=patternflow2&role=subscriber" } },
{ "send": { "wait": 20 }, "expect": { "status": { "mqttConnected": true } } },
{ "send": { "mqtt": { "topic": "patternflow2/param/1", "payload": "900" } } },
{ "send": { "wait": 20 }, "expect": { "status": { "params": [900, "<integer>", "<integer>", "<integer>"], "paramActive": [true, false, false, false] } } },
{ "send": { "mqtt": { "topic": "patternflow2/snapshot", "payload": "{\"param\":[250,500,500,500]}", "retain": true } } },
{ "send": { "wait": 20 }, "expect": { "status": { "params": [250, 500, 500, 500], "paramActive": [true, true, true, true] } } },
{ "send": { "http": "POST /api/mqtt?prefix=patternflow5" } },
{ "send": { "wait": 20 }, "expect": { "status": { "mqttRole": "subscriber", "mqttConnected": true } } },
{ "send": { "mqtt": { "topic": "patternflow5/param/3", "payload": "900" } } },
{ "send": { "wait": 20 }, "expect": { "status": { "params": [250, 500, 900, 500], "paramActive": [true, true, true, true] } } },
{ "send": { "mqtt": { "topic": "patternflow5/snapshot", "payload": "{\"param\":[250,500,640,500]}", "retain": true } } },
{ "send": { "wait": 20 }, "expect": { "status": { "params": [250, 500, 640, 500], "paramActive": [true, true, true, true] } } }
]
}
20 changes: 20 additions & 0 deletions docs/mqtt-vectors/channels-prefix.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"contract": "docs/mqtt-spec.md#transport",
"says": "Prefix `patternflow` is **Broadcast**; `patternflow1` … `patternflow4` are **channels 1–4**; `patternflow5` is **Live**; anything else is **Custom**.",
"features": ["mqtt"],
"steps": [
{ "send": { "http": "POST /api/mqtt?prefix=patternflow3&role=subscriber" }, "expect": { "reply": { "body": { "channel": "ch3", "prefix": "patternflow3" } } } },
{ "send": { "wait": 20 }, "expect": { "status": { "mqttConnected": true } } },
{ "send": { "mqtt": { "topic": "patternflow/param/1", "payload": "900" } } },
{ "send": { "wait": 20 }, "expect": { "status": { "paramActive": [false, false, false, false] } } },
{ "send": { "mqtt": { "topic": "patternflow3/param/1", "payload": "900" } } },
{ "send": { "wait": 20 }, "expect": { "status": { "params": [900, "<integer>", "<integer>", "<integer>"], "paramActive": [true, false, false, false] } } },
{ "send": { "http": "POST /api/mqtt?prefix=patternflow" }, "expect": { "reply": { "body": { "channel": "broadcast" } } } },
{ "send": { "http": "POST /api/mqtt?prefix=patternflow1" }, "expect": { "reply": { "body": { "channel": "ch1" } } } },
{ "send": { "http": "POST /api/mqtt?prefix=patternflow2" }, "expect": { "reply": { "body": { "channel": "ch2" } } } },
{ "send": { "http": "POST /api/mqtt?prefix=patternflow4" }, "expect": { "reply": { "body": { "channel": "ch4" } } } },
{ "send": { "http": "POST /api/mqtt?prefix=patternflow5" }, "expect": { "reply": { "body": { "channel": "live" } } } },
{ "send": { "http": "POST /api/mqtt?prefix=patternflow6" }, "expect": { "reply": { "body": { "channel": "custom" } } } },
{ "send": { "http": "POST /api/mqtt?prefix=studio" }, "expect": { "reply": { "body": { "channel": "custom" } } } }
]
}
22 changes: 22 additions & 0 deletions docs/mqtt-vectors/channels-snapshot.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"contract": "docs/mqtt-spec.md#the-channel-decides-whether-your-writes-survive",
"says": "Channels 1–4 and Live subscribe to the retained `<prefix>/snapshot`, whose `param:[…]` the firmware applies straight onto the knobs, exactly as if it had arrived on `param/<n>`.",
"features": ["mqtt"],
"steps": [
{ "send": { "mqtt": { "topic": "patternflow1/snapshot", "payload": "{\"param\":[110,120,130,140]}", "retain": true } } },
{ "send": { "mqtt": { "topic": "patternflow2/snapshot", "payload": "{\"param\":[210,220,230,240]}", "retain": true } } },
{ "send": { "mqtt": { "topic": "patternflow3/snapshot", "payload": "{\"param\":[310,320,330,340]}", "retain": true } } },
{ "send": { "mqtt": { "topic": "patternflow4/snapshot", "payload": "{\"param\":[410,420,430,440]}", "retain": true } } },
{ "send": { "mqtt": { "topic": "patternflow5/snapshot", "payload": "{\"param\":[510,520,530,540]}", "retain": true } } },
{ "send": { "http": "POST /api/mqtt?prefix=patternflow1&role=subscriber" } },
{ "send": { "wait": 20 }, "expect": { "status": { "params": [110, 120, 130, 140], "paramActive": [true, true, true, true] } } },
{ "send": { "http": "POST /api/mqtt?prefix=patternflow2" } },
{ "send": { "wait": 20 }, "expect": { "status": { "params": [210, 220, 230, 240], "paramActive": [true, true, true, true] } } },
{ "send": { "http": "POST /api/mqtt?prefix=patternflow3" } },
{ "send": { "wait": 20 }, "expect": { "status": { "params": [310, 320, 330, 340], "paramActive": [true, true, true, true] } } },
{ "send": { "http": "POST /api/mqtt?prefix=patternflow4" } },
{ "send": { "wait": 20 }, "expect": { "status": { "params": [410, 420, 430, 440], "paramActive": [true, true, true, true] } } },
{ "send": { "http": "POST /api/mqtt?prefix=patternflow5" } },
{ "send": { "wait": 20 }, "expect": { "status": { "params": [510, 520, 530, 540], "paramActive": [true, true, true, true] } } }
]
}
9 changes: 9 additions & 0 deletions docs/rest-api-vectors/mqtt-caps.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"contract": "docs/rest-api.md#conventions-for-future-additions",
"says": "A feature's endpoints exist only when that feature is composed in — the route itself is absent otherwise, so clients probe `caps` rather than expecting a soft 404.",
"features": ["mqtt"],
"steps": [
{ "send": { "http": "GET /api/status" }, "expect": { "reply": { "body": { "caps": ["patterns", "params", "sleep", "mqtt"] } } } },
{ "send": { "http": "GET /api/mqtt" }, "expect": { "reply": { "status": 200, "headers": { "content-type": "application/json" }, "body": { "ok": true } } } }
]
}
26 changes: 26 additions & 0 deletions docs/rest-api-vectors/mqtt-fields.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"contract": "docs/rest-api.md#get-apimqtt",
"says": "Also reported: `normalHost`, `normalPort`, `normalUser`, `normalPrefix`, `normalHasPassword` (the saved Normal-mode broker, kept while Director mode overlays it) and `directorHost`.",
"features": ["mqtt"],
"steps": [
{
"send": { "http": "GET /api/mqtt" },
"expect": {
"reply": {
"status": 200,
"body": {
"ok": true, "role": "<string>", "channel": "<string>", "state": "<string>",
"host": "<string>", "port": "<integer>", "user": "<string>", "prefix": "<string>",
"pattern": "<string>", "error": "<string>", "mode": "<string>",
"connected": "<boolean>", "configured": "<boolean>", "hasPassword": "<boolean>", "forcesSub": "<boolean>",
"knobs": ["<integer>", "<integer>", "<integer>", "<integer>"],
"params": ["<integer>", "<integer>", "<integer>", "<integer>"],
"paramActive": ["<boolean>", "<boolean>", "<boolean>", "<boolean>"],
"normalHost": "<string>", "normalPort": "<integer>", "normalUser": "<string>", "normalPrefix": "<string>",
"normalHasPassword": "<boolean>", "directorHost": "<string>"
}
}
}
}
]
}
11 changes: 11 additions & 0 deletions docs/rest-api-vectors/mqtt-forces-sub.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"contract": "docs/rest-api.md#get-apimqtt",
"says": "The selected channel (1–4) pins the role to Subscriber",
"features": ["mqtt"],
"steps": [
{ "send": { "http": "POST /api/mqtt?prefix=patternflow5&role=publisher" }, "expect": { "reply": { "body": { "channel": "live", "role": "publisher", "forcesSub": false } } } },
{ "send": { "http": "POST /api/mqtt?prefix=patternflow3" }, "expect": { "reply": { "body": { "channel": "ch3", "role": "subscriber", "forcesSub": true } } } },
{ "send": { "http": "POST /api/mqtt?role=publisher" }, "expect": { "reply": { "body": { "channel": "ch3", "role": "subscriber", "forcesSub": true } } } },
{ "send": { "http": "GET /api/mqtt" }, "expect": { "reply": { "body": { "channel": "ch3", "role": "subscriber", "forcesSub": true } } } }
]
}
17 changes: 17 additions & 0 deletions docs/rest-api-vectors/mqtt-knobs.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"contract": "docs/rest-api.md#get-apimqtt",
"says": "Absolute accumulated click counts, signed, unbounded. Live in every role.",
"features": ["mqtt"],
"steps": [
{ "send": { "hand": { "knob": 1, "turn": 3 } } },
{ "send": { "hand": { "knob": 1, "turn": 2 } } },
{ "send": { "hand": { "knob": 2, "turn": -4 } } },
{ "send": { "http": "GET /api/mqtt" }, "expect": { "reply": { "body": { "role": "off", "knobs": [5, -4, 0, 0] } } } },
{ "send": { "http": "POST /api/mqtt?role=publisher" } },
{ "send": { "hand": { "knob": 4, "turn": 30 } } },
{ "send": { "http": "GET /api/mqtt" }, "expect": { "reply": { "body": { "role": "publisher", "knobs": [5, -4, 0, 30] } } } },
{ "send": { "http": "POST /api/mqtt?role=subscriber" } },
{ "send": { "hand": { "knob": 2, "turn": -1 } } },
{ "send": { "http": "GET /api/mqtt" }, "expect": { "reply": { "body": { "role": "subscriber", "knobs": [5, -5, 0, 30] } } } }
]
}
16 changes: 16 additions & 0 deletions docs/rest-api-vectors/mqtt-password.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"contract": "docs/rest-api.md#get-apimqtt",
"says": "Passwords are never returned — only whether one is set.",
"features": ["mqtt"],
"steps": [
{ "send": { "http": "GET /api/mqtt" }, "expect": { "reply": { "body": { "hasPassword": false, "normalHasPassword": false } } } },
{
"send": { "http": { "request": "POST /api/mqtt", "form": "host=broker.lan&user=ha&password=hunter2" } },
"expect": { "reply": { "status": 200, "body": { "host": "broker.lan", "user": "ha", "hasPassword": true, "normalHasPassword": true, "password": "<absent>", "pass": "<absent>" } } }
},
{
"send": { "http": "GET /api/mqtt" },
"expect": { "reply": { "body": { "host": "broker.lan", "user": "ha", "hasPassword": true, "normalHasPassword": true, "password": "<absent>", "pass": "<absent>" } } }
}
]
}
11 changes: 11 additions & 0 deletions docs/rest-api-vectors/mqtt-role.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"contract": "docs/rest-api.md#get-apistatus",
"says": "`\"off\"`, `\"publisher\"` or `\"subscriber\"`.",
"features": ["mqtt"],
"steps": [
{ "send": { "http": "GET /api/status" }, "expect": { "reply": { "body": { "mqttRole": "off", "mqttState": "<string>", "mqttConnected": false } } } },
{ "send": { "http": "POST /api/mqtt?role=publisher" }, "expect": { "reply": { "status": 200, "body": { "ok": true, "role": "publisher" } }, "status": { "mqttRole": "publisher" } } },
{ "send": { "http": "POST /api/mqtt?role=subscriber" }, "expect": { "reply": { "status": 200, "body": { "ok": true, "role": "subscriber" } }, "status": { "mqttRole": "subscriber" } } },
{ "send": { "http": "POST /api/mqtt?role=off" }, "expect": { "reply": { "status": 200, "body": { "ok": true, "role": "off" } }, "status": { "mqttRole": "off" } } }
]
}
17 changes: 17 additions & 0 deletions docs/rest-api-vectors/mqtt-same-fields.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"contract": "docs/rest-api.md#knobs-and-parameters",
"says": "`GET /api/mqtt` reports the same three fields and has since before `/api/status` did — but it lives in the MQTT addon, so a build without MQTT does not serve it.",
"features": ["mqtt"],
"steps": [
{ "send": { "hand": { "knob": 3, "turn": 2 } } },
{ "send": { "http": "POST /api/params?p2=800" } },
{
"send": { "http": "GET /api/mqtt" },
"expect": { "reply": { "body": { "knobs": [0, 0, 2, 0], "params": ["<integer>", 800, "<integer>", "<integer>"], "paramActive": [false, true, false, false] } } }
},
{
"send": { "http": "GET /api/status" },
"expect": { "reply": { "body": { "knobs": [0, 0, 2, 0], "params": ["<integer>", 800, "<integer>", "<integer>"], "paramActive": [false, true, false, false] } } }
}
]
}
Loading