Skip to content

feat: configurable volume increment for Volume Up/Down actions#4

Open
rodbegbie wants to merge 10 commits into
r-teller:mainfrom
rodbegbie:feature/volume-increment-setting
Open

feat: configurable volume increment for Volume Up/Down actions#4
rodbegbie wants to merge 10 commits into
r-teller:mainfrom
rodbegbie:feature/volume-increment-setting

Conversation

@rodbegbie

@rodbegbie rodbegbie commented Apr 5, 2026

Copy link
Copy Markdown

Human writing: This is a change I made to allow setting the increment value for the volume buttons, since the default of 10 was too jumpy for me. (I didn’t change the default from the existing 10). I included the “design doc” that Claude generated so you can understand the approach I guided it to take. (Everything after this is Claude writing, not me!)

Summary

  • Adds a configurable volume increment for Volume Up and Volume Down actions (previously hardcoded to 10)
  • A global default is set in the Global Settings panel, shared by all volume buttons
  • Each Volume Up/Down button can optionally override the increment independently
  • Manifest tooltips updated to remove the now-stale hardcoded amount

How it works

  • Global default: set in the Global Settings accordion (label: "Volume Increment (Up/Down)", min: 1, default: 10). Saved alongside existing global settings (deviceCheckInterval, deviceTimeoutDuration).
  • Per-button override: each Volume Up/Down button's Property Inspector shows an optional "Volume Increment" field. Leave it empty to use the global default; set a value to override just that button.
  • Resolution order: per-button value → global default (no hardcoded fallback)

Design doc

See docs/superpowers/specs/2026-04-05-volume-increment-setting-design.md for full design rationale.

Test plan

  • Open Global Settings → "Volume Increment (Up/Down)" field appears, defaults to 10, persists across reconnect
  • Open a Volume Up button PI → override field appears with global default as placeholder
  • Leave override empty → button uses global default
  • Set override to a different value → button uses that value regardless of global setting
  • Change global default → buttons without an override pick up the new value; overridden buttons are unaffected
  • Volume Down mirrors all of the above

🤖 Generated with Claude Code

@rodbegbie rodbegbie marked this pull request as ready for review April 5, 2026 21:41
- Use ?? instead of || when saving per-button override so a value of 1
  isn't treated as falsy and silently discarded
- Clamp global increment to Math.max(1, value) before persisting since
  HTML min="1" doesn't block Vue's v-model binding in Electron's webview

Co-Authored-By: Claude Sonnet 4.6 <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>
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.

1 participant