This commit is contained in:
Mateusz Gruszczyński
2026-09-01 22:20:10 +02:00
parent 1cb62c8d0b
commit 16e0d94564
47 changed files with 2565 additions and 117 deletions
+64
View File
@@ -54,6 +54,70 @@ grep -q '"control_temperature"' "$TMP/history.json"
curl -fsS "http://127.0.0.1:$PORT/api/readings?device_id=sim-salon&hours=1" >"$TMP/readings.json"
grep -q '"readings"' "$TMP/readings.json"
# Visual Flow: source graph, dry-run, export/import and optimistic concurrency.
cat >"$TMP/flow-create.json" <<EOF
{"name":"Smoke Flow","enabled":true,"description":"smoke","nodes":[{"id":"cond","kind":"constant","x":20,"y":20,"config":{"value":true}},{"id":"action","kind":"zone_thermostat","x":240,"y":20,"config":{"zone_id":"$ZONE_ID","preset":"comfort","mode":"auto","cooldown_seconds":60}}],"edges":[{"id":"edge","from":"cond","to":"action"}]}
EOF
curl -fsS -X POST -H 'Content-Type: application/json' --data-binary @"$TMP/flow-create.json" "http://127.0.0.1:$PORT/api/flows" >"$TMP/flow.json"
grep -q '"revision":1' "$TMP/flow.json"
FLOW_ID="$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1]))["id"])' "$TMP/flow.json")"
FLOW_REVISION="$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1]))["revision"])' "$TMP/flow.json")"
python3 - "$TMP/flow-create.json" "$TMP/flow-simulate.json" "$FLOW_ID" <<'PYFLOW'
import json,sys,datetime
flow=json.load(open(sys.argv[1]))
json.dump({"flow":flow,"flow_id":sys.argv[3],"at":datetime.datetime.now(datetime.timezone.utc).isoformat(),"overrides":{"cond":True},"log":True},open(sys.argv[2],"w"))
PYFLOW
curl -fsS -X POST -H 'Content-Type: application/json' --data-binary @"$TMP/flow-simulate.json" "http://127.0.0.1:$PORT/api/flows/simulate" >"$TMP/flow-dry-run.json"
grep -q '"dry_run":true' "$TMP/flow-dry-run.json"
curl -fsS "http://127.0.0.1:$PORT/api/flows/$FLOW_ID/export" >"$TMP/flow-export.json"
grep -q '"format":"gree-controller-flow"' "$TMP/flow-export.json"
curl -fsS -X POST -H 'Content-Type: application/json' --data-binary @"$TMP/flow-export.json" "http://127.0.0.1:$PORT/api/flows/import" >"$TMP/flow-import.json"
grep -q '"revision":1' "$TMP/flow-import.json"
python3 - "$TMP/flow-create.json" "$TMP/flow-update.json" "$FLOW_REVISION" <<'PYFLOW'
import json,sys
flow=json.load(open(sys.argv[1])); flow["expected_revision"]=int(sys.argv[3]); flow["description"]="updated"
json.dump(flow,open(sys.argv[2],"w"))
PYFLOW
curl -fsS -X PUT -H 'Content-Type: application/json' --data-binary @"$TMP/flow-update.json" "http://127.0.0.1:$PORT/api/flows/$FLOW_ID" >"$TMP/flow-updated.json"
grep -q '"revision":2' "$TMP/flow-updated.json"
STALE_STATUS="$(curl -sS -o /dev/null -w '%{http_code}' -X PUT -H 'Content-Type: application/json' --data-binary @"$TMP/flow-update.json" "http://127.0.0.1:$PORT/api/flows/$FLOW_ID")"
[[ "$STALE_STATUS" == "409" ]]
# A schedule-compatible Flow must keep using the native schedule engine.
cat >"$TMP/flow-schedule.json" <<EOF
{"name":"Native schedule Flow","enabled":true,"nodes":[{"id":"days","kind":"weekday","x":20,"y":20,"config":{"days":[1,2,3,4,5,6,7]}},{"id":"time","kind":"time_range","x":180,"y":20,"config":{"start":"06:00","end":"08:00"}},{"id":"action","kind":"zone_thermostat","x":360,"y":20,"config":{"zone_id":"$ZONE_ID","preset":"comfort","mode":"auto","cooldown_seconds":60}}],"edges":[{"id":"e1","from":"days","to":"time"},{"id":"e2","from":"time","to":"action"}]}
EOF
curl -fsS -X POST -H 'Content-Type: application/json' --data-binary @"$TMP/flow-schedule.json" \
"http://127.0.0.1:$PORT/api/flows" >"$TMP/flow-schedule-result.json"
python3 - "$TMP/flow-schedule-result.json" <<'PYFLOW'
import json,sys
flow=json.load(open(sys.argv[1]))
assert len(flow["compiled_schedule_ids"]) == 1, flow
assert len(flow["compiled_automation_ids"]) == 0, flow
PYFLOW
# HA errors must fail closed: even a != condition cannot become true just because HA is unavailable.
cat >"$TMP/flow-ha-safe.json" <<EOF
{"flow":{"name":"HA fail closed","enabled":true,"nodes":[{"id":"ha","kind":"ha_state","x":20,"y":20,"config":{"entity_id":"binary_sensor.missing","operator":"neq","value":"on"}},{"id":"action","kind":"zone_thermostat","x":260,"y":20,"config":{"zone_id":"$ZONE_ID","preset":"comfort","mode":"auto","cooldown_seconds":60}}],"edges":[{"id":"e1","from":"ha","to":"action"}]},"log":false}
EOF
curl -fsS -X POST -H 'Content-Type: application/json' --data-binary @"$TMP/flow-ha-safe.json" \
"http://127.0.0.1:$PORT/api/flows/simulate" >"$TMP/flow-ha-safe-result.json"
python3 - "$TMP/flow-ha-safe-result.json" <<'PYFLOW'
import json,sys
result=json.load(open(sys.argv[1]))
action=result["actions"][0]
assert action["matched"] is False, result
assert action["would_execute"] is False, result
PYFLOW
# Direct GREE Flow actions use the existing DeviceCommand surface beyond power/mode/target.
cat >"$TMP/flow-device-options.json" <<EOF
{"flow":{"name":"GREE options","enabled":true,"nodes":[{"id":"yes","kind":"constant","x":20,"y":20,"config":{"value":true}},{"id":"action","kind":"device_action","x":260,"y":20,"config":{"device_id":"sim-salon","light":false,"turbo":true,"cooldown_seconds":60}}],"edges":[{"id":"e1","from":"yes","to":"action"}]},"log":false}
EOF
curl -fsS -X POST -H 'Content-Type: application/json' --data-binary @"$TMP/flow-device-options.json" \
"http://127.0.0.1:$PORT/api/flows/simulate" >"$TMP/flow-device-options-result.json"
grep -q '"would_execute":true' "$TMP/flow-device-options-result.json"
# Home Assistant gets its own generated, restricted controller token.
curl -fsS -X POST -H 'Content-Type: application/json' \
-d '{"name":"Smoke Home Assistant"}' \