Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Volume Increment Setting — Design

## Overview

Expose the volume increment for the Volume Up and Volume Down actions as a configurable setting. A global default applies to all volume buttons; individual buttons can override it.

## Requirements

- Global default increment: `10` (min: `1`, no enforced max)
- Per-button override: optional; absent means "use global"
- Volume range hint displayed in UI: "Volume range: 0–100"
- Fix manifest tooltips: "by 2" → "by 10"

## Architecture

### Global settings (`PiComponent.vue` + `PluginComponent.vue`)

Add `adjustVolumeIncrement` to the global settings payload:

- **PI (`PiComponent.vue`):** Add a number input in the Global Settings accordion, below the existing timeout/interval inputs. Label: "Volume Increment (Up/Down)". Helper text: "Volume range: 0–100". Min: 1.
- **`saveGlobalSettings()`:** Include `adjustVolumeIncrement` in the payload saved via `streamDeckConnection.value.saveGlobalSettings()`.
- **`PluginComponent.vue`:** On `globalsettings`, extract `inGlobalSettings.adjustVolumeIncrement` (defaulting to `10`) into a reactive ref `adjustVolumeIncrement`, mirroring the existing `deviceTimeoutDuration` pattern.

### Per-button settings (`PiComponent.vue`)

For `actionName === 'volume-up'` and `actionName === 'volume-down'`, show an action-specific section with a number input for `adjustVolumeIncrement`. Label: "Volume Increment". Helper: "Volume range: 0–100. Leave blank to use the global default." Min: 1. The field is optional — only written to `actionSettings` when the user provides a value; absent/null means inherit from global.

The `saveSettings()` function already writes all `actionSettings` fields unconditionally. The per-button `adjustVolumeIncrement` should only be included when the user has set a value (i.e. the ref is non-null/non-empty).

### Action handlers (`src/modules/actions/sonosController.js`)

Add `globalAdjustVolumeIncrement` as a named parameter to `volume_up_action` and `volume_down_action`. Resolution:

```js
const increment = parseInt(inActionSettings.adjustVolumeIncrement) || globalAdjustVolumeIncrement;
```

Remove the existing `|| 10` fallback — the global value is always present.

### Passing global value to actions (`PluginComponent.vue`)

Where action handlers are called, pass `globalAdjustVolumeIncrement: adjustVolumeIncrement.value` alongside the existing `deviceTimeoutDuration`, mirroring that pattern exactly.

## Manifest fix

In `public/manifest.json`, remove the hardcoded increment from both tooltips (it would become inaccurate once the setting is configurable):
- Volume Up `Tooltip`: `"Increase volume by 2"` → `"Increase volume"`
- Volume Down `Tooltip`: `"Decrease volume by 2"` → `"Decrease volume"`
4 changes: 2 additions & 2 deletions public/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,7 @@
"Icon": "images/actions/volume_up",
"Name": "Volume Up",
"PropertyInspectorPath": "pi.html",
"Tooltip": "Increase volume by 2",
"Tooltip": "Increase volume",
"UUID": "com.r-teller.sonoscontroller.volume-up",
"Controllers": [
"Keypad"
Expand All @@ -233,7 +233,7 @@
"Icon": "images/actions/volume_down",
"Name": "Volume Down",
"PropertyInspectorPath": "pi.html",
"Tooltip": "Decrease volume by 2",
"Tooltip": "Decrease volume",
"UUID": "com.r-teller.sonoscontroller.volume-down",
"Controllers": [
"Keypad"
Expand Down
38 changes: 38 additions & 0 deletions src/components/PiComponent.vue
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,23 @@
</div>
</div>

<div v-if="sonosConnectionState === OPERATIONAL_STATUS.CONNECTED && isVolumeAction">
<h1>Volume Increment</h1>
<div class="d-flex flex-column gap-2 mb-3">
<label class="form-label" for="perButtonAdjustVolumeIncrement">Override increment (leave empty to use global default)</label>
<small class="text-muted d-block">Volume range: 0–100</small>
<input
id="perButtonAdjustVolumeIncrement"
v-model.number="perButtonAdjustVolumeIncrement"
class="form-control form-control-sm"
type="number"
min="1"
:placeholder="`Global default: ${adjustVolumeIncrement}`"
@change="saveSettings"
/>
</div>
</div>

<div v-if="sonosConnectionState === OPERATIONAL_STATUS.CONNECTED && isEncoderAudioEqualizer">
<h1>Equalizer Target</h1>
<div class="d-flex flex-column gap-2 mb-3">
Expand Down Expand Up @@ -155,6 +172,15 @@
>Note: This interval is used to check the status of the device selected for this action (in seconds)</small
>
<input id="deviceCheckInterval" v-model="deviceCheckInterval" class="form-control form-control-sm" type="number" />
<label class="form-label" for="adjustVolumeIncrement">Volume Increment (Up/Down)</label>
<small class="text-muted d-block">Note: Volume range is 0–100. Used by Volume Up and Volume Down actions unless overridden per-button.</small>
<input
id="adjustVolumeIncrement"
v-model.number="adjustVolumeIncrement"
class="form-control form-control-sm"
type="number"
min="1"
/>
</div>

<div v-if="sonosError" class="alert alert-danger alert-dismissible" role="alert">
Expand Down Expand Up @@ -197,6 +223,7 @@ const sonosError = ref("");
const primaryDeviceAddress = ref("");
const deviceCheckInterval = ref(10);
const deviceTimeoutDuration = ref(5);
const adjustVolumeIncrement = ref(10);
const sonosConnectionState = ref(OPERATIONAL_STATUS.DISCONNECTED);
const availableSonosSpeakers = ref([]);
const actionSettings = ref({});
Expand Down Expand Up @@ -247,6 +274,9 @@ const isEncoderAudioEqualizer = ref(false);
const availableEqualizerTargets = ref(["volume", "bass", "treble"]);
const encoderAudioEqualizerTarget = ref("");

const isVolumeAction = ref(false);
const perButtonAdjustVolumeIncrement = ref(null);

onMounted(() => {
window.connectElgatoStreamDeckSocket = (exPort, exPropertyInspectorUUID, exRegisterEvent, exInfo, exActionInfo) => {
streamDeckConnection.value = new StreamDeck(exPort, exPropertyInspectorUUID, exRegisterEvent, exInfo, exActionInfo);
Expand Down Expand Up @@ -275,6 +305,7 @@ onMounted(() => {
if (inGlobalSettings.devices) {
deviceCheckInterval.value = inGlobalSettings.deviceCheckInterval;
deviceTimeoutDuration.value = inGlobalSettings.deviceTimeoutDuration;
adjustVolumeIncrement.value = inGlobalSettings.adjustVolumeIncrement ?? 10;
const primaryDevice = Object.values(inGlobalSettings.devices).find((device) => device.primary === true);
if (primaryDevice) {
primaryDeviceAddress.value = primaryDevice.hostAddress;
Expand Down Expand Up @@ -314,6 +345,11 @@ onMounted(() => {
encoderAudioEqualizerTarget.value = "VOLUME";
}
break;
case "volume-up":
case "volume-down":
isVolumeAction.value = true;
perButtonAdjustVolumeIncrement.value = actionSettings.value?.adjustVolumeIncrement ?? null;
break;
case "play-sonos-favorite":
isPlaySonosFavorite.value = true;
availableSonosFavorites.value = inGlobalSettings.favorites.map((favorite) => ({
Expand Down Expand Up @@ -420,6 +456,7 @@ async function saveGlobalSettings() {
devices: getDevices.list,
deviceCheckInterval: deviceCheckInterval.value,
deviceTimeoutDuration: deviceTimeoutDuration.value,
adjustVolumeIncrement: Math.max(1, adjustVolumeIncrement.value),
favorites: getFavorites.list,
},
});
Expand Down Expand Up @@ -454,6 +491,7 @@ function saveSettings() {
albumArtURI: selectedSonosFavorite.value.albumArtURI,
}
: null,
adjustVolumeIncrement: perButtonAdjustVolumeIncrement.value ?? null,
};
streamDeckConnection.value.saveSettings({
actionSettings: actionSettings.value,
Expand Down
3 changes: 3 additions & 0 deletions src/components/PluginComponent.vue
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ const actionSettings = ref([]);

const deviceCheckInterval = ref(10);
const deviceTimeoutDuration = ref(5);
const adjustVolumeIncrement = ref(10);

// At some point we may want to add a speaker check interval and timeout duration
// const speakerCheckInterval = ref(10);
Expand Down Expand Up @@ -150,6 +151,7 @@ onMounted(async () => {
globalSettings.value = inGlobalSettings;
deviceCheckInterval.value = inGlobalSettings.deviceCheckInterval;
deviceTimeoutDuration.value = inGlobalSettings.deviceTimeoutDuration;
adjustVolumeIncrement.value = inGlobalSettings.adjustVolumeIncrement ?? 10;
});

streamDeckConnection.value.on("willDisappear", (inMessage) => {
Expand Down Expand Up @@ -363,6 +365,7 @@ function callAction({ inContext, inEvent, inRotation = null }) {
inSonosSpeakerState: speaker.state,
inRotation,
deviceTimeoutDuration: deviceTimeoutDuration.value,
globalAdjustVolumeIncrement: adjustVolumeIncrement.value,
});
if (actionResult.status === "SUCCESS") {
sonosSpeakers.updateSpeakerState({
Expand Down
8 changes: 4 additions & 4 deletions src/modules/actions/sonosController.js
Original file line number Diff line number Diff line change
Expand Up @@ -1086,7 +1086,7 @@ export async function play_previous_track_action({
* @property {string} message - The message describing the action
* @property {object} updatedSonosSpeakerState - The updated state of the Sonos speaker
*/
export async function volume_up_action({ inContext, inActionSettings, inSonosSpeakerState, deviceTimeoutDuration = 1 }) {
export async function volume_up_action({ inContext, inActionSettings, inSonosSpeakerState, deviceTimeoutDuration = 1, globalAdjustVolumeIncrement = 10 }) {
const functionName = "[Volume Up Action]";
if (!inSonosSpeakerState) {
console.log(`${functionName} inSonosSpeakerState is undefined for context ${inContext}`);
Expand All @@ -1106,7 +1106,7 @@ export async function volume_up_action({ inContext, inActionSettings, inSonosSpe

const updatedVolume = Math.min(
100,
parseInt(inSonosSpeakerState.audioEqualizer.volume) + (parseInt(inActionSettings.adjustVolumeIncrement) || 10),
parseInt(inSonosSpeakerState.audioEqualizer.volume) + (parseInt(inActionSettings.adjustVolumeIncrement) || globalAdjustVolumeIncrement),
);

const volumeUpState = await Promise.race([sonosController.setVolume(updatedVolume), timeout]);
Expand Down Expand Up @@ -1153,7 +1153,7 @@ export async function volume_up_action({ inContext, inActionSettings, inSonosSpe
* @property {string} message - The message describing the action
* @property {object} updatedSonosSpeakerState - The updated state of the Sonos speaker
*/
export async function volume_down_action({ inContext, inActionSettings, inSonosSpeakerState, deviceTimeoutDuration = 1 }) {
export async function volume_down_action({ inContext, inActionSettings, inSonosSpeakerState, deviceTimeoutDuration = 1, globalAdjustVolumeIncrement = 10 }) {
const functionName = "[Volume Down Action]";
if (!inSonosSpeakerState) {
console.log(`${functionName} inSonosSpeakerState is undefined for context ${inContext}`);
Expand All @@ -1173,7 +1173,7 @@ export async function volume_down_action({ inContext, inActionSettings, inSonosS

const updatedVolume = Math.max(
0,
parseInt(inSonosSpeakerState.audioEqualizer.volume) - (parseInt(inActionSettings.adjustVolumeIncrement) || 10),
parseInt(inSonosSpeakerState.audioEqualizer.volume) - (parseInt(inActionSettings.adjustVolumeIncrement) || globalAdjustVolumeIncrement),
);

const volumeDownState = await Promise.race([sonosController.setVolume(updatedVolume), timeout]);
Expand Down