Skip to content

Effect list exposes WLED's "RSVD" placeholder entries to consumers (e.g. Home Assistant) #2053

@1saac-k

Description

@1saac-k

Summary

WLED firmware uses "RSVD" as a placeholder name for unallocated effect slots so the effect array index stays a stable identifier. WLED's own web UI filters these out, but python-wled passes them through verbatim, so they appear as real, selectable effects in the Home Assistant WLED integration's effect dropdown.

Selecting one of them in HA also produces a confusing result: the strip ends up running a different effect (see below).

Observed behavior in Home Assistant

On a stock v16.0.0 ESP8266 build (no WLED_ENABLE_GIF), my HA dropdown contains multiple RSVD entries.

Image Image

Selecting any of them in HA always activates "Chase 3" (FX_MODE_TRICOLOR_CHASE, ID 54), and the dropdown then displays "Chase 3" as the current selection. This is because python-wled resolves every "RSVD" name to the same first matching effect_id (53), and the firmware then advances 53 to the next non-reserved slot, 54.

Reproduction with curl (the second case shows the same firmware skip applied to a different reserved cluster — setting any ID inside 187..201 ends up at 202):

$ curl -s http://wled/json/effects | jq 'to_entries | map(select(.value=="RSVD")) | .[0:3]'
[{"key":53,"value":"RSVD"},{"key":142,"value":"RSVD"},{"key":169,"value":"RSVD"}]

$ curl -s -X POST -H 'Content-Type: application/json' \
       -d '{"seg":{"id":0,"fx":53}}' http://wled/json
{"success":true}
$ curl -s http://wled/json/state | jq '.seg[0].fx'
54

$ curl -s -X POST -H 'Content-Type: application/json' \
       -d '{"seg":{"id":0,"fx":187}}' http://wled/json
{"success":true}
$ curl -s http://wled/json/state | jq '.seg[0].fx'
202

The chain producing this:

  1. HA sends effect="RSVD" to python-wled's segment() setter.
  2. python-wled resolves the name to the first matching effect_id (e.g. 53).
  3. WLED's Segment::setMode explicitly skips reserved slots:
    Segment &Segment::setMode(uint8_t fx, bool loadDefaults) {
      // skip reserved
      while (fx < strip.getModeCount() && strncmp_P("RSVD", strip.getModeData(fx), 4) == 0) fx++;
      if (fx >= strip.getModeCount()) fx = 0; // set solid mode
      ...
    }
  4. seg.mode advances to the next non-reserved slot. HA reads it back and shows that effect's name in the dropdown.

The firmware itself treats "RSVD" as something that must never be a final selection — but this is invisible to python-wled.

Where python-wled parses effects

src/wled/models.py (both Device.__pre_deserialize__ and Device.update_from_dict) enumerates the array as-is, with no filter:

if _effects := d.get("effects"):
    d["effects"] = {
        effect_id: {"effect_id": effect_id, "name": name}
        for effect_id, name in enumerate(_effects)
    }

For comparison, the WLED web UI filters at wled00/data/index.js @ v16.0.0, line 951:

if (ef.name.indexOf("RSVD") < 0) {
    html += generateListItemHtml('fx', id, nm, 'setFX', '', fd);
}

"RSVD" is an established WLED-side convention (defined as _data_RESERVED in wled00/FX.cpp @ v16.0.0, line 10948) — emitted on the wire to keep array indices stable, filtered out at the presentation layer.

Where to fix this

  1. WLED firmware (drop RSVD from JSON). Not feasible without an API redesign: clients derive effect_id from the array index via enumerate(), and seg.fx only accepts the integer ID (not a name). Removing entries would shift every subsequent ID and silently break presets and automations.

  2. python-wled (preferred). Mirror the WLED web UI: keep enumerate() over the raw array so effect_id stays aligned with the firmware's internal IDs, but drop placeholders before they reach consumers.

  3. HA wled core integration. Solves it for HA only; every other python-wled consumer keeps seeing RSVD. It also pushes WLED-domain knowledge into HA, conflicting with the platinum-tier guideline that domain logic belongs in the library.

I'm filing this here (option 2) because RSVD is a WLED-API convention, not a Home-Assistant concern.

Suggested patch

In both __pre_deserialize__ and update_from_dict:

_RESERVED_EFFECT_NAMES: frozenset[str] = frozenset({"RSVD"})

if _effects := d.get("effects"):
    d["effects"] = {
        effect_id: {"effect_id": effect_id, "name": name}
        for effect_id, name in enumerate(_effects)
        if name not in _RESERVED_EFFECT_NAMES
    }

The filter must run after enumerate() so effect_id continues to match the firmware-side index. Happy to open a PR — let me know if you'd prefer a different shape (e.g. an include_reserved=True escape hatch).

Environment

  • WLED firmware: v16.0.0 (stock ESP8266, no WLED_ENABLE_GIF)
  • python-wled: 0.22.0
  • Home Assistant Core: 2026.4.4

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions