Skip to content

fix(profile): map 20GBD9901 to Stream Deck + (4 dials + 4x2 keypad)#25

Merged
jack-arturo merged 2 commits into
verygoodplugins:mainfrom
asasin235:fix/stream-deck-plus-model-id-20gbd9901
Apr 21, 2026
Merged

fix(profile): map 20GBD9901 to Stream Deck + (4 dials + 4x2 keypad)#25
jack-arturo merged 2 commits into
verygoodplugins:mainfrom
asasin235:fix/stream-deck-plus-model-id-20gbd9901

Conversation

@asasin235
Copy link
Copy Markdown
Contributor

Summary

The device model ID 20GBD9901 is currently mapped in MODEL_LAYOUTS to the Stream Deck Neo with a keypad-only layout:

# Stream Deck Neo (8 keys + touchscreen)
"20GBD9901": {KEYPAD: (4, 2)},

But an in-the-wild profile manifest written by the Elgato desktop app for this model contains a full Type: "Encoder" controller with 4 dial actions at positions 0,03,0 plus an 800×100 touchstrip Background image — matching the original Stream Deck + (2022), not the Neo (which has no rotary encoders).

Today, trying to write an encoder button to a 20GBD9901 profile raises:

ProfileValidationError: Device model does not expose a 'Encoder' controller.

This PR corrects the mapping so it matches the hardware actually in use.

The existing # NOTE in MODEL_LAYOUTS predicted this exact case and spelled out the fix:

…add that model's ID here (with ENCODER: (4, 1) and KEYPAD: (4, 2)) when seen.

Evidence

Redacted excerpts from a real 20GBD9901 profile written by the Elgato desktop app:

ProfilesV3/<profile>.sdProfile/manifest.json:

{"Device": {"Model": "20GBD9901", "UUID": "@(1)[4057/132/…]"}, }

ProfilesV3/<profile>.sdProfile/Profiles/<page>/manifest.json:

{
  "Controllers": [
    {
      "Type": "Encoder",
      "Background": "Images/…png",
      "Actions": {
        "0,0": {"Name": "Hotkey", },
        "1,0": {"Name": "Multimedia", },
        "2,0": {"Name": "Brightness", },
        "3,0": {"Name": "Hotkey", }
      }
    },
    {"Type": "Keypad", "Actions": {"0,0": …, "1,0": …, /* 8 entries across 4x2 */}}
  ]
}

Changes

  • profile_manager.py — replace the Neo mapping for 20GBD9901 with the correct Stream Deck + layout (KEYPAD: (4, 2), ENCODER: (4, 1)); remove the now-stale # NOTE that foreshadowed this change.
  • profile_manager.py — update MODEL_NAMES["20GBD9901"] from Stream Deck NeoStream Deck + so streamdeck_read_profiles reports the correct model name.
  • tests/test_profile_manager.py — add a sample_profiles_plus fixture modeled on sample_profiles_plus_xl, plus three regression tests:
    • test_plus_device_exposes_encoder_layoutread_page returns {keypad: 4x2, encoder: 4x1}
    • test_plus_device_accepts_encoder_writes — writing an encoder button to a 20GBD9901 profile succeeds and lands in the Encoder controller
    • test_plus_device_reports_correct_model_namelist_profiles surfaces ModelName: "Stream Deck +"

Context — related work

The encoder-writing machinery already supports this device fully — it was just being rejected at the model-lookup layer before reaching any of it.

Test plan

  • uv run pytest tests/ -q — 84 passed (81 existing + 3 new)
  • uv run ruff check profile_manager.py tests/test_profile_manager.py — clean
  • End-to-end: installed this branch into a venv, ran a profile-writing script against a real 20GBD9901 profile — previously errored on every page with Device model does not expose a 'Encoder' controller, now writes successfully.

🤖 Generated with Claude Code

The 20GBD9901 device model ID was mapped to "Stream Deck Neo" with a
keypad-only (4x2) layout. However, profile manifests written by the
Elgato desktop app for this model contain a full Encoder controller
with 4 dial actions at 0,0...3,0 plus an 800x100 touchstrip Background
image -- i.e. it is the original Stream Deck + (2022), not Neo.

Before this fix, write_page(..., buttons=[{"controller": "encoder", ...}])
against a 20GBD9901 profile raised:

    ProfileValidationError: Device model does not expose a 'Encoder' controller.

The existing NOTE in MODEL_LAYOUTS predicted this exact case and spelled
out the fix (ENCODER: (4, 1), KEYPAD: (4, 2)); this commit realizes it
and removes the now-stale NOTE.

Also updates MODEL_NAMES so streamdeck_read_profiles surfaces
"Stream Deck +" rather than "Stream Deck Neo" for this model ID.

Adds three regression tests driven by a 20GBD9901 fixture:
  - read_page exposes both {keypad: 4x2, encoder: 4x1} layouts
  - write_page accepts encoder buttons (previously rejected)
  - list_profiles reports ModelName "Stream Deck +"
Copilot AI review requested due to automatic review settings April 21, 2026 16:27
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes incorrect model metadata for device model 20GBD9901, aligning the expected controller layout with real-world Stream Deck + profile manifests so encoder/dial actions can be read/written without validation errors.

Changes:

  • Update MODEL_LAYOUTS for 20GBD9901 to include ENCODER: (4, 1) alongside KEYPAD: (4, 2).
  • Update MODEL_NAMES["20GBD9901"] to report Stream Deck + instead of Stream Deck Neo.
  • Add regression tests/fixture to ensure encoder layouts are exposed, encoder writes succeed, and the correct model name is reported.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

File Description
profile_manager.py Corrects 20GBD9901 layout + human-readable name to match Stream Deck + hardware capabilities.
tests/test_profile_manager.py Adds a Stream Deck + sample profile fixture and three regression tests covering layout exposure, encoder writes, and model naming.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread profile_manager.py
@jack-arturo
Copy link
Copy Markdown
Member

@copilot apply changes based on the comments in this thread

Address Copilot review on PR verygoodplugins#25: the hardware reference doc was still
mapping 20GBD9901 to Neo and marking Stream Deck + as ID-unknown. Swap
the rows in the models table, rewrite the Plus and Neo per-model
sections, and move the "ID unknown" status note from Plus to Neo.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@jack-arturo jack-arturo merged commit da0ada5 into verygoodplugins:main Apr 21, 2026
6 checks passed
@jack-arturo
Copy link
Copy Markdown
Member

Thanks @asasin235 ! 🙏

jack-arturo added a commit that referenced this pull request Apr 28, 2026
🤖 I have created a release *beep* *boop*
---


##
[0.3.0](v0.2.0...v0.3.0)
(2026-04-28)


### Features

* **icons:** bundle Material Design Icons for offline glyph rendering
([#19](#19))
([459d410](459d410))
* **profile:** preserve encoder controller + Stream Deck + XL layout
([#15](#15))
([f4a8402](f4a8402))
* **profile:** refuse writes while Elgato app is running + restart_app
path fix
([#16](#16))
([e1cb4e3](e1cb4e3))
* **skill:** streamdeck-designer Agent Skill + MCP transport hardening
([#22](#22))
([fd3ec6f](fd3ec6f))
* **touchstrip:** expose $X1/$A0/$A1/$B1/$B2/$C1 encoder layouts
([#21](#21))
([efa66e4](efa66e4))
* **touchstrip:** per-segment icon and background on Stream Deck + / +
XL ([#20](#20))
([a41a31c](a41a31c))


### Bug Fixes

* add Stream Deck XL and missing device layouts
([#10](#10))
([c89435b](c89435b))
* **profile:** correct Stream Deck + XL keypad layout to 9x4
([#18](#18))
([d6da4b2](d6da4b2))
* **profile:** map 20GBD9901 to Stream Deck + (4 dials + 4x2 keypad)
([#25](#25))
([da0ada5](da0ada5))

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants