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 +4 -1
- package/docs/assets/icon-1024.png +0 -0
- package/homebridge-ui/public/index.html +73 -39
- package/package.json +1 -1
- /package/docs/assets/{homebridge-sonos-scenes-icon-256x256.png → icon-256.png} +0 -0
- /package/docs/assets/{homebridge-sonos-scenes-icon-512x512.png → icon-512.png} +0 -0
package/README.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<p align="center">
|
|
2
|
-
<img src="docs/assets/
|
|
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.
|
|
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
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
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
|
-
</
|
|
1144
|
-
|
|
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
|
File without changes
|
|
File without changes
|