fix(profile): map 20GBD9901 to Stream Deck + (4 dials + 4x2 keypad)#25
Merged
jack-arturo merged 2 commits intoApr 21, 2026
Conversation
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 +"
Contributor
There was a problem hiding this comment.
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_LAYOUTSfor20GBD9901to includeENCODER: (4, 1)alongsideKEYPAD: (4, 2). - Update
MODEL_NAMES["20GBD9901"]to reportStream Deck +instead ofStream 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.
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>
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
The device model ID
20GBD9901is currently mapped inMODEL_LAYOUTSto the Stream Deck Neo with a keypad-only layout: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 positions0,0…3,0plus an 800×100 touchstripBackgroundimage — matching the original Stream Deck + (2022), not the Neo (which has no rotary encoders).Today, trying to write an encoder button to a
20GBD9901profile raises:This PR corrects the mapping so it matches the hardware actually in use.
The existing
# NOTEinMODEL_LAYOUTSpredicted this exact case and spelled out the fix:Evidence
Redacted excerpts from a real
20GBD9901profile 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 for20GBD9901with the correct Stream Deck + layout (KEYPAD: (4, 2),ENCODER: (4, 1)); remove the now-stale# NOTEthat foreshadowed this change.profile_manager.py— updateMODEL_NAMES["20GBD9901"]fromStream Deck Neo→Stream Deck +sostreamdeck_read_profilesreports the correct model name.tests/test_profile_manager.py— add asample_profiles_plusfixture modeled onsample_profiles_plus_xl, plus three regression tests:test_plus_device_exposes_encoder_layout—read_pagereturns{keypad: 4x2, encoder: 4x1}test_plus_device_accepts_encoder_writes— writing an encoder button to a20GBD9901profile succeeds and lands in the Encoder controllertest_plus_device_reports_correct_model_name—list_profilessurfacesModelName: "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— clean20GBD9901profile — previously errored on every page withDevice model does not expose a 'Encoder' controller, now writes successfully.🤖 Generated with Claude Code