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.
Files changed (92) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +736 -0
  3. package/bin/auth.d.ts +3 -0
  4. package/bin/auth.d.ts.map +1 -0
  5. package/bin/auth.js +130 -0
  6. package/bin/content.d.ts +3 -0
  7. package/bin/content.d.ts.map +1 -0
  8. package/bin/content.js +117 -0
  9. package/bin/devices.d.ts +3 -0
  10. package/bin/devices.d.ts.map +1 -0
  11. package/bin/devices.js +239 -0
  12. package/bin/groups.d.ts +3 -0
  13. package/bin/groups.d.ts.map +1 -0
  14. package/bin/groups.js +80 -0
  15. package/bin/icons.d.ts +3 -0
  16. package/bin/icons.d.ts.map +1 -0
  17. package/bin/icons.js +100 -0
  18. package/bin/lib/cli-helpers.d.ts +21 -0
  19. package/bin/lib/cli-helpers.d.ts.map +1 -0
  20. package/bin/lib/cli-helpers.js +140 -0
  21. package/bin/lib/token-helpers.d.ts +14 -0
  22. package/bin/lib/token-helpers.d.ts.map +1 -0
  23. package/bin/lib/token-helpers.js +151 -0
  24. package/bin/refresh-token.d.ts +3 -0
  25. package/bin/refresh-token.d.ts.map +1 -0
  26. package/bin/refresh-token.js +168 -0
  27. package/bin/token-info.d.ts +3 -0
  28. package/bin/token-info.d.ts.map +1 -0
  29. package/bin/token-info.js +351 -0
  30. package/index.d.ts +218 -0
  31. package/index.d.ts.map +1 -0
  32. package/index.js +689 -0
  33. package/lib/api-endpoints/auth.d.ts +56 -0
  34. package/lib/api-endpoints/auth.d.ts.map +1 -0
  35. package/lib/api-endpoints/auth.js +209 -0
  36. package/lib/api-endpoints/auth.test.js +27 -0
  37. package/lib/api-endpoints/constants.d.ts +6 -0
  38. package/lib/api-endpoints/constants.d.ts.map +1 -0
  39. package/lib/api-endpoints/constants.js +31 -0
  40. package/lib/api-endpoints/content.d.ts +275 -0
  41. package/lib/api-endpoints/content.d.ts.map +1 -0
  42. package/lib/api-endpoints/content.js +518 -0
  43. package/lib/api-endpoints/content.test.js +250 -0
  44. package/lib/api-endpoints/devices.d.ts +202 -0
  45. package/lib/api-endpoints/devices.d.ts.map +1 -0
  46. package/lib/api-endpoints/devices.js +404 -0
  47. package/lib/api-endpoints/devices.test.js +483 -0
  48. package/lib/api-endpoints/family-library-groups.d.ts +75 -0
  49. package/lib/api-endpoints/family-library-groups.d.ts.map +1 -0
  50. package/lib/api-endpoints/family-library-groups.js +247 -0
  51. package/lib/api-endpoints/family-library-groups.test.js +272 -0
  52. package/lib/api-endpoints/family.d.ts +39 -0
  53. package/lib/api-endpoints/family.d.ts.map +1 -0
  54. package/lib/api-endpoints/family.js +166 -0
  55. package/lib/api-endpoints/family.test.js +184 -0
  56. package/lib/api-endpoints/helpers.d.ts +29 -0
  57. package/lib/api-endpoints/helpers.d.ts.map +1 -0
  58. package/lib/api-endpoints/helpers.js +104 -0
  59. package/lib/api-endpoints/icons.d.ts +62 -0
  60. package/lib/api-endpoints/icons.d.ts.map +1 -0
  61. package/lib/api-endpoints/icons.js +201 -0
  62. package/lib/api-endpoints/icons.test.js +118 -0
  63. package/lib/api-endpoints/media.d.ts +37 -0
  64. package/lib/api-endpoints/media.d.ts.map +1 -0
  65. package/lib/api-endpoints/media.js +155 -0
  66. package/lib/api-endpoints/test-helpers.d.ts +7 -0
  67. package/lib/api-endpoints/test-helpers.d.ts.map +1 -0
  68. package/lib/api-endpoints/test-helpers.js +64 -0
  69. package/lib/mqtt/client.d.ts +124 -0
  70. package/lib/mqtt/client.d.ts.map +1 -0
  71. package/lib/mqtt/client.js +558 -0
  72. package/lib/mqtt/commands.d.ts +69 -0
  73. package/lib/mqtt/commands.d.ts.map +1 -0
  74. package/lib/mqtt/commands.js +238 -0
  75. package/lib/mqtt/factory.d.ts +12 -0
  76. package/lib/mqtt/factory.d.ts.map +1 -0
  77. package/lib/mqtt/factory.js +107 -0
  78. package/lib/mqtt/index.d.ts +5 -0
  79. package/lib/mqtt/index.d.ts.map +1 -0
  80. package/lib/mqtt/index.js +81 -0
  81. package/lib/mqtt/mqtt.test.js +168 -0
  82. package/lib/mqtt/topics.d.ts +34 -0
  83. package/lib/mqtt/topics.d.ts.map +1 -0
  84. package/lib/mqtt/topics.js +295 -0
  85. package/lib/pkg.cjs +3 -0
  86. package/lib/pkg.d.cts +70 -0
  87. package/lib/pkg.d.cts.map +1 -0
  88. package/lib/token.d.ts +29 -0
  89. package/lib/token.d.ts.map +1 -0
  90. package/lib/token.js +240 -0
  91. package/package.json +91 -0
  92. 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
+ })