homebridge-sonos-scenes 0.1.0

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.
Files changed (44) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +103 -0
  3. package/config.schema.json +86 -0
  4. package/dist/src/accessories/sceneSwitch.d.ts +17 -0
  5. package/dist/src/accessories/sceneSwitch.js +81 -0
  6. package/dist/src/accessories/sceneSwitch.js.map +1 -0
  7. package/dist/src/config.d.ts +7 -0
  8. package/dist/src/config.js +256 -0
  9. package/dist/src/config.js.map +1 -0
  10. package/dist/src/discoveryService.d.ts +10 -0
  11. package/dist/src/discoveryService.js +32 -0
  12. package/dist/src/discoveryService.js.map +1 -0
  13. package/dist/src/index.d.ts +3 -0
  14. package/dist/src/index.js +6 -0
  15. package/dist/src/index.js.map +1 -0
  16. package/dist/src/logger.d.ts +23 -0
  17. package/dist/src/logger.js +70 -0
  18. package/dist/src/logger.js.map +1 -0
  19. package/dist/src/platform.d.ts +23 -0
  20. package/dist/src/platform.js +111 -0
  21. package/dist/src/platform.js.map +1 -0
  22. package/dist/src/sampleTopology.d.ts +2 -0
  23. package/dist/src/sampleTopology.js +66 -0
  24. package/dist/src/sampleTopology.js.map +1 -0
  25. package/dist/src/sceneRunner.d.ts +20 -0
  26. package/dist/src/sceneRunner.js +186 -0
  27. package/dist/src/sceneRunner.js.map +1 -0
  28. package/dist/src/transports/index.d.ts +2 -0
  29. package/dist/src/transports/index.js +13 -0
  30. package/dist/src/transports/index.js.map +1 -0
  31. package/dist/src/transports/localTransport.d.ts +33 -0
  32. package/dist/src/transports/localTransport.js +544 -0
  33. package/dist/src/transports/localTransport.js.map +1 -0
  34. package/dist/src/types.d.ts +141 -0
  35. package/dist/src/types.js +6 -0
  36. package/dist/src/types.js.map +1 -0
  37. package/dist/src/ui/serverApi.d.ts +11 -0
  38. package/dist/src/ui/serverApi.js +52 -0
  39. package/dist/src/ui/serverApi.js.map +1 -0
  40. package/examples/config.example.json +48 -0
  41. package/examples/sample-topology.json +77 -0
  42. package/homebridge-ui/public/index.html +810 -0
  43. package/homebridge-ui/server.js +67 -0
  44. package/package.json +52 -0
@@ -0,0 +1,141 @@
1
+ import type { PlatformConfig } from "homebridge";
2
+ export declare const PLUGIN_NAME = "homebridge-sonos-scenes";
3
+ export declare const PLATFORM_NAME = "SonosScenes";
4
+ export type LogLevel = "debug" | "info" | "warn" | "error";
5
+ export type SceneSourceKind = "favorite" | "line_in" | "tv";
6
+ export type SceneTrigger = "on" | "off" | "test";
7
+ export interface FavoriteSource {
8
+ kind: "favorite";
9
+ favoriteId: string;
10
+ favoriteName?: string;
11
+ }
12
+ export interface LineInSource {
13
+ kind: "line_in";
14
+ deviceId: string;
15
+ deviceName?: string;
16
+ playOnCompletion?: boolean;
17
+ }
18
+ export interface TvSource {
19
+ kind: "tv";
20
+ deviceId: string;
21
+ deviceName?: string;
22
+ playOnCompletion?: boolean;
23
+ }
24
+ export type SceneSource = FavoriteSource | LineInSource | TvSource;
25
+ export interface SceneVolume {
26
+ playerId: string;
27
+ volume: number;
28
+ }
29
+ export interface OffBehaviorNone {
30
+ kind: "none";
31
+ }
32
+ export interface OffBehaviorUngroup {
33
+ kind: "ungroup";
34
+ }
35
+ export type SceneOffBehavior = OffBehaviorNone | OffBehaviorUngroup;
36
+ export interface SceneDefinition {
37
+ id: string;
38
+ name: string;
39
+ householdId: string;
40
+ coordinatorPlayerId: string;
41
+ memberPlayerIds: string[];
42
+ source?: SceneSource;
43
+ coordinatorVolume?: number;
44
+ playerVolumes: SceneVolume[];
45
+ offBehavior: SceneOffBehavior;
46
+ settleMs: number;
47
+ retryCount: number;
48
+ retryDelayMs: number;
49
+ autoResetMs: number;
50
+ }
51
+ export interface LocalTransportConfig {
52
+ kind: "local";
53
+ fixturePath?: string;
54
+ enableLiveDiscovery: boolean;
55
+ discoveryTimeoutMs: number;
56
+ requestTimeoutMs: number;
57
+ allowTvSource: boolean;
58
+ }
59
+ export interface ScenesPlatformConfig extends PlatformConfig {
60
+ platform: typeof PLATFORM_NAME;
61
+ name: string;
62
+ logLevel: LogLevel;
63
+ defaultHouseholdId?: string;
64
+ transport: LocalTransportConfig;
65
+ scenes: SceneDefinition[];
66
+ }
67
+ export interface SonosHouseholdSummary {
68
+ id: string;
69
+ displayName: string;
70
+ }
71
+ export interface SonosFavorite {
72
+ id: string;
73
+ name: string;
74
+ uri?: string;
75
+ }
76
+ export interface SonosPlayer {
77
+ id: string;
78
+ name: string;
79
+ model?: string;
80
+ capabilities: string[];
81
+ deviceIds: string[];
82
+ groupId?: string;
83
+ isCoordinator: boolean;
84
+ fixedVolume: boolean;
85
+ sourceOptions: SceneSourceKind[];
86
+ }
87
+ export interface SonosGroup {
88
+ id: string;
89
+ name: string;
90
+ coordinatorId: string;
91
+ playerIds: string[];
92
+ playbackState?: string;
93
+ }
94
+ export interface HouseholdSnapshot {
95
+ id: string;
96
+ displayName: string;
97
+ players: SonosPlayer[];
98
+ groups: SonosGroup[];
99
+ favorites: SonosFavorite[];
100
+ }
101
+ export interface TopologySnapshot {
102
+ capturedAt: string;
103
+ origin: "live" | "fixture";
104
+ households: HouseholdSnapshot[];
105
+ }
106
+ export interface SceneLogEntry {
107
+ timestamp: string;
108
+ level: LogLevel;
109
+ scope: string;
110
+ message: string;
111
+ sceneId?: string;
112
+ trigger?: SceneTrigger;
113
+ }
114
+ export interface SceneRunResult {
115
+ ok: boolean;
116
+ sceneId: string;
117
+ trigger: SceneTrigger;
118
+ logs: SceneLogEntry[];
119
+ errors: string[];
120
+ snapshot?: TopologySnapshot;
121
+ }
122
+ export interface ValidationResult {
123
+ valid: boolean;
124
+ errors: string[];
125
+ warnings: string[];
126
+ }
127
+ export interface SonosTransport {
128
+ readonly kind: string;
129
+ supportsSource(kind: SceneSourceKind): boolean;
130
+ discoverHouseholds(): Promise<SonosHouseholdSummary[]>;
131
+ discoverTopology(): Promise<TopologySnapshot>;
132
+ setGroupMembers(householdId: string, coordinatorPlayerId: string, memberPlayerIds: string[]): Promise<void>;
133
+ modifyGroupMembers(householdId: string, coordinatorPlayerId: string, membersToAdd: string[], membersToRemove: string[]): Promise<void>;
134
+ loadLineIn(householdId: string, coordinatorPlayerId: string, deviceId: string, playOnCompletion?: boolean): Promise<void>;
135
+ loadFavorite(householdId: string, coordinatorPlayerId: string, favoriteId: string): Promise<void>;
136
+ loadTv?(householdId: string, coordinatorPlayerId: string, deviceId: string, playOnCompletion?: boolean): Promise<void>;
137
+ setGroupVolume(householdId: string, coordinatorPlayerId: string, volume: number): Promise<void>;
138
+ setPlayerVolume(householdId: string, playerId: string, volume: number): Promise<void>;
139
+ ungroup(householdId: string, coordinatorPlayerId: string, memberPlayerIds?: string[]): Promise<void>;
140
+ subscribe?(listener: (snapshot: TopologySnapshot) => void): Promise<() => Promise<void> | void>;
141
+ }
@@ -0,0 +1,6 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PLATFORM_NAME = exports.PLUGIN_NAME = void 0;
4
+ exports.PLUGIN_NAME = "homebridge-sonos-scenes";
5
+ exports.PLATFORM_NAME = "SonosScenes";
6
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/types.ts"],"names":[],"mappings":";;;AAEa,QAAA,WAAW,GAAG,yBAAyB,CAAC;AACxC,QAAA,aAAa,GAAG,aAAa,CAAC"}
@@ -0,0 +1,11 @@
1
+ import type { SceneDefinition, SceneRunResult, ScenesPlatformConfig, TopologySnapshot, ValidationResult } from "../types";
2
+ export declare function createDefaultUiConfig(): ScenesPlatformConfig;
3
+ export declare function discoverForUi(configInput: Partial<ScenesPlatformConfig> | undefined): Promise<{
4
+ snapshot: TopologySnapshot;
5
+ }>;
6
+ export declare function validateSceneForUi(configInput: Partial<ScenesPlatformConfig> | undefined, sceneInput: Partial<SceneDefinition>): Promise<{
7
+ validation: ValidationResult;
8
+ normalizedScene: SceneDefinition;
9
+ snapshot: TopologySnapshot;
10
+ }>;
11
+ export declare function runTestForUi(configInput: Partial<ScenesPlatformConfig> | undefined, sceneInput: Partial<SceneDefinition>): Promise<SceneRunResult>;
@@ -0,0 +1,52 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createDefaultUiConfig = createDefaultUiConfig;
4
+ exports.discoverForUi = discoverForUi;
5
+ exports.validateSceneForUi = validateSceneForUi;
6
+ exports.runTestForUi = runTestForUi;
7
+ const config_1 = require("../config");
8
+ const discoveryService_1 = require("../discoveryService");
9
+ const logger_1 = require("../logger");
10
+ const sceneRunner_1 = require("../sceneRunner");
11
+ const transports_1 = require("../transports");
12
+ function createDefaultUiConfig() {
13
+ return (0, config_1.normalizePlatformConfig)(undefined);
14
+ }
15
+ async function buildServices(configInput) {
16
+ const config = (0, config_1.normalizePlatformConfig)(configInput);
17
+ const collector = new logger_1.MemoryLogCollector();
18
+ const logger = new logger_1.StructuredLogger("ui", config.logLevel, undefined, collector);
19
+ const transport = (0, transports_1.createTransport)(config);
20
+ const discoveryService = new discoveryService_1.DiscoveryService(transport);
21
+ const sceneRunner = new sceneRunner_1.SceneRunner(discoveryService, transport, logger);
22
+ return {
23
+ config,
24
+ transport,
25
+ discoveryService,
26
+ sceneRunner,
27
+ collector,
28
+ };
29
+ }
30
+ async function discoverForUi(configInput) {
31
+ const services = await buildServices(configInput);
32
+ return {
33
+ snapshot: await services.discoveryService.refresh(),
34
+ };
35
+ }
36
+ async function validateSceneForUi(configInput, sceneInput) {
37
+ const services = await buildServices(configInput);
38
+ const snapshot = await services.discoveryService.refresh();
39
+ const normalizedScene = (0, config_1.normalizeScene)(sceneInput);
40
+ const validation = (0, config_1.validateSceneDefinition)(normalizedScene, snapshot, services.transport);
41
+ return {
42
+ validation,
43
+ normalizedScene,
44
+ snapshot,
45
+ };
46
+ }
47
+ async function runTestForUi(configInput, sceneInput) {
48
+ const services = await buildServices(configInput);
49
+ const scene = (0, config_1.normalizeScene)(sceneInput);
50
+ return services.sceneRunner.runTest(scene);
51
+ }
52
+ //# sourceMappingURL=serverApi.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"serverApi.js","sourceRoot":"","sources":["../../../src/ui/serverApi.ts"],"names":[],"mappings":";;AAOA,sDAEC;AAmBD,sCAOC;AAED,gDAcC;AAED,oCAOC;AA5DD,sCAA6F;AAC7F,0DAAuD;AACvD,sCAAiE;AACjE,gDAA6C;AAC7C,8CAAgD;AAGhD,SAAgB,qBAAqB;IACnC,OAAO,IAAA,gCAAuB,EAAC,SAAS,CAAC,CAAC;AAC5C,CAAC;AAED,KAAK,UAAU,aAAa,CAAC,WAAsD;IACjF,MAAM,MAAM,GAAG,IAAA,gCAAuB,EAAC,WAAW,CAAC,CAAC;IACpD,MAAM,SAAS,GAAG,IAAI,2BAAkB,EAAE,CAAC;IAC3C,MAAM,MAAM,GAAG,IAAI,yBAAgB,CAAC,IAAI,EAAE,MAAM,CAAC,QAAQ,EAAE,SAAS,EAAE,SAAS,CAAC,CAAC;IACjF,MAAM,SAAS,GAAG,IAAA,4BAAe,EAAC,MAAM,CAAC,CAAC;IAC1C,MAAM,gBAAgB,GAAG,IAAI,mCAAgB,CAAC,SAAS,CAAC,CAAC;IACzD,MAAM,WAAW,GAAG,IAAI,yBAAW,CAAC,gBAAgB,EAAE,SAAS,EAAE,MAAM,CAAC,CAAC;IAEzE,OAAO;QACL,MAAM;QACN,SAAS;QACT,gBAAgB;QAChB,WAAW;QACX,SAAS;KACV,CAAC;AACJ,CAAC;AAEM,KAAK,UAAU,aAAa,CACjC,WAAsD;IAEtD,MAAM,QAAQ,GAAG,MAAM,aAAa,CAAC,WAAW,CAAC,CAAC;IAClD,OAAO;QACL,QAAQ,EAAE,MAAM,QAAQ,CAAC,gBAAgB,CAAC,OAAO,EAAE;KACpD,CAAC;AACJ,CAAC;AAEM,KAAK,UAAU,kBAAkB,CACtC,WAAsD,EACtD,UAAoC;IAEpC,MAAM,QAAQ,GAAG,MAAM,aAAa,CAAC,WAAW,CAAC,CAAC;IAClD,MAAM,QAAQ,GAAG,MAAM,QAAQ,CAAC,gBAAgB,CAAC,OAAO,EAAE,CAAC;IAC3D,MAAM,eAAe,GAAG,IAAA,uBAAc,EAAC,UAAU,CAAC,CAAC;IACnD,MAAM,UAAU,GAAG,IAAA,gCAAuB,EAAC,eAAe,EAAE,QAAQ,EAAE,QAAQ,CAAC,SAAS,CAAC,CAAC;IAE1F,OAAO;QACL,UAAU;QACV,eAAe;QACf,QAAQ;KACT,CAAC;AACJ,CAAC;AAEM,KAAK,UAAU,YAAY,CAChC,WAAsD,EACtD,UAAoC;IAEpC,MAAM,QAAQ,GAAG,MAAM,aAAa,CAAC,WAAW,CAAC,CAAC;IAClD,MAAM,KAAK,GAAG,IAAA,uBAAc,EAAC,UAAU,CAAC,CAAC;IACzC,OAAO,QAAQ,CAAC,WAAW,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;AAC7C,CAAC"}
@@ -0,0 +1,48 @@
1
+ {
2
+ "platforms": [
3
+ {
4
+ "platform": "SonosScenes",
5
+ "name": "Sonos Scenes",
6
+ "logLevel": "info",
7
+ "transport": {
8
+ "kind": "local",
9
+ "enableLiveDiscovery": true,
10
+ "fixturePath": "./examples/sample-topology.json",
11
+ "discoveryTimeoutMs": 2500,
12
+ "requestTimeoutMs": 5000,
13
+ "allowTvSource": false
14
+ },
15
+ "scenes": [
16
+ {
17
+ "id": "upper-level-line-in",
18
+ "name": "Upper Level Line In",
19
+ "householdId": "local-household",
20
+ "coordinatorPlayerId": "RINCON_UPPER_LEVEL",
21
+ "memberPlayerIds": [
22
+ "RINCON_PRIMARY_BEDROOM"
23
+ ],
24
+ "source": {
25
+ "kind": "line_in",
26
+ "deviceId": "RINCON_UPPER_LEVEL",
27
+ "deviceName": "Upper Level",
28
+ "playOnCompletion": true
29
+ },
30
+ "coordinatorVolume": 20,
31
+ "playerVolumes": [
32
+ {
33
+ "playerId": "RINCON_PRIMARY_BEDROOM",
34
+ "volume": 16
35
+ }
36
+ ],
37
+ "offBehavior": {
38
+ "kind": "ungroup"
39
+ },
40
+ "settleMs": 750,
41
+ "retryCount": 3,
42
+ "retryDelayMs": 750,
43
+ "autoResetMs": 1000
44
+ }
45
+ ]
46
+ }
47
+ ]
48
+ }
@@ -0,0 +1,77 @@
1
+ {
2
+ "capturedAt": "2026-04-18T00:00:00.000Z",
3
+ "households": [
4
+ {
5
+ "id": "local-household",
6
+ "displayName": "Local Sonos",
7
+ "players": [
8
+ {
9
+ "id": "RINCON_UPPER_LEVEL",
10
+ "name": "Upper Level",
11
+ "capabilities": [
12
+ "PLAYBACK",
13
+ "LINE_IN",
14
+ "AIRPLAY"
15
+ ],
16
+ "deviceIds": [
17
+ "RINCON_UPPER_LEVEL"
18
+ ],
19
+ "groupId": "GROUP_UPPER_LEVEL",
20
+ "isCoordinator": true,
21
+ "fixedVolume": false,
22
+ "sourceOptions": [
23
+ "line_in",
24
+ "favorite"
25
+ ]
26
+ },
27
+ {
28
+ "id": "RINCON_PRIMARY_BEDROOM",
29
+ "name": "Primary Bedroom",
30
+ "capabilities": [
31
+ "PLAYBACK",
32
+ "AIRPLAY"
33
+ ],
34
+ "deviceIds": [
35
+ "RINCON_PRIMARY_BEDROOM"
36
+ ],
37
+ "groupId": "GROUP_PRIMARY_BEDROOM",
38
+ "isCoordinator": true,
39
+ "fixedVolume": false,
40
+ "sourceOptions": [
41
+ "favorite"
42
+ ]
43
+ }
44
+ ],
45
+ "groups": [
46
+ {
47
+ "id": "GROUP_UPPER_LEVEL",
48
+ "name": "Upper Level",
49
+ "coordinatorId": "RINCON_UPPER_LEVEL",
50
+ "playerIds": [
51
+ "RINCON_UPPER_LEVEL"
52
+ ],
53
+ "playbackState": "PLAYBACK_STATE_IDLE"
54
+ },
55
+ {
56
+ "id": "GROUP_PRIMARY_BEDROOM",
57
+ "name": "Primary Bedroom",
58
+ "coordinatorId": "RINCON_PRIMARY_BEDROOM",
59
+ "playerIds": [
60
+ "RINCON_PRIMARY_BEDROOM"
61
+ ],
62
+ "playbackState": "PLAYBACK_STATE_IDLE"
63
+ }
64
+ ],
65
+ "favorites": [
66
+ {
67
+ "id": "favorite-line-in-demo",
68
+ "name": "Line-In Demo"
69
+ },
70
+ {
71
+ "id": "favorite-kexp",
72
+ "name": "KEXP"
73
+ }
74
+ ]
75
+ }
76
+ ]
77
+ }