Preset sync targets /json/presets, which no WLED firmware implements #115
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 project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
rbrooks/Iris-WLED#115
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
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?
Split from #62, where it was found while capturing real controller fixtures.
preset_sync.sync_presets()reads and writes WLED presets viaGET/POST /json/presets(app/services/wled_controller.py,get_presets/post_presets). That endpoint does not exist in WLED. Verified against the live roofline controller on 16.0.0:The JSON API reference confirms it has never existed: the
/json/*sub-paths arestate,info,si,nodes,eff,fxdata,pal,palx,net,cfg,pins,live. Presets live on the flash filesystem and are managed through state fields (ps,psave,pdel).So
POST /api/v1/wled/sync-presetshas never worked against real hardware. It passes tests because they mockWLEDControllerwholesale.Why this is worse than a plain 502
sync_presetswraps the read in a bareexcept Exception: existing = {}(preset_sync.py:76). The 501 raisesHTTPStatusError, gets swallowed, and the function proceeds to build its payload on top of an empty dict. Every preset outside the managed range — which the docstring explicitly promises to preserve — is dropped from the payload. Today the subsequentpost_presetsalso 501s, so nothing is lost. The moment the write path is fixed without also fixing the read, a sync would silently wipe the user's presets.What needs to happen
GET /presets.json(the file, not a/jsonsub-path). Confirmed working on the device — returns the full preset map, with unused slots as{"stop":0}segment stubs.POST /upload(multipart, target path/presets.json), which is how the WLED app does preset backup/restore. The alternative — applying each state and issuing{"psave": n}per slot — would drive the physical lights once per preset, so it is the wrong mechanism here.except Exception. A failed read must abort the sync, never degrade to "start from empty".Interim state
#62 leaves the endpoint as-is functionally but makes the failure loud rather than silent, so nobody mistakes the swallowed read for a working sync.
Note the export path is unaffected:
GET /api/v1/export/presets.jsonbuilds the file server-side for manual upload and never talks to the controller.Done — #118 merged, all seven CI jobs green.
The mechanism, verified on hardware
GET /presets.jsonPOST /upload, multipart — the part's filename is the destination pathGET /edit?func=delete&path=/…— a GET, not an HTTP DELETEThe upload path was proved on a throwaway filename (
/iris-probe.json) before going anywhere nearpresets.json, then deleted.The open question — does an uploaded preset file take effect without a reboot — is a yes. Applying
ps=100immediately after an upload ran exactly the fx, palette and colours that had been written.What was checked against the device
Snapshotted
presets.json,/json/stateand/json/infofirst; all restored after.WLEDController: read-back byte-identical to what was sent, pre-existing slots 1–3 untouchedsync_presetswith a seeded DB — 2 approved schemes to slots 100–101, correctly named and date-ordered, 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 then reads it back. A 200 from
/uploadmeans 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 return a clear error withsynced: 0, and I confirmed those tests actually fail when the verification block is removed.The bare
except Exceptionon the read is narrowed in effect rather than in type: it still catches broadly, but now aborts instead of degrading to{}. That mattered less when the write could not succeed either; now that it can, it is the only thing between an unreadable controller and a wiped preset file.Two findings beyond the issue
Compact serialisation. The 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. Compact output also matches WLED's own exactly, which is why the restored file is now byte-identical rather than merely equivalent.The tests passed by mock aliasing.
get_presets.return_value = {}hands back the same dict object on every call, sosync_presetsmutating it made the read-back compare a dict against itself — passing regardless of what was written. The mock now models the preset file, returning a fresh copy per read. Mocks agreeing with the code rather than with the device is exactly what let this bug sit behind green tests for the whole v1.0.0 cycle, so it seemed worth fixing at the root.Documented, not implemented
A controller with a settings PIN rejects
/editand/upload, and Iris does not send one. Noted indocs/wled-compatibility.md; the dev controller has no PIN. Worth its own issue if anyone needs it.Also recorded there: an upload named
cfg.jsonreboots the device, sopost_presetsis deliberately not a general-purpose upload helper.