homebridge-sonos-scenes 0.1.3 → 0.1.4
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 +5 -0
- package/broker/.env.example +5 -0
- package/broker/README.md +41 -0
- package/broker/package.json +15 -0
- package/broker/src/server.mjs +151 -0
- package/dist/src/cloud/brokerClient.d.ts +3 -0
- package/dist/src/cloud/brokerClient.js.map +1 -1
- package/dist/src/ui/serverApi.d.ts +7 -0
- package/dist/src/ui/serverApi.js +26 -0
- package/dist/src/ui/serverApi.js.map +1 -1
- package/docs/cloud-broker.md +2 -0
- package/homebridge-ui/public/index.html +328 -0
- package/homebridge-ui/server.js +9 -0
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -41,6 +41,9 @@ examples/
|
|
|
41
41
|
sample-topology.json
|
|
42
42
|
docs/
|
|
43
43
|
cloud-broker.md
|
|
44
|
+
broker/
|
|
45
|
+
README.md
|
|
46
|
+
src/server.mjs
|
|
44
47
|
test/
|
|
45
48
|
```
|
|
46
49
|
|
|
@@ -97,6 +100,8 @@ This project does not host that broker for users. The goal is an optional self-h
|
|
|
97
100
|
|
|
98
101
|
The config model already reserves a `cloud` section so advanced users and future versions do not need a breaking config redesign later. The broker contract is documented in [docs/cloud-broker.md](docs/cloud-broker.md).
|
|
99
102
|
|
|
103
|
+
There is also an early self-hosted broker scaffold in [broker/README.md](broker/README.md). It is not wired into scene execution yet, but it gives self-hosters a concrete service shape and a live `/v1/status` endpoint to target.
|
|
104
|
+
|
|
100
105
|
## Official Sonos References
|
|
101
106
|
|
|
102
107
|
These docs informed the product boundaries and future cloud-adapter shape:
|
package/broker/README.md
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# sonos-scenes-broker
|
|
2
|
+
|
|
3
|
+
This is the first self-hosted broker scaffold for `homebridge-sonos-scenes`.
|
|
4
|
+
|
|
5
|
+
It is intentionally small:
|
|
6
|
+
|
|
7
|
+
- it starts a local HTTP server
|
|
8
|
+
- it exposes `GET /healthz`
|
|
9
|
+
- it exposes `GET /v1/status`
|
|
10
|
+
- it reserves the future Sonos broker routes with `501 Not Implemented` responses
|
|
11
|
+
|
|
12
|
+
It does **not** implement Sonos OAuth or Sonos cloud playback yet. The point of this scaffold is to give self-hosters a concrete package and endpoint shape to build from.
|
|
13
|
+
|
|
14
|
+
## Run It
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
cd broker
|
|
18
|
+
node src/server.mjs
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
With environment variables:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
BROKER_PORT=8787
|
|
25
|
+
BROKER_HOST=127.0.0.1
|
|
26
|
+
BROKER_API_KEY=replace-me
|
|
27
|
+
node src/server.mjs
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Current Endpoints
|
|
31
|
+
|
|
32
|
+
- `GET /healthz`
|
|
33
|
+
- `GET /v1/status`
|
|
34
|
+
- `GET /v1/households`
|
|
35
|
+
- `GET /v1/households/:householdId/groups`
|
|
36
|
+
- `GET /v1/households/:householdId/favorites`
|
|
37
|
+
- `GET /v1/households/:householdId/playlists`
|
|
38
|
+
- `POST /v1/groups/:groupId/favorites/load`
|
|
39
|
+
- `POST /v1/groups/:groupId/playlists/load`
|
|
40
|
+
|
|
41
|
+
The `status` endpoint is meant to be usable by the Homebridge plugin right away for configuration checks. The other endpoints are placeholders for the future Sonos OAuth and cloud-control work.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "sonos-scenes-broker",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"private": true,
|
|
5
|
+
"description": "Self-hosted Sonos cloud broker scaffold for homebridge-sonos-scenes.",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"start": "node src/server.mjs",
|
|
9
|
+
"dev": "node --watch src/server.mjs"
|
|
10
|
+
},
|
|
11
|
+
"engines": {
|
|
12
|
+
"node": ">=18.20.0"
|
|
13
|
+
},
|
|
14
|
+
"license": "MIT"
|
|
15
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { createServer } from "node:http";
|
|
2
|
+
|
|
3
|
+
const port = Number(process.env.BROKER_PORT || 8787);
|
|
4
|
+
const host = process.env.BROKER_HOST || "127.0.0.1";
|
|
5
|
+
const brokerName = process.env.BROKER_NAME || "sonos-scenes-broker";
|
|
6
|
+
const apiKey = (process.env.BROKER_API_KEY || "").trim();
|
|
7
|
+
const docsUrl = process.env.BROKER_DOCS_URL || "https://github.com/applemanj/homebridge-sonos-scenes/blob/main/docs/cloud-broker.md";
|
|
8
|
+
|
|
9
|
+
function writeJson(response, statusCode, payload) {
|
|
10
|
+
response.writeHead(statusCode, {
|
|
11
|
+
"content-type": "application/json; charset=utf-8",
|
|
12
|
+
"cache-control": "no-store",
|
|
13
|
+
});
|
|
14
|
+
response.end(JSON.stringify(payload, null, 2));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function bearerToken(request) {
|
|
18
|
+
const header = request.headers.authorization || "";
|
|
19
|
+
const match = /^Bearer\s+(.+)$/i.exec(header);
|
|
20
|
+
return match?.[1]?.trim() || "";
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function isAuthorized(request) {
|
|
24
|
+
if (!apiKey) {
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return bearerToken(request) === apiKey;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function readJsonBody(request) {
|
|
32
|
+
const chunks = [];
|
|
33
|
+
for await (const chunk of request) {
|
|
34
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (chunks.length === 0) {
|
|
38
|
+
return {};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const raw = Buffer.concat(chunks).toString("utf8");
|
|
42
|
+
return raw.trim().length > 0 ? JSON.parse(raw) : {};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function notImplemented(response, route, extra = {}) {
|
|
46
|
+
writeJson(response, 501, {
|
|
47
|
+
ok: false,
|
|
48
|
+
route,
|
|
49
|
+
mode: "scaffold",
|
|
50
|
+
message: "This broker scaffold reserves the route, but Sonos OAuth and cloud playback are not implemented yet.",
|
|
51
|
+
...extra,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const server = createServer(async (request, response) => {
|
|
56
|
+
const url = new URL(request.url || "/", `http://${request.headers.host || `${host}:${port}`}`);
|
|
57
|
+
const pathname = url.pathname;
|
|
58
|
+
const method = request.method || "GET";
|
|
59
|
+
|
|
60
|
+
if (method === "GET" && pathname === "/healthz") {
|
|
61
|
+
writeJson(response, 200, {
|
|
62
|
+
ok: true,
|
|
63
|
+
name: brokerName,
|
|
64
|
+
mode: "scaffold",
|
|
65
|
+
});
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (method === "GET" && pathname === "/v1/status") {
|
|
70
|
+
writeJson(response, 200, {
|
|
71
|
+
ok: true,
|
|
72
|
+
name: brokerName,
|
|
73
|
+
version: "0.0.1",
|
|
74
|
+
mode: "scaffold",
|
|
75
|
+
oauthConfigured: false,
|
|
76
|
+
features: ["favorites", "playlists"],
|
|
77
|
+
docsUrl,
|
|
78
|
+
message: "Broker scaffold is running. Sonos OAuth is not configured yet.",
|
|
79
|
+
});
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (!isAuthorized(request)) {
|
|
84
|
+
writeJson(response, 401, {
|
|
85
|
+
ok: false,
|
|
86
|
+
message: "Unauthorized. Supply the configured broker bearer token.",
|
|
87
|
+
});
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
if (method === "GET" && pathname === "/v1/households") {
|
|
93
|
+
notImplemented(response, pathname, {
|
|
94
|
+
households: [],
|
|
95
|
+
});
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (method === "GET" && /^\/v1\/households\/[^/]+\/groups$/.test(pathname)) {
|
|
100
|
+
notImplemented(response, pathname, {
|
|
101
|
+
groups: [],
|
|
102
|
+
players: [],
|
|
103
|
+
});
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (method === "GET" && /^\/v1\/households\/[^/]+\/favorites$/.test(pathname)) {
|
|
108
|
+
notImplemented(response, pathname, {
|
|
109
|
+
favorites: [],
|
|
110
|
+
});
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (method === "GET" && /^\/v1\/households\/[^/]+\/playlists$/.test(pathname)) {
|
|
115
|
+
notImplemented(response, pathname, {
|
|
116
|
+
playlists: [],
|
|
117
|
+
});
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (method === "POST" && /^\/v1\/groups\/[^/]+\/favorites\/load$/.test(pathname)) {
|
|
122
|
+
const body = await readJsonBody(request);
|
|
123
|
+
notImplemented(response, pathname, {
|
|
124
|
+
received: body,
|
|
125
|
+
});
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (method === "POST" && /^\/v1\/groups\/[^/]+\/playlists\/load$/.test(pathname)) {
|
|
130
|
+
const body = await readJsonBody(request);
|
|
131
|
+
notImplemented(response, pathname, {
|
|
132
|
+
received: body,
|
|
133
|
+
});
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
writeJson(response, 404, {
|
|
138
|
+
ok: false,
|
|
139
|
+
message: `No route matched ${method} ${pathname}.`,
|
|
140
|
+
});
|
|
141
|
+
} catch (error) {
|
|
142
|
+
writeJson(response, 500, {
|
|
143
|
+
ok: false,
|
|
144
|
+
message: error instanceof Error ? error.message : String(error),
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
server.listen(port, host, () => {
|
|
150
|
+
console.log(`${brokerName} listening on http://${host}:${port}`);
|
|
151
|
+
});
|
|
@@ -6,6 +6,9 @@ export interface CloudBrokerStatus {
|
|
|
6
6
|
version?: string;
|
|
7
7
|
features: CloudBrokerFeature[];
|
|
8
8
|
docsUrl?: string;
|
|
9
|
+
mode?: "scaffold" | "live";
|
|
10
|
+
oauthConfigured?: boolean;
|
|
11
|
+
message?: string;
|
|
9
12
|
}
|
|
10
13
|
export declare class CloudBrokerClient {
|
|
11
14
|
private readonly config;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"brokerClient.js","sourceRoot":"","sources":["../../../src/cloud/brokerClient.ts"],"names":[],"mappings":";;;
|
|
1
|
+
{"version":3,"file":"brokerClient.js","sourceRoot":"","sources":["../../../src/cloud/brokerClient.ts"],"names":[],"mappings":";;;AAeA,SAAS,iBAAiB,CAAC,KAAa;IACtC,OAAO,KAAK,CAAC,OAAO,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC;AACpC,CAAC;AAED,MAAa,iBAAiB;IACC;IAA7B,YAA6B,MAAyB;QAAzB,WAAM,GAAN,MAAM,CAAmB;IAAG,CAAC;IAE1D,IAAI,UAAU;QACZ,OAAO,OAAO,IAAI,CAAC,MAAM,CAAC,GAAG,KAAK,QAAQ,IAAI,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,MAAM,GAAG,CAAC,CAAC;IAClF,CAAC;IAED,IAAI,OAAO;QACT,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,CAAC;YACrB,OAAO,SAAS,CAAC;QACnB,CAAC;QAED,OAAO,iBAAiB,CAAC,IAAI,CAAC,MAAM,CAAC,GAAI,CAAC,IAAI,EAAE,CAAC,CAAC;IACpD,CAAC;IAED,KAAK,CAAC,SAAS;QACb,OAAO,IAAI,CAAC,OAAO,CAAoB,YAAY,EAAE;YACnD,MAAM,EAAE,KAAK;SACd,CAAC,CAAC;IACL,CAAC;IAEO,KAAK,CAAC,OAAO,CAAI,IAAY,EAAE,IAAiB;QACtD,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC;YAClB,MAAM,IAAI,KAAK,CAAC,oCAAoC,CAAC,CAAC;QACxD,CAAC;QAED,MAAM,UAAU,GAAG,IAAI,eAAe,EAAE,CAAC;QACzC,MAAM,OAAO,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,UAAU,CAAC,KAAK,EAAE,EAAE,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;QAE5E,IAAI,CAAC;YACH,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,GAAG,IAAI,CAAC,OAAO,GAAG,IAAI,EAAE,EAAE;gBACrD,GAAG,IAAI;gBACP,OAAO,EAAE;oBACP,MAAM,EAAE,kBAAkB;oBAC1B,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,MAAM;wBACpB,CAAC,CAAC;4BACE,aAAa,EAAE,UAAU,IAAI,CAAC,MAAM,CAAC,MAAM,EAAE;yBAC9C;wBACH,CAAC,CAAC,EAAE,CAAC;oBACP,GAAG,IAAI,CAAC,OAAO;iBAChB;gBACD,MAAM,EAAE,UAAU,CAAC,MAAM;aAC1B,CAAC,CAAC;YAEH,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;gBACjB,MAAM,IAAI,KAAK,CAAC,gCAAgC,QAAQ,CAAC,MAAM,IAAI,QAAQ,CAAC,UAAU,EAAE,CAAC,CAAC;YAC5F,CAAC;YAED,OAAO,MAAM,QAAQ,CAAC,IAAI,EAAO,CAAC;QACpC,CAAC;gBAAS,CAAC;YACT,YAAY,CAAC,OAAO,CAAC,CAAC;QACxB,CAAC;IACH,CAAC;CACF;AArDD,8CAqDC"}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { CloudBrokerClient } from "../cloud/brokerClient";
|
|
1
2
|
import type { SceneDefinition, SceneRunResult, ScenesPlatformConfig, TopologySnapshot, ValidationResult } from "../types";
|
|
2
3
|
export declare function createDefaultUiConfig(): ScenesPlatformConfig;
|
|
3
4
|
export declare function discoverForUi(configInput: Partial<ScenesPlatformConfig> | undefined): Promise<{
|
|
@@ -9,3 +10,9 @@ export declare function validateSceneForUi(configInput: Partial<ScenesPlatformCo
|
|
|
9
10
|
snapshot: TopologySnapshot;
|
|
10
11
|
}>;
|
|
11
12
|
export declare function runTestForUi(configInput: Partial<ScenesPlatformConfig> | undefined, sceneInput: Partial<SceneDefinition>): Promise<SceneRunResult>;
|
|
13
|
+
export declare function checkBrokerForUi(configInput: Partial<ScenesPlatformConfig> | undefined): Promise<{
|
|
14
|
+
configured: boolean;
|
|
15
|
+
url?: string;
|
|
16
|
+
status?: Awaited<ReturnType<CloudBrokerClient["getStatus"]>>;
|
|
17
|
+
error?: string;
|
|
18
|
+
}>;
|
package/dist/src/ui/serverApi.js
CHANGED
|
@@ -4,7 +4,9 @@ exports.createDefaultUiConfig = createDefaultUiConfig;
|
|
|
4
4
|
exports.discoverForUi = discoverForUi;
|
|
5
5
|
exports.validateSceneForUi = validateSceneForUi;
|
|
6
6
|
exports.runTestForUi = runTestForUi;
|
|
7
|
+
exports.checkBrokerForUi = checkBrokerForUi;
|
|
7
8
|
const config_1 = require("../config");
|
|
9
|
+
const brokerClient_1 = require("../cloud/brokerClient");
|
|
8
10
|
const discoveryService_1 = require("../discoveryService");
|
|
9
11
|
const logger_1 = require("../logger");
|
|
10
12
|
const sceneRunner_1 = require("../sceneRunner");
|
|
@@ -49,4 +51,28 @@ async function runTestForUi(configInput, sceneInput) {
|
|
|
49
51
|
const scene = (0, config_1.normalizeScene)(sceneInput);
|
|
50
52
|
return services.sceneRunner.runTest(scene);
|
|
51
53
|
}
|
|
54
|
+
async function checkBrokerForUi(configInput) {
|
|
55
|
+
const config = (0, config_1.normalizePlatformConfig)(configInput);
|
|
56
|
+
const client = new brokerClient_1.CloudBrokerClient(config.cloud.broker);
|
|
57
|
+
if (!client.configured) {
|
|
58
|
+
return {
|
|
59
|
+
configured: false,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
try {
|
|
63
|
+
const status = await client.getStatus();
|
|
64
|
+
return {
|
|
65
|
+
configured: true,
|
|
66
|
+
url: client.baseUrl,
|
|
67
|
+
status,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
return {
|
|
72
|
+
configured: true,
|
|
73
|
+
url: client.baseUrl,
|
|
74
|
+
error: error instanceof Error ? error.message : String(error),
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
}
|
|
52
78
|
//# sourceMappingURL=serverApi.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"serverApi.js","sourceRoot":"","sources":["../../../src/ui/serverApi.ts"],"names":[],"mappings":";;
|
|
1
|
+
{"version":3,"file":"serverApi.js","sourceRoot":"","sources":["../../../src/ui/serverApi.ts"],"names":[],"mappings":"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"}
|
package/docs/cloud-broker.md
CHANGED
|
@@ -4,6 +4,8 @@ This project is intentionally local-first. For some Sonos favorites and playlist
|
|
|
4
4
|
|
|
5
5
|
This document defines the shape of that future broker so the Homebridge plugin can support a `local_plus_cloud` mode without forcing the maintainer to host user tokens.
|
|
6
6
|
|
|
7
|
+
An initial self-hosted scaffold now lives under [broker/](../broker/README.md). It exposes the status endpoint and reserves the future route surface, but it does not implement Sonos OAuth or cloud playback yet.
|
|
8
|
+
|
|
7
9
|
## Design Goals
|
|
8
10
|
|
|
9
11
|
- Keep the Homebridge plugin installable and useful with no cloud dependency.
|
|
@@ -27,6 +27,95 @@
|
|
|
27
27
|
color: rgba(232, 242, 255, 0.88);
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
.scene-mode-grid {
|
|
31
|
+
display: grid;
|
|
32
|
+
gap: 1rem;
|
|
33
|
+
grid-template-columns: minmax(280px, 1.1fr) minmax(320px, 1.3fr);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.scene-mode-panel {
|
|
37
|
+
border-radius: 1rem;
|
|
38
|
+
border: 1px solid rgba(22, 32, 51, 0.08);
|
|
39
|
+
box-shadow: 0 8px 24px rgba(15, 23, 42, 0.05);
|
|
40
|
+
background: white;
|
|
41
|
+
overflow: hidden;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.scene-mode-panel .card-header {
|
|
45
|
+
border-bottom: 1px solid rgba(22, 32, 51, 0.08);
|
|
46
|
+
background: rgba(255, 255, 255, 0.65);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.scene-mode-choice {
|
|
50
|
+
display: block;
|
|
51
|
+
border: 1px solid rgba(22, 32, 51, 0.1);
|
|
52
|
+
border-radius: 0.85rem;
|
|
53
|
+
padding: 0.85rem 0.95rem;
|
|
54
|
+
background: #fff;
|
|
55
|
+
cursor: pointer;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.scene-mode-choice.active {
|
|
59
|
+
border-color: rgba(10, 132, 255, 0.42);
|
|
60
|
+
background: rgba(10, 132, 255, 0.06);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.scene-mode-choice input {
|
|
64
|
+
margin-right: 0.65rem;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.scene-mode-choice-title {
|
|
68
|
+
color: var(--scene-ink);
|
|
69
|
+
font-weight: 600;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.scene-mode-choice-copy {
|
|
73
|
+
display: block;
|
|
74
|
+
color: #667085;
|
|
75
|
+
font-size: 0.9rem;
|
|
76
|
+
line-height: 1.45;
|
|
77
|
+
margin-top: 0.35rem;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.scene-cloud-settings {
|
|
81
|
+
display: grid;
|
|
82
|
+
gap: 1rem;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.scene-cloud-hidden {
|
|
86
|
+
display: none;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.scene-status-pill {
|
|
90
|
+
display: inline-flex;
|
|
91
|
+
align-items: center;
|
|
92
|
+
gap: 0.45rem;
|
|
93
|
+
padding: 0.25rem 0.7rem;
|
|
94
|
+
border-radius: 999px;
|
|
95
|
+
font-size: 0.8rem;
|
|
96
|
+
font-weight: 600;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
.scene-status-pill.local {
|
|
100
|
+
background: rgba(10, 132, 255, 0.08);
|
|
101
|
+
color: #0a84ff;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
.scene-status-pill.pending {
|
|
105
|
+
background: rgba(245, 158, 11, 0.14);
|
|
106
|
+
color: #b45309;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.scene-status-pill.success {
|
|
110
|
+
background: rgba(46, 213, 115, 0.14);
|
|
111
|
+
color: #0f9f50;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
.scene-status-pill.error {
|
|
115
|
+
background: rgba(239, 68, 68, 0.14);
|
|
116
|
+
color: #b42318;
|
|
117
|
+
}
|
|
118
|
+
|
|
30
119
|
.scene-grid {
|
|
31
120
|
display: grid;
|
|
32
121
|
gap: 1rem;
|
|
@@ -236,6 +325,10 @@
|
|
|
236
325
|
}
|
|
237
326
|
|
|
238
327
|
@media (max-width: 960px) {
|
|
328
|
+
.scene-mode-grid {
|
|
329
|
+
grid-template-columns: 1fr;
|
|
330
|
+
}
|
|
331
|
+
|
|
239
332
|
.scene-grid {
|
|
240
333
|
grid-template-columns: 1fr;
|
|
241
334
|
}
|
|
@@ -278,6 +371,80 @@
|
|
|
278
371
|
</div>
|
|
279
372
|
</section>
|
|
280
373
|
|
|
374
|
+
<section class="scene-mode-grid">
|
|
375
|
+
<section class="scene-mode-panel">
|
|
376
|
+
<div class="card-header d-flex justify-content-between align-items-center">
|
|
377
|
+
<div>
|
|
378
|
+
<strong>Execution Mode</strong>
|
|
379
|
+
<div class="scene-help">Choose whether scenes stay fully local or can use a self-hosted Sonos cloud broker later.</div>
|
|
380
|
+
</div>
|
|
381
|
+
<div id="cloud-mode-pill" class="scene-status-pill local">Local Only</div>
|
|
382
|
+
</div>
|
|
383
|
+
<div class="card-body d-grid gap-3">
|
|
384
|
+
<label class="scene-mode-choice" data-mode-choice="local_only">
|
|
385
|
+
<span class="d-flex align-items-start">
|
|
386
|
+
<input type="radio" name="cloud-mode" value="local_only">
|
|
387
|
+
<span>
|
|
388
|
+
<span class="scene-mode-choice-title">Local Only</span>
|
|
389
|
+
<span class="scene-mode-choice-copy">Best for line-in, grouping, TV, and favorites that already work over the local Sonos path. No external broker required.</span>
|
|
390
|
+
</span>
|
|
391
|
+
</span>
|
|
392
|
+
</label>
|
|
393
|
+
<label class="scene-mode-choice" data-mode-choice="local_plus_cloud">
|
|
394
|
+
<span class="d-flex align-items-start">
|
|
395
|
+
<input type="radio" name="cloud-mode" value="local_plus_cloud">
|
|
396
|
+
<span>
|
|
397
|
+
<span class="scene-mode-choice-title">Local + Cloud</span>
|
|
398
|
+
<span class="scene-mode-choice-copy">Future-ready mode for self-hosters who run their own Sonos cloud broker to support favorites and playlists that are not reliable locally.</span>
|
|
399
|
+
</span>
|
|
400
|
+
</span>
|
|
401
|
+
</label>
|
|
402
|
+
</div>
|
|
403
|
+
</section>
|
|
404
|
+
|
|
405
|
+
<section class="scene-mode-panel">
|
|
406
|
+
<div class="card-header d-flex justify-content-between align-items-center">
|
|
407
|
+
<div>
|
|
408
|
+
<strong>Cloud Broker</strong>
|
|
409
|
+
<div class="scene-help">Optional. Only needed if you choose Local + Cloud and run your own broker service.</div>
|
|
410
|
+
</div>
|
|
411
|
+
<button class="btn btn-sm btn-outline-primary" id="check-broker-button" type="button">Check Broker</button>
|
|
412
|
+
</div>
|
|
413
|
+
<div class="card-body scene-cloud-settings">
|
|
414
|
+
<div id="cloud-broker-copy" class="scene-help">Local Only is active. Broker settings are preserved, but they are not used until you switch modes.</div>
|
|
415
|
+
<div id="cloud-broker-fields" class="scene-cloud-hidden">
|
|
416
|
+
<div class="row g-3">
|
|
417
|
+
<div class="col-12">
|
|
418
|
+
<label class="form-label" for="broker-url">Broker URL</label>
|
|
419
|
+
<input class="form-control" id="broker-url" type="url" placeholder="https://sonos-broker.example.com">
|
|
420
|
+
</div>
|
|
421
|
+
<div class="col-md-8">
|
|
422
|
+
<label class="form-label" for="broker-api-key">Broker API Key</label>
|
|
423
|
+
<input class="form-control" id="broker-api-key" type="password" placeholder="Optional bearer token">
|
|
424
|
+
</div>
|
|
425
|
+
<div class="col-md-4">
|
|
426
|
+
<label class="form-label" for="broker-timeout-ms">Timeout (ms)</label>
|
|
427
|
+
<input class="form-control" id="broker-timeout-ms" type="number" min="1000">
|
|
428
|
+
</div>
|
|
429
|
+
<div class="col-md-6">
|
|
430
|
+
<label class="form-check">
|
|
431
|
+
<input class="form-check-input" id="broker-route-favorites" type="checkbox">
|
|
432
|
+
<span class="form-check-label">Route favorites through broker</span>
|
|
433
|
+
</label>
|
|
434
|
+
</div>
|
|
435
|
+
<div class="col-md-6">
|
|
436
|
+
<label class="form-check">
|
|
437
|
+
<input class="form-check-input" id="broker-route-playlists" type="checkbox">
|
|
438
|
+
<span class="form-check-label">Route playlists through broker</span>
|
|
439
|
+
</label>
|
|
440
|
+
</div>
|
|
441
|
+
</div>
|
|
442
|
+
</div>
|
|
443
|
+
<div id="broker-status-output" class="scene-help">No broker check has been run yet.</div>
|
|
444
|
+
</div>
|
|
445
|
+
</section>
|
|
446
|
+
</section>
|
|
447
|
+
|
|
281
448
|
<div class="scene-grid">
|
|
282
449
|
<section class="card scene-card">
|
|
283
450
|
<div class="card-header d-flex justify-content-between align-items-center">
|
|
@@ -398,6 +565,7 @@
|
|
|
398
565
|
activeSceneId: null,
|
|
399
566
|
validation: null,
|
|
400
567
|
lastRun: null,
|
|
568
|
+
brokerStatus: null,
|
|
401
569
|
};
|
|
402
570
|
|
|
403
571
|
const elements = {
|
|
@@ -424,6 +592,18 @@
|
|
|
424
592
|
validateButton: document.getElementById("validate-button"),
|
|
425
593
|
testButton: document.getElementById("test-button"),
|
|
426
594
|
saveHomebridgeButton: document.getElementById("save-homebridge-button"),
|
|
595
|
+
cloudModeInputs: Array.from(document.querySelectorAll("input[name='cloud-mode']")),
|
|
596
|
+
cloudModeChoices: Array.from(document.querySelectorAll("[data-mode-choice]")),
|
|
597
|
+
cloudModePill: document.getElementById("cloud-mode-pill"),
|
|
598
|
+
cloudBrokerCopy: document.getElementById("cloud-broker-copy"),
|
|
599
|
+
cloudBrokerFields: document.getElementById("cloud-broker-fields"),
|
|
600
|
+
brokerUrl: document.getElementById("broker-url"),
|
|
601
|
+
brokerApiKey: document.getElementById("broker-api-key"),
|
|
602
|
+
brokerTimeoutMs: document.getElementById("broker-timeout-ms"),
|
|
603
|
+
brokerRouteFavorites: document.getElementById("broker-route-favorites"),
|
|
604
|
+
brokerRoutePlaylists: document.getElementById("broker-route-playlists"),
|
|
605
|
+
brokerStatusOutput: document.getElementById("broker-status-output"),
|
|
606
|
+
checkBrokerButton: document.getElementById("check-broker-button"),
|
|
427
607
|
saveSceneButton: document.getElementById("save-scene-button"),
|
|
428
608
|
deleteSceneButton: document.getElementById("delete-scene-button"),
|
|
429
609
|
newSceneButton: document.getElementById("new-scene-button"),
|
|
@@ -474,6 +654,21 @@
|
|
|
474
654
|
};
|
|
475
655
|
}
|
|
476
656
|
|
|
657
|
+
function serializeCloudConfig() {
|
|
658
|
+
const mode = elements.cloudModeInputs.find((input) => input.checked)?.value || "local_only";
|
|
659
|
+
state.config.cloud = {
|
|
660
|
+
mode,
|
|
661
|
+
broker: {
|
|
662
|
+
url: elements.brokerUrl.value.trim(),
|
|
663
|
+
apiKey: elements.brokerApiKey.value.trim(),
|
|
664
|
+
timeoutMs: Number(elements.brokerTimeoutMs.value || 8000),
|
|
665
|
+
routeFavorites: Boolean(elements.brokerRouteFavorites.checked),
|
|
666
|
+
routePlaylists: Boolean(elements.brokerRoutePlaylists.checked),
|
|
667
|
+
},
|
|
668
|
+
};
|
|
669
|
+
return state.config.cloud;
|
|
670
|
+
}
|
|
671
|
+
|
|
477
672
|
function normalizeScene(scene) {
|
|
478
673
|
const safeScene = scene || {};
|
|
479
674
|
return {
|
|
@@ -527,6 +722,7 @@
|
|
|
527
722
|
}
|
|
528
723
|
|
|
529
724
|
function serializeDraft() {
|
|
725
|
+
serializeCloudConfig();
|
|
530
726
|
const draft = clone(state.draft);
|
|
531
727
|
draft.name = elements.sceneName.value.trim() || "New Scene";
|
|
532
728
|
draft.householdId = elements.householdSelect.value;
|
|
@@ -605,6 +801,85 @@
|
|
|
605
801
|
});
|
|
606
802
|
}
|
|
607
803
|
|
|
804
|
+
function renderCloudModeControls() {
|
|
805
|
+
const cloud = state.config.cloud || {
|
|
806
|
+
mode: "local_only",
|
|
807
|
+
broker: {
|
|
808
|
+
url: "",
|
|
809
|
+
apiKey: "",
|
|
810
|
+
timeoutMs: 8000,
|
|
811
|
+
routeFavorites: true,
|
|
812
|
+
routePlaylists: true,
|
|
813
|
+
},
|
|
814
|
+
};
|
|
815
|
+
|
|
816
|
+
elements.cloudModeInputs.forEach((input) => {
|
|
817
|
+
input.checked = input.value === cloud.mode;
|
|
818
|
+
});
|
|
819
|
+
|
|
820
|
+
elements.cloudModeChoices.forEach((choice) => {
|
|
821
|
+
choice.classList.toggle("active", choice.dataset.modeChoice === cloud.mode);
|
|
822
|
+
});
|
|
823
|
+
|
|
824
|
+
const isHybrid = cloud.mode === "local_plus_cloud";
|
|
825
|
+
elements.cloudModePill.textContent = isHybrid ? "Local + Cloud" : "Local Only";
|
|
826
|
+
elements.cloudModePill.className = `scene-status-pill ${isHybrid ? "pending" : "local"}`;
|
|
827
|
+
elements.cloudBrokerFields.classList.toggle("scene-cloud-hidden", !isHybrid);
|
|
828
|
+
elements.cloudBrokerCopy.textContent = isHybrid
|
|
829
|
+
? "Enter the URL for your self-hosted broker and optionally an API key. The plugin can then check that broker before cloud-backed scene support is wired in."
|
|
830
|
+
: "Local Only is active. Broker settings are preserved, but they are not used until you switch modes.";
|
|
831
|
+
|
|
832
|
+
elements.brokerUrl.value = cloud.broker?.url || "";
|
|
833
|
+
elements.brokerApiKey.value = cloud.broker?.apiKey || "";
|
|
834
|
+
elements.brokerTimeoutMs.value = cloud.broker?.timeoutMs ?? 8000;
|
|
835
|
+
elements.brokerRouteFavorites.checked = cloud.broker?.routeFavorites !== false;
|
|
836
|
+
elements.brokerRoutePlaylists.checked = cloud.broker?.routePlaylists !== false;
|
|
837
|
+
|
|
838
|
+
renderBrokerStatus();
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
function renderBrokerStatus() {
|
|
842
|
+
const cloud = state.config.cloud;
|
|
843
|
+
const status = state.brokerStatus;
|
|
844
|
+
|
|
845
|
+
if (cloud.mode !== "local_plus_cloud") {
|
|
846
|
+
elements.brokerStatusOutput.innerHTML = `<span class="scene-help">Broker checks are optional while Local Only is active.</span>`;
|
|
847
|
+
return;
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
if (!cloud.broker.url) {
|
|
851
|
+
elements.brokerStatusOutput.innerHTML = `<span class="scene-status-pill pending">Needs URL</span> <span class="scene-help">Enter your self-hosted broker URL, then click Check Broker.</span>`;
|
|
852
|
+
return;
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
if (!status) {
|
|
856
|
+
elements.brokerStatusOutput.innerHTML = `<span class="scene-status-pill pending">Not Checked</span> <span class="scene-help">Broker configured at ${cloud.broker.url}. Run a check to verify it responds.</span>`;
|
|
857
|
+
return;
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
if (status.error) {
|
|
861
|
+
elements.brokerStatusOutput.innerHTML = `<span class="scene-status-pill error">Broker Error</span> <span class="scene-help">${status.error}</span>`;
|
|
862
|
+
return;
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
const brokerStatus = status.status;
|
|
866
|
+
if (!brokerStatus) {
|
|
867
|
+
elements.brokerStatusOutput.innerHTML = `<span class="scene-status-pill pending">Unknown</span> <span class="scene-help">The broker returned no status payload.</span>`;
|
|
868
|
+
return;
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
const featureText = Array.isArray(brokerStatus.features) && brokerStatus.features.length > 0
|
|
872
|
+
? brokerStatus.features.join(", ")
|
|
873
|
+
: "none reported";
|
|
874
|
+
const readiness = brokerStatus.oauthConfigured === false
|
|
875
|
+
? "OAuth is not configured yet."
|
|
876
|
+
: brokerStatus.message || "Broker responded successfully.";
|
|
877
|
+
elements.brokerStatusOutput.innerHTML = `
|
|
878
|
+
<span class="scene-status-pill success">${brokerStatus.mode === "scaffold" ? "Scaffold" : "Ready"}</span>
|
|
879
|
+
<span class="scene-help"><strong>${brokerStatus.name || "Broker"}</strong> ${brokerStatus.version ? `v${brokerStatus.version}` : ""} at ${status.url || state.config.cloud.broker.url}. Features: ${featureText}. ${readiness}</span>
|
|
880
|
+
`;
|
|
881
|
+
}
|
|
882
|
+
|
|
608
883
|
function renderHouseholdOptions() {
|
|
609
884
|
const households = state.topology?.households || [];
|
|
610
885
|
const currentId = state.draft.householdId || state.config.defaultHouseholdId || households[0]?.id || "";
|
|
@@ -832,6 +1107,7 @@
|
|
|
832
1107
|
}
|
|
833
1108
|
|
|
834
1109
|
function renderInputs() {
|
|
1110
|
+
renderCloudModeControls();
|
|
835
1111
|
elements.sceneName.value = state.draft.name || "";
|
|
836
1112
|
elements.sceneId.value = state.draft.id || "";
|
|
837
1113
|
renderHouseholdOptions();
|
|
@@ -864,6 +1140,7 @@
|
|
|
864
1140
|
}
|
|
865
1141
|
|
|
866
1142
|
async function discover() {
|
|
1143
|
+
serializeCloudConfig();
|
|
867
1144
|
homebridge.showSpinner();
|
|
868
1145
|
try {
|
|
869
1146
|
const result = await homebridge.request("/discover", { config: state.config });
|
|
@@ -947,11 +1224,38 @@
|
|
|
947
1224
|
}
|
|
948
1225
|
|
|
949
1226
|
async function saveToHomebridge() {
|
|
1227
|
+
serializeCloudConfig();
|
|
950
1228
|
await persistConfig(false);
|
|
951
1229
|
await homebridge.savePluginConfig();
|
|
952
1230
|
homebridge.toast.success("Configuration saved to config.json.");
|
|
953
1231
|
}
|
|
954
1232
|
|
|
1233
|
+
async function checkBroker() {
|
|
1234
|
+
serializeCloudConfig();
|
|
1235
|
+
if (!state.config.cloud.broker.url) {
|
|
1236
|
+
state.brokerStatus = {
|
|
1237
|
+
error: "Enter a broker URL first.",
|
|
1238
|
+
};
|
|
1239
|
+
render();
|
|
1240
|
+
return;
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
homebridge.showSpinner();
|
|
1244
|
+
try {
|
|
1245
|
+
state.brokerStatus = await homebridge.request("/broker-status", {
|
|
1246
|
+
config: state.config,
|
|
1247
|
+
});
|
|
1248
|
+
render();
|
|
1249
|
+
} catch (error) {
|
|
1250
|
+
state.brokerStatus = {
|
|
1251
|
+
error: error.message || "Broker check failed.",
|
|
1252
|
+
};
|
|
1253
|
+
render();
|
|
1254
|
+
} finally {
|
|
1255
|
+
homebridge.hideSpinner();
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
|
|
955
1259
|
function bindEvents() {
|
|
956
1260
|
elements.refreshButton.addEventListener("click", discover);
|
|
957
1261
|
elements.validateButton.addEventListener("click", validateDraft);
|
|
@@ -960,6 +1264,29 @@
|
|
|
960
1264
|
elements.deleteSceneButton.addEventListener("click", deleteScene);
|
|
961
1265
|
elements.newSceneButton.addEventListener("click", newScene);
|
|
962
1266
|
elements.saveHomebridgeButton.addEventListener("click", saveToHomebridge);
|
|
1267
|
+
elements.checkBrokerButton.addEventListener("click", checkBroker);
|
|
1268
|
+
|
|
1269
|
+
elements.cloudModeInputs.forEach((input) => {
|
|
1270
|
+
input.addEventListener("change", () => {
|
|
1271
|
+
serializeCloudConfig();
|
|
1272
|
+
state.brokerStatus = null;
|
|
1273
|
+
render();
|
|
1274
|
+
});
|
|
1275
|
+
});
|
|
1276
|
+
|
|
1277
|
+
[
|
|
1278
|
+
elements.brokerUrl,
|
|
1279
|
+
elements.brokerApiKey,
|
|
1280
|
+
elements.brokerTimeoutMs,
|
|
1281
|
+
elements.brokerRouteFavorites,
|
|
1282
|
+
elements.brokerRoutePlaylists,
|
|
1283
|
+
].forEach((element) => {
|
|
1284
|
+
element.addEventListener("change", () => {
|
|
1285
|
+
serializeCloudConfig();
|
|
1286
|
+
state.brokerStatus = null;
|
|
1287
|
+
renderBrokerStatus();
|
|
1288
|
+
});
|
|
1289
|
+
});
|
|
963
1290
|
|
|
964
1291
|
elements.householdSelect.addEventListener("change", () => {
|
|
965
1292
|
serializeDraft();
|
|
@@ -993,6 +1320,7 @@
|
|
|
993
1320
|
state.config = Object.assign({}, state.config, schemaConfig, {
|
|
994
1321
|
scenes: state.config.scenes,
|
|
995
1322
|
});
|
|
1323
|
+
state.brokerStatus = null;
|
|
996
1324
|
});
|
|
997
1325
|
}
|
|
998
1326
|
|
package/homebridge-ui/server.js
CHANGED
|
@@ -19,6 +19,7 @@ class UiServer extends HomebridgePluginUiServer {
|
|
|
19
19
|
this.onRequest("/discover", this.handleDiscover.bind(this));
|
|
20
20
|
this.onRequest("/validate-scene", this.handleValidateScene.bind(this));
|
|
21
21
|
this.onRequest("/run-test", this.handleRunTest.bind(this));
|
|
22
|
+
this.onRequest("/broker-status", this.handleBrokerStatus.bind(this));
|
|
22
23
|
|
|
23
24
|
this.ready();
|
|
24
25
|
}
|
|
@@ -53,6 +54,14 @@ class UiServer extends HomebridgePluginUiServer {
|
|
|
53
54
|
}
|
|
54
55
|
}
|
|
55
56
|
|
|
57
|
+
async handleBrokerStatus(payload = {}) {
|
|
58
|
+
try {
|
|
59
|
+
return await this.serverApi.checkBrokerForUi(payload.config);
|
|
60
|
+
} catch (error) {
|
|
61
|
+
throw this.asRequestError(error, "Broker check failed");
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
56
65
|
asRequestError(error, fallbackMessage) {
|
|
57
66
|
if (error instanceof RequestError) {
|
|
58
67
|
return error;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "homebridge-sonos-scenes",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
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",
|
|
@@ -27,7 +27,8 @@
|
|
|
27
27
|
"README.md",
|
|
28
28
|
"LICENSE",
|
|
29
29
|
"examples",
|
|
30
|
-
"docs"
|
|
30
|
+
"docs",
|
|
31
|
+
"broker"
|
|
31
32
|
],
|
|
32
33
|
"keywords": [
|
|
33
34
|
"homebridge-plugin",
|