fix: normalise ZoneGroup to array for single zone group setups#3
Open
rodbegbie wants to merge 1 commit into
Open
fix: normalise ZoneGroup to array for single zone group setups#3rodbegbie wants to merge 1 commit into
rodbegbie wants to merge 1 commit into
Conversation
convertXmlToJson returns a plain object for single child elements, only promoting to array for duplicates. Wrap with [].concat() so getDevices/getDeviceLocation always iterate over an array regardless of how many zone groups are present. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
r-teller
added a commit
that referenced
this pull request
May 4, 2026
…Error)
Establishes the pattern that downstream Phase 2 beads (h75.3 getDevices,
h75.4 getFavorites, h75.6 play/pause/setVolume/etc.) use to produce
user-facing error messages.
src/modules/common/sonosErrors.js — new module:
- `SonosError` class with `category` (timeout|network|fault|http|parse|
unknown) + `context` ({host, port, timeoutMs, faultCode, faultDescription,
status, cause}). Carries enough info for the translator to produce a
per-op message without parsing strings.
- `translateSonosError(err, {op, host, port, timeoutSec})` helper. Returns
a new Error of the form "Failed to <op>: <translated cause>". Original
error preserved as `.cause` for diagnostics. Categories:
- timeout → "Timeout while reaching <host>:<port> after <N> seconds"
- network → "Could not reach <host>:<port>"
- fault → "Sonos returned error <code>: <description>"
- http → "Sonos returned HTTP <status>"
- parse → "Could not parse response from <host>:<port>"
- PR #3 safety net: TypeError matching /is not iterable|Cannot read prop/i
is translated to "Unexpected response shape from <host>:<port>" — the
user-visible safety net for any code path that forgot `asArray`. Should
be unreachable once h75.3 ships, but keeps a regression visible.
src/modules/common/sonosService.js — refactored:
- execute() now throws categorized SonosError instances instead of plain
Error. Same wire-format messages preserved; downstream consumers get
programmatic access to the failure category for translation.
Tests: 80 → 101 (added sonosErrors.test.js covering every category + the
safety net + the never-leaks-programmer-artifacts spirit; updated
sonosService.test.js for the new SonosError shape).
All gates green: lint, format:check, build, validate, 101/101 vitest.
Closes: streamdeck-sonoscontroller-h75.7
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
r-teller
added a commit
that referenced
this pull request
May 4, 2026
CLEANROOM authoring. The load-bearing fix for the prd-what.md §3.3
regression where one-element households surface as
"Failed to get devices: <name> is not iterable".
src/modules/common/sonosController.js — adds:
- `getDevices({setAsPrimary})` walks ZoneGroupState topology returning
DeviceRecord[]. Every iteration over ZoneGroup, ZoneGroupMember, and
Satellite goes through `asArray` — the discipline that prevents the
regression class. Wraps with translateSonosError({op: "get devices"})
so network/timeout/parse/safety-net failures all surface as
"Failed to get devices: ..." per prd-what.md §7.7.
- Static `SonosController.getDeviceLocation(member)` extracts host from
member._attributes.Location URL.
- Private `_memberToRecord(member, isSatellite, setAsPrimary)` produces
the canonical DeviceRecord shape per data-model.md.
Fixtures: tests/fixtures/sonos/zonegroupstate-{stereo-pair, home-theater,
multi-group}.xml — three new SOAP envelopes covering the AC#2/#3/#4
topologies. The single-group-single-member fixture from h75.2 covers AC#1
(the regression case).
Tests: 101 → 114 (13 new in sonosController.discovery.test.js covering
all 7 ACs: single-group regression, stereo pair, home theater
1+3 satellites, multi-group 4 members across 3 groups, primary marking
on/off, UUID stability, network-failure translation, safety-net
translation, getDeviceLocation host extraction).
Closes: streamdeck-sonoscontroller-h75.3
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
r-teller
added a commit
that referenced
this pull request
May 5, 2026
…very Phase 4 (Property Inspector) advances from 8% → 92% in one push. Plugin is now manually testable end-to-end against a real Sonos household: type IP → click Save and Connect → see speakers populate → bind a key to a speaker → configure per-action toggles. orw.2: BootstrapAccordeon + BootstrapAccordeonItem reusable primitives. forceExpanded prop drives initial show/collapse + aria-expanded; user clicks override via Bootstrap Collapse JS; reactive watcher syncs prop changes via .show()/.hide(). Listens to shown.bs.collapse / hidden.bs.collapse to track user-driven state. provide/inject for accordeonId. Establishes Vue component testing pattern via @vue/test-utils + jsdom — 11 tests cover forceExpanded, click toggle, aria, slot, id composition. orw.10: errorMessage ref + Bootstrap alert-danger inside Global Settings accordion. Hidden when null (no empty shell). X dismisses; second failure re-renders. Verbatim message text — no scrubbing (Phase 2 boundary translation owns the wrapping). orw.11: src/modules/pi/actionSettingsSchema.js — buildActionSettingsPayload owns the 16-field per-context schema with action-specific gating. PR #4 ?? null preserves value 1; base64-decode utility for favorite metadata round-trip. Allow-list constants exported (DISPLAY_*_ACTIONS, PLAY_MODE_DEFAULTS, INPUT_SOURCE_DEFAULTS). orw.12: src/modules/pi/globalSettingsSchema.js — buildGlobalSettingsPayload with Math.max(1, value) clamp on adjustVolumeIncrement at write site (HTML min="1" is bypassable in the Stream Deck Electron webview's v-model.number binding, so input-side validation alone is insufficient). orw.3: src/components/SonosSelection.vue — speaker dropdown (size=5) with case-insensitive sort by '<zoneName> (<host>) [🛰️]' label, grey hint line, free-text filter (label OR UUID, case-insensitive). v-model UUID + emits selection-saved on change. orw.5: 4 presentation toggles in PiComponent (Display State Based Title / Marquee Title / Marquee Album Title / Album Art). Each gated on per-action allow-list (allow-list constants from actionSettingsSchema). Auto-save on change. orw.6: 4 action-specific sections — Play Mode(s) / Input Source(s) / Equalizer Target / Sonos Favorite(s). Six play-mode switches with verbatim labels (Normal, Shuffle No Repeat, Shuffle Repeat One, Shuffle, Repeat One, Repeat All), three input-source switches, EQ select (Volume/Bass/Treble), favorites select keyed by URI. All auto-save. orw.7: Volume Increment override input (volume-up / volume-down only). Verbatim labels and helper text per prd-what.md §5.8. Placeholder reflects current global default. PR #4 ?? null preserves entered value 1. orw.8: Global Settings accordion built on orw.2 primitive with forceExpanded={!isConnected}. Four form fields with verbatim labels and hint text per prd-what.md §6. Reactive bindings to globalSettings ref. Math.max(1, value) clamp lives at the saveGlobalSettings write site (orw.12), not on the input. orw.9: Save and Connect / Save and Reconnect button. Label flips on connectionState. Spinner during flight. Disabled when address empty or in flight. Click handler: SonosController.connect → Promise.all(getDevices, getFavorites) sharing one controller (so zoneGroupState memoizes), persists via saveGlobalSettings, builds speakers picker. Brand-new actions default to discovery seed primary. Sonos Speakers accordion with embedded SonosSelection bound to settings.uuid via @selection-saved → saveSettings. Tests: +180 (210 → 390 total). 13 new test files cover schemas, primitives, every per-action visibility matrix, PR #3 tolerance fixtures (single-Move / stereo-pair / home-theater), error path, button state machine. Closes: streamdeck-sonoscontroller-orw.2 Closes: streamdeck-sonoscontroller-orw.3 Closes: streamdeck-sonoscontroller-orw.5 Closes: streamdeck-sonoscontroller-orw.6 Closes: streamdeck-sonoscontroller-orw.7 Closes: streamdeck-sonoscontroller-orw.8 Closes: streamdeck-sonoscontroller-orw.9 Closes: streamdeck-sonoscontroller-orw.10 Closes: streamdeck-sonoscontroller-orw.11 Closes: streamdeck-sonoscontroller-orw.12 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.
Human writing: Hi there. Was working on a tweak to the plugin, and was getting an "Failed to get devices: groups is not iterable” error when trying to set Global Settings, so this is the fix Claude Code made to address that. (I’ll open a separate PR for the feature work I did)
Summary
convertXmlToJsonreturns a plain object for single XML child elements, only promoting to an array when duplicate keys exist. When a Sonos network has only one zone group,ZoneGroups.ZoneGroupis an object rather than an array, causingfor...ofto throw "groups is not iterable".[].concat(...)in bothgetDeviceLocationandgetDevicesso iteration always works regardless of the number of zone groups.Test plan
🤖 Generated with Claude Code