homebridge-sonos-scenes 0.1.6 → 0.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,5 +1,5 @@
1
1
  <p align="center">
2
- <img src="docs/assets/homebridge-sonos-scenes-icon-512x512.png" alt="homebridge-sonos-scenes icon" width="164">
2
+ <img src="docs/assets/icon-512.png" alt="homebridge-sonos-scenes icon" width="164">
3
3
  </p>
4
4
 
5
5
  <h1 align="center">homebridge-sonos-scenes</h1>
@@ -8,6 +8,9 @@
8
8
 
9
9
  `homebridge-sonos-scenes` is a Homebridge plugin scaffold for Sonos workflow scenes.
10
10
 
11
+ > [!IMPORTANT]
12
+ > This project is in an active early-testing phase. The local-first scene workflow is usable and published to npm, but cloud-backed Sonos playback is still planned work and some edge cases are still being hardened. If you try the plugin, please share bugs, UI feedback, and Sonos compatibility notes in [GitHub Issues](https://github.com/applemanj/homebridge-sonos-scenes/issues). Real-world feedback is especially helpful right now.
13
+
11
14
  The goal is not general Sonos control. The goal is a clean way to trigger multi-step Sonos workflows from Apple Home, such as:
12
15
 
13
16
  - grouping rooms around a coordinator,
Binary file
@@ -307,12 +307,22 @@
307
307
  display: block;
308
308
  }
309
309
 
310
+ .scene-member-pill.selected {
311
+ border-color: rgba(10, 132, 255, 0.34);
312
+ background: rgba(10, 132, 255, 0.07);
313
+ box-shadow: inset 0 0 0 1px rgba(10, 132, 255, 0.08);
314
+ }
315
+
310
316
  .scene-member-row {
311
317
  display: flex;
312
318
  align-items: flex-start;
313
319
  gap: 0.65rem;
314
320
  }
315
321
 
322
+ .scene-member-toggle {
323
+ cursor: pointer;
324
+ }
325
+
316
326
  .scene-member-pill .form-check-input {
317
327
  margin-top: 0.2rem;
318
328
  flex: 0 0 auto;
@@ -336,6 +346,25 @@
336
346
  line-height: 1.35;
337
347
  }
338
348
 
349
+ .scene-member-volume {
350
+ display: grid;
351
+ gap: 0.35rem;
352
+ margin-top: 0.65rem;
353
+ padding-top: 0.65rem;
354
+ border-top: 1px solid rgba(22, 32, 51, 0.08);
355
+ }
356
+
357
+ .scene-member-volume-label {
358
+ color: #556579;
359
+ font-size: 0.78rem;
360
+ font-weight: 600;
361
+ line-height: 1.3;
362
+ }
363
+
364
+ .scene-member-volume-input {
365
+ max-width: 132px;
366
+ }
367
+
339
368
  .scene-log {
340
369
  max-height: 280px;
341
370
  overflow: auto;
@@ -632,7 +661,7 @@
632
661
  <span>Group Members</span>
633
662
  <span class="scene-info-tag" tabindex="0" role="button" aria-label="Explain Group Members">
634
663
  ?
635
- <span class="scene-tooltip">Additional rooms to join the coordinator when the scene runs. Leave all unchecked for a single-room scene.</span>
664
+ <span class="scene-tooltip">Additional rooms to join the coordinator when the scene runs. Leave all unchecked for a single-room scene. Selected member tiles can also carry their own optional volume override.</span>
636
665
  </span>
637
666
  </span>
638
667
  </label>
@@ -737,18 +766,6 @@
737
766
  <option value="ungroup">Ungroup</option>
738
767
  </select>
739
768
  </div>
740
- <div class="col-12">
741
- <label class="form-label">
742
- <span class="scene-label-row">
743
- <span>Per-room Volumes</span>
744
- <span class="scene-info-tag" tabindex="0" role="button" aria-label="Explain Per-room Volumes">
745
- ?
746
- <span class="scene-tooltip">Optional volume overrides for specific member rooms. Leave a room blank here to keep its current volume or let the group volume behavior apply.</span>
747
- </span>
748
- </span>
749
- </label>
750
- <div id="volume-list" class="scene-member-grid"></div>
751
- </div>
752
769
  </div>
753
770
  </div>
754
771
  </section>
@@ -804,7 +821,6 @@
804
821
  retryDelayMs: document.getElementById("retry-delay-ms"),
805
822
  autoResetMs: document.getElementById("auto-reset-ms"),
806
823
  offBehavior: document.getElementById("off-behavior"),
807
- volumeList: document.getElementById("volume-list"),
808
824
  validationOutput: document.getElementById("validation-output"),
809
825
  logOutput: document.getElementById("log-output"),
810
826
  discoverySummary: document.getElementById("discovery-summary"),
@@ -956,7 +972,7 @@
956
972
  draft.retryDelayMs = Number(elements.retryDelayMs.value || 0);
957
973
  draft.autoResetMs = Number(elements.autoResetMs.value || 0);
958
974
  draft.offBehavior = { kind: elements.offBehavior.value };
959
- draft.playerVolumes = Array.from(elements.volumeList.querySelectorAll("input[data-player-id]"))
975
+ draft.playerVolumes = Array.from(elements.memberList.querySelectorAll("input[data-player-id]"))
960
976
  .filter((input) => input.value !== "")
961
977
  .map((input) => ({
962
978
  playerId: input.dataset.playerId,
@@ -1127,22 +1143,40 @@
1127
1143
  const household = getActiveHousehold();
1128
1144
  const coordinatorId = state.draft.coordinatorPlayerId;
1129
1145
  const selected = new Set(state.draft.memberPlayerIds.filter((playerId) => playerId !== coordinatorId));
1146
+ const values = new Map((state.draft.playerVolumes || []).map((entry) => [entry.playerId, entry.volume]));
1130
1147
  const players = (household?.players || []).filter((player) => player.id !== coordinatorId);
1131
1148
  elements.memberList.innerHTML = players.length === 0
1132
1149
  ? `<div class="scene-help">No additional players are available for grouping.</div>`
1133
1150
  : players
1134
1151
  .map(
1135
- (player) => `
1136
- <label class="scene-member-pill">
1137
- <span class="scene-member-row">
1138
- <input class="form-check-input" type="checkbox" value="${player.id}" ${selected.has(player.id) ? "checked" : ""}>
1152
+ (player) => {
1153
+ const isSelected = selected.has(player.id);
1154
+ return `
1155
+ <div class="scene-member-pill ${isSelected ? "selected" : ""}">
1156
+ <label class="scene-member-row scene-member-toggle">
1157
+ <input class="form-check-input" data-member-checkbox type="checkbox" value="${player.id}" ${isSelected ? "checked" : ""}>
1139
1158
  <span class="scene-member-copy">
1140
1159
  <span class="scene-member-title">${player.name}</span>
1141
1160
  <span class="scene-member-meta">${player.model || "Unknown model"} - ${((player.sourceOptions || []).join(", ") || "favorite").replaceAll("_", " ")}</span>
1142
1161
  </span>
1143
- </span>
1144
- </label>
1145
- `,
1162
+ </label>
1163
+ ${isSelected ? `
1164
+ <div class="scene-member-volume">
1165
+ <label class="scene-member-volume-label" for="member-volume-${player.id}">Volume Override</label>
1166
+ <input
1167
+ class="form-control form-control-sm scene-member-volume-input"
1168
+ id="member-volume-${player.id}"
1169
+ data-player-id="${player.id}"
1170
+ type="number"
1171
+ min="0"
1172
+ max="100"
1173
+ placeholder="Keep current"
1174
+ value="${values.get(player.id) ?? ""}">
1175
+ </div>
1176
+ ` : ""}
1177
+ </div>
1178
+ `;
1179
+ },
1146
1180
  )
1147
1181
  .join("");
1148
1182
  }
@@ -1201,22 +1235,6 @@
1201
1235
  .join("");
1202
1236
  }
1203
1237
 
1204
- function renderVolumeControls() {
1205
- const household = getActiveHousehold();
1206
- const selectedMembers = state.draft.memberPlayerIds || [];
1207
- const values = new Map((state.draft.playerVolumes || []).map((entry) => [entry.playerId, entry.volume]));
1208
- elements.volumeList.innerHTML = selectedMembers.length === 0
1209
- ? `<div class="scene-help">Select one or more member rooms to configure per-room volume overrides.</div>`
1210
- : selectedMembers
1211
- .map((playerId) => `
1212
- <label class="scene-member-pill">
1213
- <span class="scene-member-title d-block mb-1">${getPlayerName(playerId)}</span>
1214
- <input class="form-control" data-player-id="${playerId}" type="number" min="0" max="100" value="${values.get(playerId) ?? ""}">
1215
- </label>
1216
- `)
1217
- .join("");
1218
- }
1219
-
1220
1238
  function renderValidation() {
1221
1239
  if (!state.validation && !state.lastRun) {
1222
1240
  elements.validationOutput.innerHTML = `<div class="scene-help">Validation messages and test results will appear here.</div>`;
@@ -1342,7 +1360,6 @@
1342
1360
  elements.retryDelayMs.value = state.draft.retryDelayMs ?? 750;
1343
1361
  elements.autoResetMs.value = state.draft.autoResetMs ?? 1000;
1344
1362
  elements.offBehavior.value = state.draft.offBehavior?.kind || "none";
1345
- renderVolumeControls();
1346
1363
  }
1347
1364
 
1348
1365
  function render() {
@@ -1510,6 +1527,23 @@
1510
1527
  });
1511
1528
  });
1512
1529
 
1530
+ elements.memberList.addEventListener("change", (event) => {
1531
+ const target = event.target;
1532
+ if (!(target instanceof HTMLInputElement)) {
1533
+ return;
1534
+ }
1535
+
1536
+ if (target.matches("input[data-member-checkbox]")) {
1537
+ serializeDraft();
1538
+ renderMemberOptions();
1539
+ return;
1540
+ }
1541
+
1542
+ if (target.matches("input[data-player-id]")) {
1543
+ serializeDraft();
1544
+ }
1545
+ });
1546
+
1513
1547
  elements.householdSelect.addEventListener("change", () => {
1514
1548
  serializeDraft();
1515
1549
  state.draft.coordinatorPlayerId = "";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "homebridge-sonos-scenes",
3
- "version": "0.1.6",
3
+ "version": "0.1.7",
4
4
  "description": "Homebridge plugin for Sonos workflow scenes and orchestration.",
5
5
  "author": "applemanj",
6
6
  "homepage": "https://github.com/applemanj/homebridge-sonos-scenes#readme",