yoto-nodejs-client 0.0.1
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/LICENSE +21 -0
- package/README.md +736 -0
- package/bin/auth.d.ts +3 -0
- package/bin/auth.d.ts.map +1 -0
- package/bin/auth.js +130 -0
- package/bin/content.d.ts +3 -0
- package/bin/content.d.ts.map +1 -0
- package/bin/content.js +117 -0
- package/bin/devices.d.ts +3 -0
- package/bin/devices.d.ts.map +1 -0
- package/bin/devices.js +239 -0
- package/bin/groups.d.ts +3 -0
- package/bin/groups.d.ts.map +1 -0
- package/bin/groups.js +80 -0
- package/bin/icons.d.ts +3 -0
- package/bin/icons.d.ts.map +1 -0
- package/bin/icons.js +100 -0
- package/bin/lib/cli-helpers.d.ts +21 -0
- package/bin/lib/cli-helpers.d.ts.map +1 -0
- package/bin/lib/cli-helpers.js +140 -0
- package/bin/lib/token-helpers.d.ts +14 -0
- package/bin/lib/token-helpers.d.ts.map +1 -0
- package/bin/lib/token-helpers.js +151 -0
- package/bin/refresh-token.d.ts +3 -0
- package/bin/refresh-token.d.ts.map +1 -0
- package/bin/refresh-token.js +168 -0
- package/bin/token-info.d.ts +3 -0
- package/bin/token-info.d.ts.map +1 -0
- package/bin/token-info.js +351 -0
- package/index.d.ts +218 -0
- package/index.d.ts.map +1 -0
- package/index.js +689 -0
- package/lib/api-endpoints/auth.d.ts +56 -0
- package/lib/api-endpoints/auth.d.ts.map +1 -0
- package/lib/api-endpoints/auth.js +209 -0
- package/lib/api-endpoints/auth.test.js +27 -0
- package/lib/api-endpoints/constants.d.ts +6 -0
- package/lib/api-endpoints/constants.d.ts.map +1 -0
- package/lib/api-endpoints/constants.js +31 -0
- package/lib/api-endpoints/content.d.ts +275 -0
- package/lib/api-endpoints/content.d.ts.map +1 -0
- package/lib/api-endpoints/content.js +518 -0
- package/lib/api-endpoints/content.test.js +250 -0
- package/lib/api-endpoints/devices.d.ts +202 -0
- package/lib/api-endpoints/devices.d.ts.map +1 -0
- package/lib/api-endpoints/devices.js +404 -0
- package/lib/api-endpoints/devices.test.js +483 -0
- package/lib/api-endpoints/family-library-groups.d.ts +75 -0
- package/lib/api-endpoints/family-library-groups.d.ts.map +1 -0
- package/lib/api-endpoints/family-library-groups.js +247 -0
- package/lib/api-endpoints/family-library-groups.test.js +272 -0
- package/lib/api-endpoints/family.d.ts +39 -0
- package/lib/api-endpoints/family.d.ts.map +1 -0
- package/lib/api-endpoints/family.js +166 -0
- package/lib/api-endpoints/family.test.js +184 -0
- package/lib/api-endpoints/helpers.d.ts +29 -0
- package/lib/api-endpoints/helpers.d.ts.map +1 -0
- package/lib/api-endpoints/helpers.js +104 -0
- package/lib/api-endpoints/icons.d.ts +62 -0
- package/lib/api-endpoints/icons.d.ts.map +1 -0
- package/lib/api-endpoints/icons.js +201 -0
- package/lib/api-endpoints/icons.test.js +118 -0
- package/lib/api-endpoints/media.d.ts +37 -0
- package/lib/api-endpoints/media.d.ts.map +1 -0
- package/lib/api-endpoints/media.js +155 -0
- package/lib/api-endpoints/test-helpers.d.ts +7 -0
- package/lib/api-endpoints/test-helpers.d.ts.map +1 -0
- package/lib/api-endpoints/test-helpers.js +64 -0
- package/lib/mqtt/client.d.ts +124 -0
- package/lib/mqtt/client.d.ts.map +1 -0
- package/lib/mqtt/client.js +558 -0
- package/lib/mqtt/commands.d.ts +69 -0
- package/lib/mqtt/commands.d.ts.map +1 -0
- package/lib/mqtt/commands.js +238 -0
- package/lib/mqtt/factory.d.ts +12 -0
- package/lib/mqtt/factory.d.ts.map +1 -0
- package/lib/mqtt/factory.js +107 -0
- package/lib/mqtt/index.d.ts +5 -0
- package/lib/mqtt/index.d.ts.map +1 -0
- package/lib/mqtt/index.js +81 -0
- package/lib/mqtt/mqtt.test.js +168 -0
- package/lib/mqtt/topics.d.ts +34 -0
- package/lib/mqtt/topics.d.ts.map +1 -0
- package/lib/mqtt/topics.js +295 -0
- package/lib/pkg.cjs +3 -0
- package/lib/pkg.d.cts +70 -0
- package/lib/pkg.d.cts.map +1 -0
- package/lib/token.d.ts +29 -0
- package/lib/token.d.ts.map +1 -0
- package/lib/token.js +240 -0
- package/package.json +91 -0
- package/yoto.png +0 -0
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
export function createVolumeCommand(volume: number): YotoVolumeCommand;
|
|
2
|
+
export function createAmbientCommand(r: number, g: number, b: number): YotoAmbientCommand;
|
|
3
|
+
export function createAmbientCommandFromHex(hexColor: string): YotoAmbientCommand;
|
|
4
|
+
export function createSleepTimerCommand(seconds: number): YotoSleepTimerCommand;
|
|
5
|
+
export function createCardStartCommand(options: {
|
|
6
|
+
uri: string;
|
|
7
|
+
chapterKey?: string | undefined;
|
|
8
|
+
trackKey?: string | undefined;
|
|
9
|
+
secondsIn?: number | undefined;
|
|
10
|
+
cutOff?: number | undefined;
|
|
11
|
+
anyButtonStop?: boolean | undefined;
|
|
12
|
+
}): YotoCardStartCommand;
|
|
13
|
+
export function createBluetoothOnCommand(options?: {
|
|
14
|
+
action?: string | undefined;
|
|
15
|
+
mode?: string | boolean | undefined;
|
|
16
|
+
rssi?: number | undefined;
|
|
17
|
+
name?: string | undefined;
|
|
18
|
+
mac?: string | undefined;
|
|
19
|
+
}): YotoBluetoothCommand;
|
|
20
|
+
export function createBluetoothSpeakerCommand(): YotoBluetoothCommand;
|
|
21
|
+
export function createBluetoothAudioSourceCommand(): YotoBluetoothCommand;
|
|
22
|
+
export function createDisplayPreviewCommand(options: {
|
|
23
|
+
uri: string;
|
|
24
|
+
timeout: number;
|
|
25
|
+
animated: boolean;
|
|
26
|
+
}): YotoDisplayPreviewCommand;
|
|
27
|
+
export namespace commands {
|
|
28
|
+
export { createVolumeCommand as volume };
|
|
29
|
+
export { createAmbientCommand as ambient };
|
|
30
|
+
export { createAmbientCommandFromHex as ambientFromHex };
|
|
31
|
+
export { createSleepTimerCommand as sleepTimer };
|
|
32
|
+
export { createCardStartCommand as cardStart };
|
|
33
|
+
export { createBluetoothOnCommand as bluetoothOn };
|
|
34
|
+
export { createBluetoothSpeakerCommand as bluetoothSpeaker };
|
|
35
|
+
export { createBluetoothAudioSourceCommand as bluetoothAudioSource };
|
|
36
|
+
export { createDisplayPreviewCommand as displayPreview };
|
|
37
|
+
}
|
|
38
|
+
export type YotoVolumeCommand = {
|
|
39
|
+
volume: number;
|
|
40
|
+
};
|
|
41
|
+
export type YotoAmbientCommand = {
|
|
42
|
+
r: number;
|
|
43
|
+
g: number;
|
|
44
|
+
b: number;
|
|
45
|
+
};
|
|
46
|
+
export type YotoSleepTimerCommand = {
|
|
47
|
+
seconds: number;
|
|
48
|
+
};
|
|
49
|
+
export type YotoCardStartCommand = {
|
|
50
|
+
uri: string;
|
|
51
|
+
chapterKey?: string;
|
|
52
|
+
trackKey?: string;
|
|
53
|
+
secondsIn?: number;
|
|
54
|
+
cutOff?: number;
|
|
55
|
+
anyButtonStop?: boolean;
|
|
56
|
+
};
|
|
57
|
+
export type YotoBluetoothCommand = {
|
|
58
|
+
action?: string;
|
|
59
|
+
mode?: boolean | string;
|
|
60
|
+
rssi?: number;
|
|
61
|
+
name?: string;
|
|
62
|
+
mac?: string;
|
|
63
|
+
};
|
|
64
|
+
export type YotoDisplayPreviewCommand = {
|
|
65
|
+
uri: string;
|
|
66
|
+
timeout: number;
|
|
67
|
+
animated: 0 | 1;
|
|
68
|
+
};
|
|
69
|
+
//# sourceMappingURL=commands.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"commands.d.ts","sourceRoot":"","sources":["commands.js"],"names":[],"mappings":"AAwEA,4CAJW,MAAM,GACJ,iBAAiB,CAS7B;AAUD,wCANW,MAAM,KACN,MAAM,KACN,MAAM,GACJ,kBAAkB,CAS9B;AAQD,sDAJW,MAAM,GACJ,kBAAkB,CAgB9B;AAQD,iDAJW,MAAM,GACJ,qBAAqB,CASjC;AAcD,gDATG;IAAwB,GAAG,EAAnB,MAAM;IACW,UAAU;IACV,QAAQ;IACR,SAAS;IACT,MAAM;IACL,aAAa;CACvC,GAAU,oBAAoB,CAkBhC;AAYD,mDAPG;IAAyB,MAAM;IACI,IAAI;IACd,IAAI;IACJ,IAAI;IACJ,GAAG;CAC5B,GAAU,oBAAoB,CAahC;AAMD,iDAFa,oBAAoB,CAIhC;AAMD,qDAFa,oBAAoB,CAIhC;AAWD,qDANG;IAAwB,GAAG,EAAnB,MAAM;IACU,OAAO,EAAvB,MAAM;IACW,QAAQ,EAAzB,OAAO;CACf,GAAU,yBAAyB,CAiBrC;;;;;;;;;;;;;YA/Ma,MAAM;;;OAON,MAAM;OACN,MAAM;OACN,MAAM;;;aAON,MAAM;;;SAON,MAAM;iBACN,MAAM;eACN,MAAM;gBACN,MAAM;aACN,MAAM;oBACN,OAAO;;;aAOP,MAAM;WACN,OAAO,GAAG,MAAM;WAChB,MAAM;WACN,MAAM;UACN,MAAM;;;SAON,MAAM;aACN,MAAM;cACN,CAAC,GAAG,CAAC"}
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MQTT Command Builders for Yoto Players
|
|
3
|
+
*
|
|
4
|
+
* Type-safe builders for constructing MQTT command payloads
|
|
5
|
+
* @see https://yoto.dev/players-mqtt/
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// ============================================================================
|
|
9
|
+
// MQTT Commands: Type-safe command builders
|
|
10
|
+
// ============================================================================
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Volume command payload
|
|
14
|
+
* @see https://yoto.dev/players-mqtt/mqtt-docs/#deviceidcommandvolumeset
|
|
15
|
+
* @typedef {Object} YotoVolumeCommand
|
|
16
|
+
* @property {number} volume - Volume level [0-100]
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Ambient light command payload
|
|
21
|
+
* @see https://yoto.dev/players-mqtt/mqtt-docs/#deviceidcommandambientsset
|
|
22
|
+
* @typedef {Object} YotoAmbientCommand
|
|
23
|
+
* @property {number} r - Red intensity [0-255]
|
|
24
|
+
* @property {number} g - Green intensity [0-255]
|
|
25
|
+
* @property {number} b - Blue intensity [0-255]
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Sleep timer command payload
|
|
30
|
+
* @see https://yoto.dev/players-mqtt/mqtt-docs/#deviceidcommandsleep-timerset
|
|
31
|
+
* @typedef {Object} YotoSleepTimerCommand
|
|
32
|
+
* @property {number} seconds - Timer duration in seconds (0 to disable)
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Card start command payload
|
|
37
|
+
* @see https://yoto.dev/players-mqtt/mqtt-docs/#deviceidcommandcardstart
|
|
38
|
+
* @typedef {Object} YotoCardStartCommand
|
|
39
|
+
* @property {string} uri - Card URI (e.g., "https://yoto.io/<cardID>")
|
|
40
|
+
* @property {string} [chapterKey] - Chapter to start from
|
|
41
|
+
* @property {string} [trackKey] - Track to start from
|
|
42
|
+
* @property {number} [secondsIn] - Playback start offset in seconds
|
|
43
|
+
* @property {number} [cutOff] - Playback stop offset in seconds
|
|
44
|
+
* @property {boolean} [anyButtonStop] - Whether button press stops playback
|
|
45
|
+
*/
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Bluetooth command payload
|
|
49
|
+
* @see https://yoto.dev/players-mqtt/mqtt-docs/#deviceidcommandbluetooth
|
|
50
|
+
* @typedef {Object} YotoBluetoothCommand
|
|
51
|
+
* @property {string} [action] - Bluetooth action (e.g., "on")
|
|
52
|
+
* @property {boolean | string} [mode] - Bluetooth mode (true for audio source, "bt_speaker" for sink)
|
|
53
|
+
* @property {number} [rssi] - RSSI threshold for auto-connect
|
|
54
|
+
* @property {string} [name] - Target Bluetooth device name
|
|
55
|
+
* @property {string} [mac] - Target Bluetooth MAC address
|
|
56
|
+
*/
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Display preview command payload
|
|
60
|
+
* @see https://yoto.dev/players-mqtt/mqtt-docs/#deviceidcommanddisplaypreview
|
|
61
|
+
* @typedef {Object} YotoDisplayPreviewCommand
|
|
62
|
+
* @property {string} uri - Filepath to icon asset
|
|
63
|
+
* @property {number} timeout - Display duration in seconds
|
|
64
|
+
* @property {0 | 1} animated - Whether icon is animated (1) or static (0)
|
|
65
|
+
*/
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Create a volume set command
|
|
69
|
+
* @param {number} volume - Volume level [0-100]
|
|
70
|
+
* @returns {YotoVolumeCommand}
|
|
71
|
+
* @throws {Error} If volume is out of range
|
|
72
|
+
*/
|
|
73
|
+
export function createVolumeCommand (volume) {
|
|
74
|
+
if (volume < 0 || volume > 100) {
|
|
75
|
+
throw new Error('Volume must be between 0 and 100')
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return { volume }
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Create an ambient light set command
|
|
83
|
+
* @param {number} r - Red intensity [0-255]
|
|
84
|
+
* @param {number} g - Green intensity [0-255]
|
|
85
|
+
* @param {number} b - Blue intensity [0-255]
|
|
86
|
+
* @returns {YotoAmbientCommand}
|
|
87
|
+
* @throws {Error} If any color value is out of range
|
|
88
|
+
*/
|
|
89
|
+
export function createAmbientCommand (r, g, b) {
|
|
90
|
+
if (r < 0 || r > 255 || g < 0 || g > 255 || b < 0 || b > 255) {
|
|
91
|
+
throw new Error('RGB values must be between 0 and 255')
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return { r, g, b }
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Create an ambient light command from hex color
|
|
99
|
+
* @param {string} hexColor - Hex color string (e.g., "#FF0000" or "FF0000")
|
|
100
|
+
* @returns {YotoAmbientCommand}
|
|
101
|
+
* @throws {Error} If hex color is invalid
|
|
102
|
+
*/
|
|
103
|
+
export function createAmbientCommandFromHex (hexColor) {
|
|
104
|
+
// Remove # if present
|
|
105
|
+
const hex = hexColor.replace(/^#/, '')
|
|
106
|
+
|
|
107
|
+
if (!/^[0-9A-Fa-f]{6}$/.test(hex)) {
|
|
108
|
+
throw new Error('Invalid hex color format. Expected format: #RRGGBB or RRGGBB')
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const r = parseInt(hex.slice(0, 2), 16)
|
|
112
|
+
const g = parseInt(hex.slice(2, 4), 16)
|
|
113
|
+
const b = parseInt(hex.slice(4, 6), 16)
|
|
114
|
+
|
|
115
|
+
return { r, g, b }
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Create a sleep timer set command
|
|
120
|
+
* @param {number} seconds - Timer duration in seconds (0 to disable)
|
|
121
|
+
* @returns {YotoSleepTimerCommand}
|
|
122
|
+
* @throws {Error} If seconds is negative
|
|
123
|
+
*/
|
|
124
|
+
export function createSleepTimerCommand (seconds) {
|
|
125
|
+
if (seconds < 0) {
|
|
126
|
+
throw new Error('Seconds must be non-negative (use 0 to disable timer)')
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return { seconds }
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Create a card start command
|
|
134
|
+
* @param {Object} options - Card start options
|
|
135
|
+
* @param {string} options.uri - Card URI (e.g., "https://yoto.io/<cardID>")
|
|
136
|
+
* @param {string} [options.chapterKey] - Chapter to start from
|
|
137
|
+
* @param {string} [options.trackKey] - Track to start from
|
|
138
|
+
* @param {number} [options.secondsIn] - Playback start offset in seconds
|
|
139
|
+
* @param {number} [options.cutOff] - Playback stop offset in seconds
|
|
140
|
+
* @param {boolean} [options.anyButtonStop] - Whether button press stops playback
|
|
141
|
+
* @returns {YotoCardStartCommand}
|
|
142
|
+
* @throws {Error} If uri is missing
|
|
143
|
+
*/
|
|
144
|
+
export function createCardStartCommand (options) {
|
|
145
|
+
if (!options.uri) {
|
|
146
|
+
throw new Error('Card URI is required')
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** @type {YotoCardStartCommand} */
|
|
150
|
+
const command = { uri: options.uri }
|
|
151
|
+
|
|
152
|
+
if (options.chapterKey !== undefined) command.chapterKey = options.chapterKey
|
|
153
|
+
if (options.trackKey !== undefined) command.trackKey = options.trackKey
|
|
154
|
+
if (options.secondsIn !== undefined) command.secondsIn = options.secondsIn
|
|
155
|
+
if (options.cutOff !== undefined) command.cutOff = options.cutOff
|
|
156
|
+
if (options.anyButtonStop !== undefined) command.anyButtonStop = options.anyButtonStop
|
|
157
|
+
|
|
158
|
+
return command
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Create a bluetooth on command
|
|
163
|
+
* @param {Object} [options] - Bluetooth options
|
|
164
|
+
* @param {string} [options.action] - Bluetooth action (e.g., "on")
|
|
165
|
+
* @param {boolean | string} [options.mode] - Bluetooth mode (true for audio source, "bt_speaker" for sink)
|
|
166
|
+
* @param {number} [options.rssi] - RSSI threshold for auto-connect
|
|
167
|
+
* @param {string} [options.name] - Target Bluetooth device name
|
|
168
|
+
* @param {string} [options.mac] - Target Bluetooth MAC address
|
|
169
|
+
* @returns {YotoBluetoothCommand}
|
|
170
|
+
*/
|
|
171
|
+
export function createBluetoothOnCommand (options = {}) {
|
|
172
|
+
/** @type {YotoBluetoothCommand} */
|
|
173
|
+
const command = {}
|
|
174
|
+
|
|
175
|
+
if (options.action !== undefined) command.action = options.action
|
|
176
|
+
if (options.mode !== undefined) command.mode = options.mode
|
|
177
|
+
if (options.rssi !== undefined) command.rssi = options.rssi
|
|
178
|
+
if (options.name !== undefined) command.name = options.name
|
|
179
|
+
if (options.mac !== undefined) command.mac = options.mac
|
|
180
|
+
|
|
181
|
+
return command
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Create a bluetooth speaker mode command
|
|
186
|
+
* @returns {YotoBluetoothCommand}
|
|
187
|
+
*/
|
|
188
|
+
export function createBluetoothSpeakerCommand () {
|
|
189
|
+
return { mode: 'bt_speaker' }
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Create a bluetooth audio source mode command
|
|
194
|
+
* @returns {YotoBluetoothCommand}
|
|
195
|
+
*/
|
|
196
|
+
export function createBluetoothAudioSourceCommand () {
|
|
197
|
+
return { action: 'on', mode: true }
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Create a display preview command
|
|
202
|
+
* @param {Object} options - Display preview options
|
|
203
|
+
* @param {string} options.uri - Filepath to icon asset
|
|
204
|
+
* @param {number} options.timeout - Display duration in seconds
|
|
205
|
+
* @param {boolean} options.animated - Whether icon is animated
|
|
206
|
+
* @returns {YotoDisplayPreviewCommand}
|
|
207
|
+
* @throws {Error} If uri or timeout is missing
|
|
208
|
+
*/
|
|
209
|
+
export function createDisplayPreviewCommand (options) {
|
|
210
|
+
if (!options.uri) {
|
|
211
|
+
throw new Error('Icon URI is required')
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (options.timeout === undefined) {
|
|
215
|
+
throw new Error('Timeout is required')
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return {
|
|
219
|
+
uri: options.uri,
|
|
220
|
+
timeout: options.timeout,
|
|
221
|
+
animated: options.animated ? 1 : 0
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Command builders object for convenient access
|
|
227
|
+
*/
|
|
228
|
+
export const commands = {
|
|
229
|
+
volume: createVolumeCommand,
|
|
230
|
+
ambient: createAmbientCommand,
|
|
231
|
+
ambientFromHex: createAmbientCommandFromHex,
|
|
232
|
+
sleepTimer: createSleepTimerCommand,
|
|
233
|
+
cardStart: createCardStartCommand,
|
|
234
|
+
bluetoothOn: createBluetoothOnCommand,
|
|
235
|
+
bluetoothSpeaker: createBluetoothSpeakerCommand,
|
|
236
|
+
bluetoothAudioSource: createBluetoothAudioSourceCommand,
|
|
237
|
+
displayPreview: createDisplayPreviewCommand
|
|
238
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export function createYotoMqttClient(options: YotoMqttOptions): YotoMqttClient;
|
|
2
|
+
export type YotoMqttOptions = {
|
|
3
|
+
deviceId: string;
|
|
4
|
+
accessToken: string;
|
|
5
|
+
clientIdPrefix?: string;
|
|
6
|
+
brokerUrl?: string;
|
|
7
|
+
keepalive?: number;
|
|
8
|
+
port?: number;
|
|
9
|
+
autoSubscribe?: boolean;
|
|
10
|
+
};
|
|
11
|
+
import { YotoMqttClient } from './client.js';
|
|
12
|
+
//# sourceMappingURL=factory.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"factory.d.ts","sourceRoot":"","sources":["factory.js"],"names":[],"mappings":"AA2DA,8CApBW,eAAe,GACb,cAAc,CAkE1B;;cAzFa,MAAM;iBACN,MAAM;qBACN,MAAM;gBACN,MAAM;gBACN,MAAM;WACN,MAAM;oBACN,OAAO;;+BAIU,aAAa"}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MQTT Client Factory for Yoto Players
|
|
3
|
+
*
|
|
4
|
+
* Factory function to create properly configured MQTT clients for Yoto devices
|
|
5
|
+
* @see https://yoto.dev/players-mqtt/
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @import { IClientOptions } from 'mqtt'
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
// ============================================================================
|
|
13
|
+
// MQTT Factory: Create properly configured MQTT clients for Yoto devices
|
|
14
|
+
// ============================================================================
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @typedef {Object} YotoMqttOptions
|
|
18
|
+
* @property {string} deviceId - Device ID to connect to
|
|
19
|
+
* @property {string} accessToken - JWT access token for authentication
|
|
20
|
+
* @property {string} [clientIdPrefix='DASH'] - Prefix for MQTT client ID (default: 'DASH')
|
|
21
|
+
* @property {string} [brokerUrl='wss://aqrphjqbp3u2z-ats.iot.eu-west-2.amazonaws.com'] - MQTT broker URL
|
|
22
|
+
* @property {number} [keepalive=300] - Keepalive interval in seconds
|
|
23
|
+
* @property {number} [port=443] - MQTT broker port
|
|
24
|
+
* @property {boolean} [autoSubscribe=true] - Auto-subscribe to device topics on connect
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import mqtt from 'mqtt'
|
|
28
|
+
import { YotoMqttClient } from './client.js'
|
|
29
|
+
import {
|
|
30
|
+
MQTT_BROKER_URL,
|
|
31
|
+
MQTT_AUTH_NAME,
|
|
32
|
+
MQTT_PORT,
|
|
33
|
+
MQTT_PROTOCOL,
|
|
34
|
+
MQTT_KEEPALIVE,
|
|
35
|
+
MQTT_ALPN_PROTOCOLS
|
|
36
|
+
} from './topics.js'
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Create a configured MQTT client for a Yoto device
|
|
40
|
+
* @param {YotoMqttOptions} options - MQTT connection options
|
|
41
|
+
* @returns {YotoMqttClient} Configured Yoto MQTT client
|
|
42
|
+
* @throws {Error} If required options are missing
|
|
43
|
+
*
|
|
44
|
+
* @example
|
|
45
|
+
* ```javascript
|
|
46
|
+
* import { createYotoMqttClient } from 'yoto-nodejs-client/lib/mqtt'
|
|
47
|
+
*
|
|
48
|
+
* const client = createYotoMqttClient({
|
|
49
|
+
* deviceId: 'abc123',
|
|
50
|
+
* accessToken: 'eyJhbGc...'
|
|
51
|
+
* })
|
|
52
|
+
*
|
|
53
|
+
* client.on('events', (message) => {
|
|
54
|
+
* console.log('Playing:', message.trackTitle)
|
|
55
|
+
* })
|
|
56
|
+
*
|
|
57
|
+
* await client.connect()
|
|
58
|
+
* ```
|
|
59
|
+
*/
|
|
60
|
+
export function createYotoMqttClient (options) {
|
|
61
|
+
// Validate required options
|
|
62
|
+
if (!options.deviceId) {
|
|
63
|
+
throw new Error('deviceId is required')
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (!options.accessToken) {
|
|
67
|
+
throw new Error('accessToken is required')
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Extract options with defaults
|
|
71
|
+
const {
|
|
72
|
+
deviceId,
|
|
73
|
+
accessToken,
|
|
74
|
+
clientIdPrefix = 'DASH',
|
|
75
|
+
brokerUrl = MQTT_BROKER_URL,
|
|
76
|
+
keepalive = MQTT_KEEPALIVE,
|
|
77
|
+
port = MQTT_PORT,
|
|
78
|
+
autoSubscribe = true
|
|
79
|
+
} = options
|
|
80
|
+
|
|
81
|
+
// Build MQTT client ID
|
|
82
|
+
const clientId = `${clientIdPrefix}${deviceId}`
|
|
83
|
+
|
|
84
|
+
// Build username with authorizer
|
|
85
|
+
const username = `${deviceId}?x-amz-customauthorizer-name=${MQTT_AUTH_NAME}`
|
|
86
|
+
|
|
87
|
+
// Create MQTT connection options
|
|
88
|
+
/** @type {IClientOptions} */
|
|
89
|
+
const mqttOptions = {
|
|
90
|
+
clientId,
|
|
91
|
+
username,
|
|
92
|
+
password: accessToken,
|
|
93
|
+
keepalive,
|
|
94
|
+
port,
|
|
95
|
+
protocol: MQTT_PROTOCOL,
|
|
96
|
+
reconnectPeriod: 0, // Disable auto-reconnect, handle manually
|
|
97
|
+
ALPNProtocols: MQTT_ALPN_PROTOCOLS
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Create underlying MQTT client (not connected yet)
|
|
101
|
+
const mqttClient = mqtt.connect(brokerUrl, mqttOptions)
|
|
102
|
+
|
|
103
|
+
// Create and return Yoto MQTT client wrapper
|
|
104
|
+
return new YotoMqttClient(mqttClient, deviceId, {
|
|
105
|
+
autoSubscribe
|
|
106
|
+
})
|
|
107
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { createYotoMqttClient } from "./factory.js";
|
|
2
|
+
export { YotoMqttClient } from "./client.js";
|
|
3
|
+
export { createVolumeCommand, createAmbientCommand, createAmbientCommandFromHex, createSleepTimerCommand, createCardStartCommand, createBluetoothOnCommand, createBluetoothSpeakerCommand, createBluetoothAudioSourceCommand, createDisplayPreviewCommand, commands } from "./commands.js";
|
|
4
|
+
export { MQTT_BROKER_URL, MQTT_AUTH_NAME, MQTT_PORT, MQTT_PROTOCOL, MQTT_KEEPALIVE, MQTT_ALPN_PROTOCOLS, getEventsTopic, getStatusTopic, getResponseTopic, getSubscriptionTopics, getCommandTopic, parseTopic, getEventsRequestTopic, getStatusRequestTopic, getVolumeSetTopic, getAmbientsSetTopic, getSleepTimerSetTopic, getRebootTopic, getCardStartTopic, getCardStopTopic, getCardPauseTopic, getCardResumeTopic, getBluetoothOnTopic, getBluetoothOffTopic, getBluetoothDeleteBondsTopic, getBluetoothConnectTopic, getBluetoothDisconnectTopic, getBluetoothStateTopic, getDisplayPreviewTopic } from "./topics.js";
|
|
5
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["index.js"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Yoto MQTT Module
|
|
3
|
+
*
|
|
4
|
+
* MQTT client for connecting to and controlling Yoto players
|
|
5
|
+
* @see https://yoto.dev/players-mqtt/
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```javascript
|
|
9
|
+
* import { createYotoMqttClient } from 'yoto-nodejs-client/lib/mqtt'
|
|
10
|
+
*
|
|
11
|
+
* const client = createYotoMqttClient({
|
|
12
|
+
* deviceId: 'abc123',
|
|
13
|
+
* accessToken: 'eyJhbGc...'
|
|
14
|
+
* })
|
|
15
|
+
*
|
|
16
|
+
* client.on('events', (message) => {
|
|
17
|
+
* console.log('Now playing:', message.trackTitle)
|
|
18
|
+
* })
|
|
19
|
+
*
|
|
20
|
+
* client.on('status', (message) => {
|
|
21
|
+
* console.log('Battery:', message.batteryLevel, '%')
|
|
22
|
+
* })
|
|
23
|
+
*
|
|
24
|
+
* await client.connect()
|
|
25
|
+
* await client.setVolume(50)
|
|
26
|
+
* await client.setAmbientHex('#FF0000')
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
// Factory
|
|
31
|
+
export { createYotoMqttClient } from './factory.js'
|
|
32
|
+
|
|
33
|
+
// Client class
|
|
34
|
+
export { YotoMqttClient } from './client.js'
|
|
35
|
+
|
|
36
|
+
// Command builders
|
|
37
|
+
export {
|
|
38
|
+
createVolumeCommand,
|
|
39
|
+
createAmbientCommand,
|
|
40
|
+
createAmbientCommandFromHex,
|
|
41
|
+
createSleepTimerCommand,
|
|
42
|
+
createCardStartCommand,
|
|
43
|
+
createBluetoothOnCommand,
|
|
44
|
+
createBluetoothSpeakerCommand,
|
|
45
|
+
createBluetoothAudioSourceCommand,
|
|
46
|
+
createDisplayPreviewCommand,
|
|
47
|
+
commands
|
|
48
|
+
} from './commands.js'
|
|
49
|
+
|
|
50
|
+
// Topic builders and constants
|
|
51
|
+
export {
|
|
52
|
+
MQTT_BROKER_URL,
|
|
53
|
+
MQTT_AUTH_NAME,
|
|
54
|
+
MQTT_PORT,
|
|
55
|
+
MQTT_PROTOCOL,
|
|
56
|
+
MQTT_KEEPALIVE,
|
|
57
|
+
MQTT_ALPN_PROTOCOLS,
|
|
58
|
+
getEventsTopic,
|
|
59
|
+
getStatusTopic,
|
|
60
|
+
getResponseTopic,
|
|
61
|
+
getSubscriptionTopics,
|
|
62
|
+
getCommandTopic,
|
|
63
|
+
parseTopic,
|
|
64
|
+
getEventsRequestTopic,
|
|
65
|
+
getStatusRequestTopic,
|
|
66
|
+
getVolumeSetTopic,
|
|
67
|
+
getAmbientsSetTopic,
|
|
68
|
+
getSleepTimerSetTopic,
|
|
69
|
+
getRebootTopic,
|
|
70
|
+
getCardStartTopic,
|
|
71
|
+
getCardStopTopic,
|
|
72
|
+
getCardPauseTopic,
|
|
73
|
+
getCardResumeTopic,
|
|
74
|
+
getBluetoothOnTopic,
|
|
75
|
+
getBluetoothOffTopic,
|
|
76
|
+
getBluetoothDeleteBondsTopic,
|
|
77
|
+
getBluetoothConnectTopic,
|
|
78
|
+
getBluetoothDisconnectTopic,
|
|
79
|
+
getBluetoothStateTopic,
|
|
80
|
+
getDisplayPreviewTopic
|
|
81
|
+
} from './topics.js'
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import test from 'node:test'
|
|
2
|
+
import assert from 'node:assert'
|
|
3
|
+
import { getDevices } from '../api-endpoints/devices.js'
|
|
4
|
+
import { loadTestTokens, logResponse } from '../api-endpoints/test-helpers.js'
|
|
5
|
+
import { createYotoMqttClient } from './index.js'
|
|
6
|
+
|
|
7
|
+
const { accessToken } = loadTestTokens()
|
|
8
|
+
|
|
9
|
+
test('MQTT client', async (t) => {
|
|
10
|
+
await t.test('should connect, request status/events, and receive response and events messages', async () => {
|
|
11
|
+
// Get an online device
|
|
12
|
+
const response = await getDevices({ accessToken })
|
|
13
|
+
assert.ok(response.devices.length > 0, 'Should have at least one device')
|
|
14
|
+
|
|
15
|
+
const onlineDevice = response.devices.find(d => d.online)
|
|
16
|
+
assert.ok(onlineDevice, 'Should have at least one online device for MQTT testing')
|
|
17
|
+
|
|
18
|
+
const deviceId = onlineDevice.deviceId
|
|
19
|
+
|
|
20
|
+
// Create MQTT client
|
|
21
|
+
const mqttClient = createYotoMqttClient({
|
|
22
|
+
deviceId,
|
|
23
|
+
accessToken
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
// Track received messages
|
|
27
|
+
let statusResponseReceived = false
|
|
28
|
+
let eventsResponseReceived = false
|
|
29
|
+
let eventsMessageReceived = false
|
|
30
|
+
let statusMessageReceived = false
|
|
31
|
+
/** @type {Error[]} */
|
|
32
|
+
const errors = []
|
|
33
|
+
|
|
34
|
+
// Setup message handlers
|
|
35
|
+
mqttClient.on('events', (message) => {
|
|
36
|
+
logResponse('MQTT events message', message)
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
// Validate events message structure
|
|
40
|
+
assert.ok(message, 'Events message should exist')
|
|
41
|
+
|
|
42
|
+
// Events messages are partial - not all fields are always present
|
|
43
|
+
if (message.playbackStatus !== undefined) {
|
|
44
|
+
assert.ok(typeof message.playbackStatus === 'string', 'playbackStatus should be string')
|
|
45
|
+
}
|
|
46
|
+
if (message.cardId !== undefined) {
|
|
47
|
+
assert.ok(typeof message.cardId === 'string', 'cardId should be string')
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
eventsMessageReceived = true
|
|
51
|
+
} catch (err) {
|
|
52
|
+
errors.push(/** @type {Error} */ (err))
|
|
53
|
+
}
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
mqttClient.on('status', (message) => {
|
|
57
|
+
logResponse('MQTT status message', message)
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
// Validate status message structure
|
|
61
|
+
assert.ok(message, 'Status message should exist')
|
|
62
|
+
assert.ok(message.status, 'Status message should have status object')
|
|
63
|
+
assert.ok(typeof message.status.statusVersion === 'number', 'Should have statusVersion number')
|
|
64
|
+
assert.ok(typeof message.status.fwVersion === 'string', 'Should have fwVersion string')
|
|
65
|
+
assert.ok(typeof message.status.productType === 'string', 'Should have productType string')
|
|
66
|
+
assert.ok(typeof message.status.batteryLevel === 'number', 'Should have batteryLevel number')
|
|
67
|
+
assert.ok(typeof message.status.volume === 'number', 'Should have volume number')
|
|
68
|
+
|
|
69
|
+
statusMessageReceived = true
|
|
70
|
+
} catch (err) {
|
|
71
|
+
errors.push(/** @type {Error} */ (err))
|
|
72
|
+
}
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
mqttClient.on('response', (message) => {
|
|
76
|
+
logResponse('MQTT response message', message)
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
// Validate response message structure
|
|
80
|
+
assert.ok(message, 'Response message should exist')
|
|
81
|
+
assert.ok(message.status, 'Response should have status object')
|
|
82
|
+
assert.ok(typeof message.status.req_body === 'string', 'Response should have req_body string')
|
|
83
|
+
|
|
84
|
+
// Check if status request was acknowledged (field name is 'status/request')
|
|
85
|
+
if (message.status['status/request']) {
|
|
86
|
+
assert.ok(
|
|
87
|
+
message.status['status/request'] === 'OK' || message.status['status/request'] === 'FAIL',
|
|
88
|
+
'Status request field should be OK or FAIL'
|
|
89
|
+
)
|
|
90
|
+
statusResponseReceived = true
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Check if events request was acknowledged
|
|
94
|
+
if (message.status.events) {
|
|
95
|
+
assert.ok(
|
|
96
|
+
message.status.events === 'OK' || message.status.events === 'FAIL',
|
|
97
|
+
'Events request field should be OK or FAIL'
|
|
98
|
+
)
|
|
99
|
+
eventsResponseReceived = true
|
|
100
|
+
}
|
|
101
|
+
} catch (err) {
|
|
102
|
+
errors.push(/** @type {Error} */ (err))
|
|
103
|
+
}
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
mqttClient.on('error', (error) => {
|
|
107
|
+
console.error('MQTT error:', error)
|
|
108
|
+
errors.push(error)
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
/** @type {NodeJS.Timeout | undefined} */
|
|
112
|
+
let timeoutId
|
|
113
|
+
/** @type {NodeJS.Timeout | undefined} */
|
|
114
|
+
let checkIntervalId
|
|
115
|
+
|
|
116
|
+
try {
|
|
117
|
+
// Connect to MQTT
|
|
118
|
+
await mqttClient.connect()
|
|
119
|
+
assert.ok(mqttClient.connected, 'MQTT client should be connected')
|
|
120
|
+
|
|
121
|
+
// Request status and events
|
|
122
|
+
await mqttClient.requestStatus()
|
|
123
|
+
await mqttClient.requestEvents()
|
|
124
|
+
|
|
125
|
+
// Wait for messages (with timeout)
|
|
126
|
+
await new Promise((resolve, reject) => {
|
|
127
|
+
timeoutId = setTimeout(() => {
|
|
128
|
+
reject(new Error(`Timeout waiting for messages. statusResponse=${statusResponseReceived}, eventsResponse=${eventsResponseReceived}, eventsMessage=${eventsMessageReceived}, statusMessage=${statusMessageReceived}`))
|
|
129
|
+
}, 5000) // 5 second timeout
|
|
130
|
+
|
|
131
|
+
checkIntervalId = setInterval(() => {
|
|
132
|
+
if (errors.length > 0) {
|
|
133
|
+
clearTimeout(timeoutId)
|
|
134
|
+
clearInterval(checkIntervalId)
|
|
135
|
+
reject(errors[0])
|
|
136
|
+
}
|
|
137
|
+
if (statusResponseReceived && eventsResponseReceived && eventsMessageReceived && statusMessageReceived) {
|
|
138
|
+
clearTimeout(timeoutId)
|
|
139
|
+
clearInterval(checkIntervalId)
|
|
140
|
+
resolve(undefined)
|
|
141
|
+
}
|
|
142
|
+
}, 100)
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
// Verify we received all expected messages
|
|
146
|
+
assert.ok(statusResponseReceived, 'Should have received status request response')
|
|
147
|
+
assert.ok(eventsResponseReceived, 'Should have received events request response')
|
|
148
|
+
assert.ok(eventsMessageReceived, 'Should have received events data message')
|
|
149
|
+
assert.ok(statusMessageReceived, 'Should have received status data message')
|
|
150
|
+
} catch (err) {
|
|
151
|
+
// Clean up timers on error
|
|
152
|
+
if (timeoutId) clearTimeout(timeoutId)
|
|
153
|
+
if (checkIntervalId) clearInterval(checkIntervalId)
|
|
154
|
+
throw err
|
|
155
|
+
} finally {
|
|
156
|
+
// Always disconnect, even if test fails
|
|
157
|
+
if (timeoutId) clearTimeout(timeoutId)
|
|
158
|
+
if (checkIntervalId) clearInterval(checkIntervalId)
|
|
159
|
+
await mqttClient.disconnect()
|
|
160
|
+
assert.ok(!mqttClient.connected, 'MQTT client should be disconnected')
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Re-throw any errors that occurred in event handlers
|
|
164
|
+
if (errors.length > 0) {
|
|
165
|
+
throw errors[0]
|
|
166
|
+
}
|
|
167
|
+
})
|
|
168
|
+
})
|