Sync presets by uploading the file WLED actually reads (#115) #118
No reviewers
Labels
No labels
area/ai
area/backend
area/frontend
area/infra
area/scheduler
area/wled
good-first-issue
priority/high
priority/low
priority/medium
type/bug
type/chore
type/ci-cd
type/docs
type/feature
type/qa
v1.0.0
v1.1.0
v1.2.0
v2.0.0
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
rbrooks/Iris-WLED!118
Loading…
Reference in a new issue
No description provided.
Delete branch "feat/115-preset-sync-upload"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes #115.
POST /api/v1/wled/sync-presetstargetedGET/POST /json/presets, which no WLED firmware implements — a real 16.0.0 device answers501 {"error":4}. It had never once succeeded outside the test suite.The actual mechanism
Presets are a file on the controller's flash filesystem, not a JSON API resource:
GET /presets.jsonPOST /upload, multipart — the part's filename is the destination pathGET /edit?func=delete&path=/…— a GET, not an HTTP DELETEAll three verified against your roofline controller. I proved the upload path on a throwaway filename (
/iris-probe.json) before going anywhere nearpresets.json, then deleted it.An uploaded preset file takes effect with no reboot — that was the open question, and it's a yes: applying
ps=100immediately after an upload ran exactly the fx, palette and colours that had been written.Verification on hardware
Snapshotted
presets.json,/json/stateand/json/infofirst; everything below was restored afterwards.WLEDController: read-back byte-identical to what was sent, pre-existing slots 1–3 untouchedsync_presetsagainst the live device with a seeded DB — wrote 2 approved schemes to slots 100–101, correctly named and date-ordered (Halloween 2026 (peak),Christmas Day 2026 (mid)), pre-existing presets intact, read-back verification passedon:false, bri:8, fx 115Read-back verification
The write replaces the whole file, so sync merges into what it read — and now reads the file back to confirm what landed. A 200 from
/uploadsays the request was accepted, not that a complete file reached a flash chip. Without this, a truncated write would surface in December as a preset that does nothing. Both failure paths (mismatch, unreadable read-back) return a clear error andsynced: 0.I confirmed those two tests actually fail when the verification block is removed, rather than trusting that they would.
Compact serialisation, and how it was caught
My first restore reported success but left the file byte-different:
json.dumps' default", "/": "separators inflated it 20% (2240 → 2682 bytes) in pure whitespace. On a device with ~983KB of filesystem that has to hold up to 366 presets, that margin is worth having. Compact output also matches WLED's own serialisation exactly — which is why the restored file is now byte-identical to what the controller originally wrote.The test bug that hid all of this
get_presets.return_value = {}hands back the same dict object on every call.sync_presetsmutates what it reads, so the read-back compared a dict against itself and passed no matter what was written. I checked this explicitly rather than inferring it.The mock now models the preset file — a fresh copy per read, writes replacing the store — so these tests assert what the controller would hold rather than what the code happened to pass along. Mocks agreeing with the code rather than with the device is precisely what let the original
/json/presetsbug sit behind green tests for the entire v1.0.0 cycle, which felt worth fixing at the root while I was here.Two hazards documented for anyone reusing this path
cfg.jsonreboots the device.post_presetsis deliberately not a general-purpose upload helper.{"psave": n}on/json/statesaves whatever is currently showing. Syncing a year that way would light the house up once per preset — hence the file upload.Also noted in
docs/wled-compatibility.md: a controller with a settings PIN will reject/editand/upload, and Iris does not currently send one. Yours has no PIN set. I have not implemented PIN support — say the word if you want it and I'll open an issue.Verification
ruff check,ruff format --check,mypy appcleanuvx pre-commit run --all-filespasses🤖 Generated with Claude Code
Preset sync targeted GET/POST /json/presets, which no WLED firmware implements -- a real 16.0.0 device answers 501 {"error":4} -- so POST /api/v1/wled/sync-presets had never once succeeded outside the test suite. Presets are a file on the controller's flash filesystem, not a JSON API resource: read GET /presets.json write POST /upload multipart; the part's filename IS the path delete GET /edit?func=delete&path=/... (a GET, not an HTTP DELETE) All verified against the roofline controller, including that an uploaded preset file takes effect with no reboot: applying ps=100 straight after an upload ran the fx, palette and colours that were written. The write replaces the whole file, so sync merges into what it read and now reads the file back to confirm what landed. A 200 from /upload means the request was accepted, not that a complete file reached a flash chip; without the read-back a truncated write would surface in December as a preset that does nothing. The file is serialised compactly. json.dumps' defaults cost ~20% of the payload in whitespace (2240 -> 2682 bytes for four presets) on a device with ~983KB of filesystem that has to hold up to 366 of them. Compact output also matches WLED's own, so a round-trip through Iris leaves the file byte-identical to what the controller would have written itself -- which is how the inflation was noticed. Not used: {"psave": n} on /json/state, which saves whatever is currently showing. Syncing a year that way would light the house up once per preset. Also fixes the tests that hid this. `get_presets.return_value = {}` hands back the same dict on every call, so sync mutating it made the read-back compare a dict against itself and pass regardless. The mock now models the preset file, returning a fresh copy per read. Mocks agreeing with the code rather than with the device is precisely what let the original bug sit behind green tests for the whole v1.0.0 cycle. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>