yoto-nodejs-client 0.0.1 → 0.0.3
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 +523 -30
- package/bin/auth.js +36 -46
- package/bin/content.js +0 -0
- package/bin/device-model.d.ts +3 -0
- package/bin/device-model.d.ts.map +1 -0
- package/bin/device-model.js +360 -0
- package/bin/device-tui.TODO.md +125 -0
- package/bin/device-tui.d.ts +31 -0
- package/bin/device-tui.d.ts.map +1 -0
- package/bin/device-tui.js +1123 -0
- package/bin/devices.js +166 -28
- package/bin/groups.js +0 -0
- package/bin/icons.js +0 -0
- package/bin/lib/cli-helpers.d.ts +33 -1
- package/bin/lib/cli-helpers.d.ts.map +1 -1
- package/bin/lib/cli-helpers.js +5 -5
- package/bin/lib/token-helpers.d.ts +32 -0
- package/bin/lib/token-helpers.d.ts.map +1 -1
- package/bin/refresh-token.js +6 -6
- package/bin/token-info.js +3 -3
- package/index.d.ts +4 -217
- package/index.d.ts.map +1 -1
- package/index.js +11 -689
- package/lib/api-client.d.ts +576 -0
- package/lib/api-client.d.ts.map +1 -0
- package/lib/api-client.js +681 -0
- package/lib/api-endpoints/auth.d.ts +280 -4
- package/lib/api-endpoints/auth.d.ts.map +1 -1
- package/lib/api-endpoints/auth.js +224 -7
- package/lib/api-endpoints/auth.test.js +54 -2
- package/lib/api-endpoints/constants.d.ts +30 -2
- package/lib/api-endpoints/constants.d.ts.map +1 -1
- package/lib/api-endpoints/constants.js +17 -10
- package/lib/api-endpoints/content.d.ts +760 -0
- package/lib/api-endpoints/content.d.ts.map +1 -1
- package/lib/api-endpoints/content.test.js +1 -1
- package/lib/api-endpoints/devices.d.ts +917 -48
- package/lib/api-endpoints/devices.d.ts.map +1 -1
- package/lib/api-endpoints/devices.js +114 -52
- package/lib/api-endpoints/devices.test.js +1 -1
- package/lib/api-endpoints/endpoint-test-helpers.d.ts +28 -0
- package/lib/api-endpoints/endpoint-test-helpers.d.ts.map +1 -0
- package/lib/api-endpoints/family-library-groups.d.ts +187 -0
- package/lib/api-endpoints/family-library-groups.d.ts.map +1 -1
- package/lib/api-endpoints/family-library-groups.test.js +1 -1
- package/lib/api-endpoints/family.d.ts +88 -0
- package/lib/api-endpoints/family.d.ts.map +1 -1
- package/lib/api-endpoints/family.test.js +1 -1
- package/lib/api-endpoints/helpers.d.ts +37 -3
- package/lib/api-endpoints/helpers.d.ts.map +1 -1
- package/lib/api-endpoints/icons.d.ts +196 -0
- package/lib/api-endpoints/icons.d.ts.map +1 -1
- package/lib/api-endpoints/icons.test.js +1 -1
- package/lib/api-endpoints/media.d.ts +83 -0
- package/lib/api-endpoints/media.d.ts.map +1 -1
- package/lib/helpers/power-state.d.ts +53 -0
- package/lib/helpers/power-state.d.ts.map +1 -0
- package/lib/helpers/power-state.js +73 -0
- package/lib/helpers/power-state.test.js +100 -0
- package/lib/helpers/temperature.d.ts +24 -0
- package/lib/helpers/temperature.d.ts.map +1 -0
- package/lib/helpers/temperature.js +61 -0
- package/lib/helpers/temperature.test.js +58 -0
- package/lib/helpers/typed-keys.d.ts +7 -0
- package/lib/helpers/typed-keys.d.ts.map +1 -0
- package/lib/helpers/typed-keys.js +8 -0
- package/lib/mqtt/client.d.ts +610 -7
- package/lib/mqtt/client.d.ts.map +1 -1
- package/lib/mqtt/client.js +213 -31
- package/lib/mqtt/commands.d.ts +195 -0
- package/lib/mqtt/commands.d.ts.map +1 -1
- package/lib/mqtt/factory.d.ts +62 -1
- package/lib/mqtt/factory.d.ts.map +1 -1
- package/lib/mqtt/factory.js +27 -5
- package/lib/mqtt/mqtt.test.js +85 -28
- package/lib/mqtt/topics.d.ts +186 -1
- package/lib/mqtt/topics.d.ts.map +1 -1
- package/lib/mqtt/topics.js +54 -20
- package/lib/pkg.d.cts +9 -0
- package/lib/token.d.ts +106 -3
- package/lib/token.d.ts.map +1 -1
- package/lib/token.js +30 -23
- package/lib/yoto-account.d.ts +163 -0
- package/lib/yoto-account.d.ts.map +1 -0
- package/lib/yoto-account.js +340 -0
- package/lib/yoto-device.d.ts +656 -0
- package/lib/yoto-device.d.ts.map +1 -0
- package/lib/yoto-device.js +2850 -0
- package/package.json +22 -15
- package/lib/api-endpoints/test-helpers.d.ts +0 -7
- package/lib/api-endpoints/test-helpers.d.ts.map +0 -1
- /package/lib/api-endpoints/{test-helpers.js → endpoint-test-helpers.js} +0 -0
package/README.md
CHANGED
|
@@ -7,7 +7,15 @@
|
|
|
7
7
|
[](https://github.com/neostandard/neostandard)
|
|
8
8
|
[](https://socket.dev/npm/package/yoto-nodejs-client)
|
|
9
9
|
|
|
10
|
-
A comprehensive Node.js client for the [Yoto API][yoto-api] with automatic token refresh, MQTT device communication, and full TypeScript support.
|
|
10
|
+
A comprehensive Node.js client for the [Yoto API][yoto-api] with automatic token refresh, MQTT device communication, stateful device management, and full TypeScript support.
|
|
11
|
+
|
|
12
|
+
**Features:**
|
|
13
|
+
- **YotoClient** - Low-level HTTP API client with automatic token refresh
|
|
14
|
+
- **YotoDeviceModel** - Stateful device client combining HTTP + MQTT for unified, real time device state
|
|
15
|
+
- **YotoAccount** - Multi-device account manager with automatic discovery and lifecycle management
|
|
16
|
+
- Full TypeScript types
|
|
17
|
+
- Real-time MQTT device control and monitoring
|
|
18
|
+
- Debugging CLI tools for authentication and data inspection
|
|
11
19
|
|
|
12
20
|
<p align="center">
|
|
13
21
|
<img src="yoto.png" alt="Yoto" width="200">
|
|
@@ -19,6 +27,8 @@ npm install yoto-nodejs-client
|
|
|
19
27
|
|
|
20
28
|
## Usage
|
|
21
29
|
|
|
30
|
+
### Basic API Client
|
|
31
|
+
|
|
22
32
|
```js
|
|
23
33
|
import { YotoClient } from 'yoto-nodejs-client'
|
|
24
34
|
|
|
@@ -30,11 +40,16 @@ const deviceCodeResponse = await YotoClient.requestDeviceCode({
|
|
|
30
40
|
console.log(`Visit ${deviceCodeResponse.verification_uri_complete}`)
|
|
31
41
|
console.log(`Enter code: ${deviceCodeResponse.user_code}`)
|
|
32
42
|
|
|
33
|
-
//
|
|
34
|
-
const tokenResponse = await YotoClient.
|
|
35
|
-
grantType: 'urn:ietf:params:oauth:grant-type:device_code',
|
|
43
|
+
// Wait for authorization (simplest approach - handles polling automatically)
|
|
44
|
+
const tokenResponse = await YotoClient.waitForDeviceAuthorization({
|
|
36
45
|
deviceCode: deviceCodeResponse.device_code,
|
|
37
|
-
clientId: 'your-client-id'
|
|
46
|
+
clientId: 'your-client-id',
|
|
47
|
+
initialInterval: deviceCodeResponse.interval * 1000,
|
|
48
|
+
expiresIn: deviceCodeResponse.expires_in,
|
|
49
|
+
onPoll: (result) => {
|
|
50
|
+
if (result.status === 'pending') process.stdout.write('.')
|
|
51
|
+
if (result.status === 'slow_down') console.log('\nSlowing down...')
|
|
52
|
+
}
|
|
38
53
|
})
|
|
39
54
|
|
|
40
55
|
// Create client with automatic token refresh
|
|
@@ -45,9 +60,9 @@ const client = new YotoClient({
|
|
|
45
60
|
onTokenRefresh: async (event) => {
|
|
46
61
|
// REQUIRED: Persist tokens when they refresh
|
|
47
62
|
await saveTokens({
|
|
48
|
-
accessToken: event.
|
|
49
|
-
refreshToken: event.
|
|
50
|
-
expiresAt: event.
|
|
63
|
+
accessToken: event.updatedAccessToken,
|
|
64
|
+
refreshToken: event.updatedRefreshToken,
|
|
65
|
+
expiresAt: event.updatedExpiresAt
|
|
51
66
|
})
|
|
52
67
|
}
|
|
53
68
|
})
|
|
@@ -84,6 +99,153 @@ await mqtt.setVolume(50)
|
|
|
84
99
|
await mqtt.setAmbientHex('#FF0000')
|
|
85
100
|
```
|
|
86
101
|
|
|
102
|
+
### YotoDeviceModel - Stateful Device Client
|
|
103
|
+
|
|
104
|
+
```js
|
|
105
|
+
import { YotoClient, YotoDeviceModel } from 'yoto-nodejs-client'
|
|
106
|
+
|
|
107
|
+
// Create API client
|
|
108
|
+
const client = new YotoClient({
|
|
109
|
+
clientId: 'your-client-id',
|
|
110
|
+
refreshToken: 'your-refresh-token',
|
|
111
|
+
accessToken: 'your-access-token',
|
|
112
|
+
onTokenRefresh: async (event) => {
|
|
113
|
+
await saveTokens(event)
|
|
114
|
+
}
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
// Get a device
|
|
118
|
+
const { devices } = await client.getDevices()
|
|
119
|
+
const device = devices[0]
|
|
120
|
+
|
|
121
|
+
// Create stateful device client (manages HTTP + MQTT state)
|
|
122
|
+
const deviceClient = new YotoDeviceModel(client, device, {
|
|
123
|
+
httpPollIntervalMs: 600000 // Background polling every 10 minutes
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
// Listen for status updates (from MQTT or HTTP)
|
|
127
|
+
deviceClient.on('statusUpdate', (status, source, changedFields) => {
|
|
128
|
+
console.log(`Battery: ${status.batteryLevelPercentage}% (via ${source})`)
|
|
129
|
+
console.log(`Temperature: ${status.temperatureCelsius}°C`)
|
|
130
|
+
console.log(`Online: ${status.isOnline}`)
|
|
131
|
+
console.log('Changed fields:', changedFields)
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
// Listen for config changes
|
|
135
|
+
deviceClient.on('configUpdate', (config, changedFields) => {
|
|
136
|
+
console.log('Config updated:', config.maxVolumeLimit)
|
|
137
|
+
console.log('Changed fields:', changedFields)
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
// Listen for playback events
|
|
141
|
+
deviceClient.on('playbackUpdate', (playback, changedFields) => {
|
|
142
|
+
console.log(`Playing: ${playback.trackTitle}`)
|
|
143
|
+
console.log(`Position: ${playback.position}/${playback.trackLength}s`)
|
|
144
|
+
console.log('Changed fields:', changedFields)
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
// Listen for online/offline events
|
|
148
|
+
deviceClient.on('online', (metadata) => {
|
|
149
|
+
if (metadata.reason === 'startup') {
|
|
150
|
+
console.log(`Device powered on (uptime: ${metadata.upTime}s)`)
|
|
151
|
+
} else {
|
|
152
|
+
console.log('Device came online')
|
|
153
|
+
}
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
deviceClient.on('offline', (metadata) => {
|
|
157
|
+
if (metadata.reason === 'shutdown') {
|
|
158
|
+
console.log(`Device shut down: ${metadata.shutDownReason}`)
|
|
159
|
+
}
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
// Start the device client (connects MQTT, starts background polling)
|
|
163
|
+
await deviceClient.start()
|
|
164
|
+
|
|
165
|
+
// Access current state
|
|
166
|
+
console.log('Current status:', deviceClient.status)
|
|
167
|
+
console.log('Current config:', deviceClient.config)
|
|
168
|
+
console.log('Current playback:', deviceClient.playback)
|
|
169
|
+
console.log('Device capabilities:', deviceClient.capabilities)
|
|
170
|
+
|
|
171
|
+
// Control the device
|
|
172
|
+
await deviceClient.updateConfig({ maxVolumeLimit: '14' })
|
|
173
|
+
await deviceClient.sendCommand({ volume: 50 })
|
|
174
|
+
|
|
175
|
+
// Stop when done
|
|
176
|
+
await deviceClient.stop()
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
### Account Manager (Multiple Devices)
|
|
180
|
+
|
|
181
|
+
```js
|
|
182
|
+
import { YotoAccount } from 'yoto-nodejs-client'
|
|
183
|
+
|
|
184
|
+
// Create account manager
|
|
185
|
+
const account = new YotoAccount({
|
|
186
|
+
clientOptions: {
|
|
187
|
+
clientId: 'your-client-id',
|
|
188
|
+
refreshToken: 'your-refresh-token',
|
|
189
|
+
accessToken: 'your-access-token',
|
|
190
|
+
onTokenRefresh: async (event) => {
|
|
191
|
+
await saveTokens(event)
|
|
192
|
+
}
|
|
193
|
+
},
|
|
194
|
+
deviceOptions: {
|
|
195
|
+
httpPollIntervalMs: 600000 // Applied to all devices
|
|
196
|
+
}
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
// Listen for account events
|
|
200
|
+
account.on('started', (metadata) => {
|
|
201
|
+
console.log(`Managing ${metadata.deviceCount} devices`)
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
// Listen for device-specific events from individual devices
|
|
207
|
+
account.on('deviceAdded', (deviceId, deviceModel) => {
|
|
208
|
+
// Attach event listeners to each device as it's added
|
|
209
|
+
deviceModel.on('statusUpdate', (status, source) => {
|
|
210
|
+
console.log(`${deviceId} battery: ${status.batteryLevelPercentage}%`)
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
deviceModel.on('online', (metadata) => {
|
|
214
|
+
console.log(`${deviceId} came online`)
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
deviceModel.on('offline', (metadata) => {
|
|
218
|
+
console.log(`${deviceId} went offline`)
|
|
219
|
+
})
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
// Unified error handling
|
|
223
|
+
account.on('error', (error, context) => {
|
|
224
|
+
console.error(`Error in ${context.source}:`, error.message)
|
|
225
|
+
if (context.deviceId) {
|
|
226
|
+
console.error(`Device: ${context.deviceId}`)
|
|
227
|
+
}
|
|
228
|
+
})
|
|
229
|
+
|
|
230
|
+
// Start managing all devices
|
|
231
|
+
await account.start()
|
|
232
|
+
|
|
233
|
+
// Access individual devices
|
|
234
|
+
const device = account.getDevice('abc123')
|
|
235
|
+
console.log('Device status:', device.status)
|
|
236
|
+
console.log('Device config:', device.config)
|
|
237
|
+
|
|
238
|
+
// Get all devices
|
|
239
|
+
const allDevices = account.devices // Map<deviceId, YotoDeviceModel>
|
|
240
|
+
console.log('Total devices:', allDevices.size)
|
|
241
|
+
|
|
242
|
+
// Refresh device list (add new, remove missing)
|
|
243
|
+
await account.refreshDevices()
|
|
244
|
+
|
|
245
|
+
// Stop all devices
|
|
246
|
+
await account.stop()
|
|
247
|
+
```
|
|
248
|
+
|
|
87
249
|
## API
|
|
88
250
|
|
|
89
251
|
### Authentication
|
|
@@ -122,9 +284,11 @@ Exchange authorization code, refresh token, or device code for access tokens.
|
|
|
122
284
|
See [Yoto API: Token Exchange][api-token]
|
|
123
285
|
|
|
124
286
|
```js
|
|
287
|
+
import { YotoClient, DEVICE_CODE_GRANT_TYPE } from 'yoto-nodejs-client'
|
|
288
|
+
|
|
125
289
|
// Exchange device code
|
|
126
290
|
const tokens = await YotoClient.exchangeToken({
|
|
127
|
-
grantType:
|
|
291
|
+
grantType: DEVICE_CODE_GRANT_TYPE,
|
|
128
292
|
deviceCode: response.device_code,
|
|
129
293
|
clientId: 'your-client-id'
|
|
130
294
|
})
|
|
@@ -137,6 +301,103 @@ const refreshed = await YotoClient.exchangeToken({
|
|
|
137
301
|
})
|
|
138
302
|
```
|
|
139
303
|
|
|
304
|
+
#### `YotoClient.waitForDeviceAuthorization({ deviceCode, clientId, [initialInterval], [expiresIn], [onPoll] })`
|
|
305
|
+
|
|
306
|
+
Wait for device authorization to complete with automatic polling. This is the **simplest approach** - just call it and await the result. It handles all polling logic internally including interval adjustments and timeout detection.
|
|
307
|
+
|
|
308
|
+
Designed for CLI usage where you want to block until authorization completes. For UI implementations with custom progress feedback, use `pollForDeviceToken()` directly.
|
|
309
|
+
|
|
310
|
+
- **deviceCode** - Device code from `requestDeviceCode()`
|
|
311
|
+
- **clientId** - OAuth client ID
|
|
312
|
+
- **initialInterval** - Initial polling interval in milliseconds (default: 5000)
|
|
313
|
+
- **expiresIn** - Seconds until device code expires (for timeout detection)
|
|
314
|
+
- **audience** - Audience for the token (default: 'https://api.yotoplay.com')
|
|
315
|
+
- **onPoll** - Optional callback invoked after each poll attempt with the poll result
|
|
316
|
+
|
|
317
|
+
Returns a promise that resolves to `YotoTokenResponse` on successful authorization.
|
|
318
|
+
|
|
319
|
+
Throws `YotoAPIError` for unrecoverable errors (expired_token, access_denied, invalid_grant) or `Error` if device code expires.
|
|
320
|
+
|
|
321
|
+
See [Yoto API: Token Exchange][api-token]
|
|
322
|
+
|
|
323
|
+
```js
|
|
324
|
+
import { YotoClient } from 'yoto-nodejs-client'
|
|
325
|
+
|
|
326
|
+
// Simplest approach - just wait for tokens
|
|
327
|
+
const deviceAuth = await YotoClient.requestDeviceCode({
|
|
328
|
+
clientId: 'your-client-id'
|
|
329
|
+
})
|
|
330
|
+
|
|
331
|
+
console.log(`Visit: ${deviceAuth.verification_uri_complete}`)
|
|
332
|
+
console.log(`Code: ${deviceAuth.user_code}`)
|
|
333
|
+
|
|
334
|
+
// This blocks until authorization completes (or fails)
|
|
335
|
+
const tokens = await YotoClient.waitForDeviceAuthorization({
|
|
336
|
+
deviceCode: deviceAuth.device_code,
|
|
337
|
+
clientId: 'your-client-id',
|
|
338
|
+
initialInterval: deviceAuth.interval * 1000,
|
|
339
|
+
expiresIn: deviceAuth.expires_in,
|
|
340
|
+
onPoll: (result) => {
|
|
341
|
+
if (result.status === 'pending') process.stdout.write('.')
|
|
342
|
+
if (result.status === 'slow_down') console.log('\nSlowing down...')
|
|
343
|
+
}
|
|
344
|
+
})
|
|
345
|
+
|
|
346
|
+
console.log('Got tokens:', tokens)
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
#### `YotoClient.pollForDeviceToken({ deviceCode, clientId, [currentInterval], [audience] })`
|
|
350
|
+
|
|
351
|
+
Poll for device authorization completion with automatic error handling (single poll attempt). This is a lower-level method that gives you control over the polling loop. Use `waitForDeviceAuthorization()` for a simpler approach.
|
|
352
|
+
|
|
353
|
+
Non-blocking - returns immediately with poll result. Suitable for:
|
|
354
|
+
- Manual polling loops in CLI applications
|
|
355
|
+
- Server-side endpoints that poll on behalf of clients (e.g., Homebridge UI server)
|
|
356
|
+
- Custom UI implementations with specific polling behavior
|
|
357
|
+
|
|
358
|
+
- **deviceCode** - Device code from `requestDeviceCode()`
|
|
359
|
+
- **clientId** - OAuth client ID
|
|
360
|
+
- **currentInterval** - Current polling interval in milliseconds (default: 5000)
|
|
361
|
+
- **audience** - Audience for the token (default: 'https://api.yotoplay.com')
|
|
362
|
+
|
|
363
|
+
Returns a promise that resolves to one of:
|
|
364
|
+
- `{ status: 'success', tokens: YotoTokenResponse }` - Authorization successful
|
|
365
|
+
- `{ status: 'pending', interval: number }` - Still waiting for user authorization
|
|
366
|
+
- `{ status: 'slow_down', interval: number }` - Polling too fast, use new interval
|
|
367
|
+
|
|
368
|
+
Throws `YotoAPIError` for unrecoverable errors (expired_token, access_denied, invalid_grant, etc).
|
|
369
|
+
|
|
370
|
+
See [Yoto API: Token Exchange][api-token]
|
|
371
|
+
|
|
372
|
+
```js
|
|
373
|
+
import { YotoClient } from 'yoto-nodejs-client'
|
|
374
|
+
|
|
375
|
+
// Manual polling loop with full control
|
|
376
|
+
const deviceAuth = await YotoClient.requestDeviceCode({
|
|
377
|
+
clientId: 'your-client-id'
|
|
378
|
+
})
|
|
379
|
+
|
|
380
|
+
let interval = deviceAuth.interval * 1000
|
|
381
|
+
|
|
382
|
+
while (true) {
|
|
383
|
+
const result = await YotoClient.pollForDeviceToken({
|
|
384
|
+
deviceCode: deviceAuth.device_code,
|
|
385
|
+
clientId: 'your-client-id',
|
|
386
|
+
currentInterval: interval
|
|
387
|
+
})
|
|
388
|
+
|
|
389
|
+
if (result.status === 'success') {
|
|
390
|
+
console.log('Tokens:', result.tokens)
|
|
391
|
+
break
|
|
392
|
+
} else if (result.status === 'slow_down') {
|
|
393
|
+
interval = result.interval
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// Sleep
|
|
397
|
+
await new Promise(resolve => setTimeout(resolve, interval))
|
|
398
|
+
}
|
|
399
|
+
```
|
|
400
|
+
|
|
140
401
|
#### `YotoClient.getAuthorizeUrl({ clientId, redirectUri, responseType, state, ...params })`
|
|
141
402
|
|
|
142
403
|
Get authorization URL for browser-based OAuth flow. Returns a URL string to redirect users to.
|
|
@@ -156,9 +417,9 @@ Create a new Yoto API client with automatic token refresh.
|
|
|
156
417
|
- **bufferSeconds** - Seconds before expiration to refresh (default: 30)
|
|
157
418
|
- **userAgent** - Optional user agent string to identify your application
|
|
158
419
|
- **defaultRequestOptions** - Optional default undici request options (dispatcher, timeouts, etc.) applied to all requests
|
|
159
|
-
- **onRefreshStart** - Optional callback when refresh starts
|
|
160
|
-
- **onRefreshError** - Optional callback for transient refresh errors
|
|
161
|
-
- **onInvalid** - Optional callback when refresh token is permanently invalid
|
|
420
|
+
- **onRefreshStart** - Optional callback when refresh starts. Console.logs by default.
|
|
421
|
+
- **onRefreshError** - Optional callback for transient refresh errors. Console.error's by default.
|
|
422
|
+
- **onInvalid** - Optional callback when refresh token is permanently invalid. Console.errors by default.
|
|
162
423
|
|
|
163
424
|
```js
|
|
164
425
|
const client = new YotoClient({
|
|
@@ -171,16 +432,16 @@ const client = new YotoClient({
|
|
|
171
432
|
headersTimeout: 10000, // 10 second header timeout
|
|
172
433
|
// dispatcher, signal, etc.
|
|
173
434
|
},
|
|
174
|
-
onTokenRefresh: async ({
|
|
435
|
+
onTokenRefresh: async ({ updatedAccessToken, updatedRefreshToken, updatedExpiresAt }) => {
|
|
175
436
|
// Save to database, file, etc.
|
|
176
|
-
await db.saveTokens({ accessToken, refreshToken, expiresAt })
|
|
437
|
+
await db.saveTokens({ accessToken: updatedAccessToken, refreshToken: updatedRefreshToken, expiresAt: updatedExpiresAt })
|
|
177
438
|
}
|
|
178
439
|
})
|
|
179
440
|
```
|
|
180
441
|
|
|
181
442
|
#### Request Options
|
|
182
443
|
|
|
183
|
-
All API methods accept an optional `requestOptions` parameter that allows you to override the default undici request options for individual requests. This is useful for setting custom timeouts, using a specific dispatcher, or aborting requests.
|
|
444
|
+
All API methods accept an optional [undici][undici] `requestOptions` parameter that allows you to override the default undici request options for individual requests. This is useful for setting custom timeouts, using a specific dispatcher, or aborting requests.
|
|
184
445
|
|
|
185
446
|
```js
|
|
186
447
|
// Override timeout for a specific request
|
|
@@ -650,49 +911,280 @@ The MQTT client emits three types of messages:
|
|
|
650
911
|
- `await mqtt.stopCard()` - Stop playback
|
|
651
912
|
- `await mqtt.reboot()` - Reboot device
|
|
652
913
|
|
|
914
|
+
### YotoDeviceModel - Stateful Device Client
|
|
915
|
+
|
|
916
|
+
#### `new YotoDeviceModel(client, device, [options])`
|
|
917
|
+
|
|
918
|
+
Create a stateful device client that manages device state primarily from MQTT with HTTP background sync.
|
|
919
|
+
|
|
920
|
+
**Philosophy:**
|
|
921
|
+
- MQTT is the primary source for all real-time status updates
|
|
922
|
+
- MQTT connection is always maintained and handles its own reconnection
|
|
923
|
+
- Device online/offline state is tracked by MQTT activity and explicit shutdown messages
|
|
924
|
+
- HTTP background polling runs every 10 minutes to sync config+status regardless of online state
|
|
925
|
+
- HTTP status updates emit offline events if device state changes to offline
|
|
926
|
+
|
|
927
|
+
**Parameters:**
|
|
928
|
+
- `client` - YotoClient instance
|
|
929
|
+
- `device` - Device object from `getDevices()`
|
|
930
|
+
- `options.httpPollIntervalMs` - Background HTTP polling interval (default: 600000ms / 10 minutes)
|
|
931
|
+
- `options.mqttOptions` - MQTT.js client options to pass through
|
|
932
|
+
|
|
933
|
+
**Lifecycle:**
|
|
934
|
+
- `await deviceClient.start()` - Start device client (connects MQTT, starts polling)
|
|
935
|
+
- `await deviceClient.stop()` - Stop device client (disconnects MQTT, stops polling)
|
|
936
|
+
- `await deviceClient.restart()` - Restart device client
|
|
937
|
+
|
|
938
|
+
**State Accessors:**
|
|
939
|
+
- `deviceClient.device` - Device information
|
|
940
|
+
- `deviceClient.status` - Current device status (normalized from HTTP/MQTT)
|
|
941
|
+
- `deviceClient.config` - Device configuration
|
|
942
|
+
- `deviceClient.shortcuts` - Button shortcuts
|
|
943
|
+
- `deviceClient.playback` - Current playback state
|
|
944
|
+
- `deviceClient.capabilities` - Hardware capabilities (sensors, nightlight support, etc.)
|
|
945
|
+
- `deviceClient.nightlight` - Nightlight info: { value, name, supported }
|
|
946
|
+
- `deviceClient.initialized` - Whether device has been initialized
|
|
947
|
+
- `deviceClient.running` - Whether device client is currently running
|
|
948
|
+
- `deviceClient.mqttConnected` - MQTT connection status
|
|
949
|
+
- `deviceClient.deviceOnline` - Device online status
|
|
950
|
+
- `deviceClient.mqttClient` - Underlying MQTT client instance (or null)
|
|
951
|
+
|
|
952
|
+
**Device Control:**
|
|
953
|
+
- `await deviceClient.refreshConfig()` - Refresh config from HTTP API
|
|
954
|
+
- `await deviceClient.updateConfig(configUpdate)` - Update device configuration
|
|
955
|
+
- `await deviceClient.sendCommand(command)` - Send device command via HTTP
|
|
956
|
+
|
|
957
|
+
**Events:**
|
|
958
|
+
- `started(metadata)` - Device client started, passes metadata object with device, config, shortcuts, status, playback, initialized, running
|
|
959
|
+
- `stopped()` - Device client stopped
|
|
960
|
+
- `statusUpdate(status, source, changedFields)` - Status changed, passes (status, source, changedFields). Source is 'http', 'mqtt', or 'mqtt-event'
|
|
961
|
+
- `configUpdate(config, changedFields)` - Configuration changed, passes (config, changedFields)
|
|
962
|
+
- `playbackUpdate(playback, changedFields)` - Playback state changed, passes (playback, changedFields)
|
|
963
|
+
- `online(metadata)` - Device came online, passes metadata with reason and optional upTime
|
|
964
|
+
- `offline(metadata)` - Device went offline, passes metadata with reason and optional shutDownReason or timeSinceLastSeen
|
|
965
|
+
- `mqttConnected()` - MQTT client connected
|
|
966
|
+
- `mqttDisconnected()` - MQTT client disconnected
|
|
967
|
+
- `error(error)` - Error occurred, passes error
|
|
968
|
+
|
|
969
|
+
**Static Properties & Methods:**
|
|
970
|
+
- `YotoDeviceModel.NIGHTLIGHT_COLORS` - Map of nightlight color hex codes to official color names
|
|
971
|
+
- `YotoDeviceModel.getNightlightColorName(colorValue)` - Get official color name for a nightlight value
|
|
972
|
+
|
|
973
|
+
```js
|
|
974
|
+
import { YotoClient, YotoDeviceModel } from 'yoto-nodejs-client'
|
|
975
|
+
|
|
976
|
+
const client = new YotoClient({ /* ... */ })
|
|
977
|
+
const { devices } = await client.getDevices()
|
|
978
|
+
|
|
979
|
+
const deviceClient = new YotoDeviceModel(client, devices[0], {
|
|
980
|
+
httpPollIntervalMs: 300000 // Poll every 5 minutes
|
|
981
|
+
})
|
|
982
|
+
|
|
983
|
+
deviceClient.on('statusUpdate', (status, source, changedFields) => {
|
|
984
|
+
console.log(`Battery: ${status.batteryLevelPercentage}% (${source})`)
|
|
985
|
+
console.log('Changed fields:', changedFields)
|
|
986
|
+
})
|
|
987
|
+
|
|
988
|
+
deviceClient.on('online', (metadata) => {
|
|
989
|
+
console.log('Device online:', metadata.reason)
|
|
990
|
+
})
|
|
991
|
+
|
|
992
|
+
await deviceClient.start()
|
|
993
|
+
|
|
994
|
+
// Access state
|
|
995
|
+
console.log('Temperature:', deviceClient.status.temperatureCelsius)
|
|
996
|
+
console.log('Has temp sensor:', deviceClient.capabilities.hasTemperatureSensor)
|
|
997
|
+
console.log('Nightlight:', deviceClient.nightlight) // { value, name, supported }
|
|
998
|
+
|
|
999
|
+
// Use static nightlight utilities
|
|
1000
|
+
console.log('Available colors:', YotoDeviceModel.NIGHTLIGHT_COLORS)
|
|
1001
|
+
console.log('Color name:', YotoDeviceModel.getNightlightColorName('0x643600'))
|
|
1002
|
+
|
|
1003
|
+
// Control device
|
|
1004
|
+
await deviceClient.updateConfig({ maxVolumeLimit: '14' })
|
|
1005
|
+
|
|
1006
|
+
await deviceClient.stop()
|
|
1007
|
+
```
|
|
1008
|
+
|
|
1009
|
+
### Account Manager
|
|
1010
|
+
|
|
1011
|
+
#### `new YotoAccount({ clientOptions, deviceOptions })`
|
|
1012
|
+
|
|
1013
|
+
Create an account manager that automatically discovers and manages all devices for a Yoto account.
|
|
1014
|
+
|
|
1015
|
+
**Parameters:**
|
|
1016
|
+
- `clientOptions` - YotoClient constructor options (clientId, refreshToken, accessToken, onTokenRefresh, etc.)
|
|
1017
|
+
- `deviceOptions` - YotoDeviceModel options applied to all devices (httpPollIntervalMs, mqttOptions)
|
|
1018
|
+
|
|
1019
|
+
**Lifecycle:**
|
|
1020
|
+
- `await account.start()` - Start account (creates client, discovers devices, starts all device clients)
|
|
1021
|
+
- `await account.stop()` - Stop account (stops all device clients gracefully)
|
|
1022
|
+
- `await account.restart()` - Restart account
|
|
1023
|
+
- `await account.refreshDevices()` - Refresh device list (add new, remove missing)
|
|
1024
|
+
|
|
1025
|
+
**State Accessors:**
|
|
1026
|
+
- `account.client` - Underlying YotoClient instance
|
|
1027
|
+
- `account.devices` - Map of all device models (Map<deviceId, YotoDeviceModel>)
|
|
1028
|
+
- `account.getDevice(deviceId)` - Get specific device model
|
|
1029
|
+
- `account.getDeviceIds()` - Get array of all device IDs
|
|
1030
|
+
- `account.running` - Whether account is currently running
|
|
1031
|
+
- `account.initialized` - Whether account has been initialized
|
|
1032
|
+
|
|
1033
|
+
**Events:**
|
|
1034
|
+
- `started(metadata)` - Account started (metadata: { deviceCount, devices })
|
|
1035
|
+
- `stopped()` - Account stopped
|
|
1036
|
+
- `deviceAdded(deviceId, deviceModel)` - Device was added
|
|
1037
|
+
- `deviceRemoved(deviceId)` - Device was removed
|
|
1038
|
+
- `error(error, context)` - Error occurred (context: { source, deviceId, operation })
|
|
1039
|
+
|
|
1040
|
+
**Note:** To listen to individual device events (statusUpdate, configUpdate, playbackUpdate, online, offline, mqttConnected, mqttDisconnected, etc.), access the device models directly via `account.devices` or `account.getDevice(deviceId)` and attach listeners to them.
|
|
1041
|
+
|
|
1042
|
+
```js
|
|
1043
|
+
import { YotoAccount } from 'yoto-nodejs-client'
|
|
1044
|
+
|
|
1045
|
+
const account = new YotoAccount({
|
|
1046
|
+
clientOptions: {
|
|
1047
|
+
clientId: 'your-client-id',
|
|
1048
|
+
refreshToken: 'your-refresh-token',
|
|
1049
|
+
accessToken: 'your-access-token',
|
|
1050
|
+
onTokenRefresh: async (event) => {
|
|
1051
|
+
await saveTokens(event)
|
|
1052
|
+
}
|
|
1053
|
+
},
|
|
1054
|
+
deviceOptions: {
|
|
1055
|
+
httpPollIntervalMs: 600000 // 10 minutes
|
|
1056
|
+
}
|
|
1057
|
+
})
|
|
1058
|
+
|
|
1059
|
+
// Account-level error handling
|
|
1060
|
+
account.on('error', (error, context) => {
|
|
1061
|
+
console.error(`Error in ${context.source}:`, error.message)
|
|
1062
|
+
})
|
|
1063
|
+
|
|
1064
|
+
// Listen to device added events
|
|
1065
|
+
account.on('deviceAdded', (deviceId, deviceModel) => {
|
|
1066
|
+
console.log(`Device ${deviceId} added`)
|
|
1067
|
+
|
|
1068
|
+
// Attach listeners to individual devices
|
|
1069
|
+
deviceModel.on('statusUpdate', (status, source) => {
|
|
1070
|
+
console.log(`${deviceId} battery: ${status.batteryLevelPercentage}%`)
|
|
1071
|
+
})
|
|
1072
|
+
|
|
1073
|
+
deviceModel.on('online', (metadata) => {
|
|
1074
|
+
console.log(`${deviceId} came online (${metadata.reason})`)
|
|
1075
|
+
})
|
|
1076
|
+
|
|
1077
|
+
deviceModel.on('offline', (metadata) => {
|
|
1078
|
+
console.log(`${deviceId} went offline (${metadata.reason})`)
|
|
1079
|
+
})
|
|
1080
|
+
})
|
|
1081
|
+
|
|
1082
|
+
await account.start()
|
|
1083
|
+
|
|
1084
|
+
// Access individual devices and attach listeners
|
|
1085
|
+
const device = account.getDevice('abc123')
|
|
1086
|
+
console.log('Device battery:', device.status.batteryLevelPercentage)
|
|
1087
|
+
|
|
1088
|
+
// Listen to specific device events
|
|
1089
|
+
device.on('playbackUpdate', (playback) => {
|
|
1090
|
+
console.log('Now playing:', playback.currentCardTitle)
|
|
1091
|
+
})
|
|
1092
|
+
|
|
1093
|
+
// Iterate all devices and attach listeners
|
|
1094
|
+
for (const [deviceId, deviceModel] of account.devices) {
|
|
1095
|
+
console.log(`${deviceId}: ${deviceModel.status.batteryLevelPercentage}%`)
|
|
1096
|
+
|
|
1097
|
+
deviceModel.on('configUpdate', (config) => {
|
|
1098
|
+
console.log(`${deviceId} config updated:`, config.name)
|
|
1099
|
+
})
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
await account.stop()
|
|
1103
|
+
```
|
|
1104
|
+
|
|
653
1105
|
## CLI Tools
|
|
654
1106
|
|
|
655
|
-
The library includes CLI tools for authentication and data inspection:
|
|
1107
|
+
The library includes CLI tools for authentication and data inspection. After installing the package, these commands are available globally:
|
|
656
1108
|
|
|
657
1109
|
### Authentication
|
|
658
1110
|
|
|
659
1111
|
```bash
|
|
660
1112
|
# Get initial tokens (device flow)
|
|
661
|
-
|
|
1113
|
+
yoto-auth --output .env
|
|
1114
|
+
# or: node bin/auth.js --output .env
|
|
662
1115
|
|
|
663
|
-
# Refresh tokens
|
|
664
|
-
|
|
1116
|
+
# Refresh existing tokens
|
|
1117
|
+
yoto-refresh-token
|
|
1118
|
+
# or: node bin/refresh-token.js
|
|
665
1119
|
|
|
666
|
-
# Show token info
|
|
667
|
-
|
|
1120
|
+
# Show token info and inspect JWT contents
|
|
1121
|
+
yoto-token-info
|
|
1122
|
+
# or: node bin/token-info.js
|
|
668
1123
|
```
|
|
669
1124
|
|
|
670
|
-
###
|
|
1125
|
+
### Devices
|
|
671
1126
|
|
|
672
1127
|
```bash
|
|
673
1128
|
# List all devices
|
|
674
|
-
|
|
1129
|
+
yoto-devices
|
|
1130
|
+
# or: node bin/devices.js
|
|
675
1131
|
|
|
676
1132
|
# Get device details with config
|
|
677
|
-
|
|
1133
|
+
yoto-devices --device-id abc123
|
|
678
1134
|
|
|
679
1135
|
# Get device status only
|
|
680
|
-
|
|
1136
|
+
yoto-devices --device-id abc123 --status
|
|
681
1137
|
|
|
682
1138
|
# Connect to MQTT and listen for messages
|
|
683
|
-
|
|
1139
|
+
yoto-devices --device-id abc123 --mqtt
|
|
684
1140
|
|
|
685
1141
|
# Sample MQTT messages for 10 seconds
|
|
686
|
-
|
|
1142
|
+
yoto-devices --device-id abc123 --mqtt --mqtt-timeout 10
|
|
1143
|
+
|
|
1144
|
+
# Use YotoDeviceModel to monitor device (HTTP + MQTT)
|
|
1145
|
+
yoto-device-model --device-id abc123
|
|
687
1146
|
|
|
1147
|
+
# Interactive TUI for device control (Prototype/WIP)
|
|
1148
|
+
yoto-device-tui --device-id abc123
|
|
1149
|
+
```
|
|
1150
|
+
|
|
1151
|
+
### Content
|
|
1152
|
+
|
|
1153
|
+
```bash
|
|
688
1154
|
# List all MYO content
|
|
689
|
-
|
|
1155
|
+
yoto-content
|
|
1156
|
+
# or: node bin/content.js
|
|
690
1157
|
|
|
691
1158
|
# Get specific card details
|
|
692
|
-
|
|
1159
|
+
yoto-content --card-id 5WsQg
|
|
693
1160
|
|
|
694
1161
|
# Get card with playable URLs
|
|
695
|
-
|
|
1162
|
+
yoto-content --card-id 5WsQg --playable
|
|
1163
|
+
```
|
|
1164
|
+
|
|
1165
|
+
### Family Library Groups
|
|
1166
|
+
|
|
1167
|
+
```bash
|
|
1168
|
+
# List all family library groups
|
|
1169
|
+
yoto-groups
|
|
1170
|
+
# or: node bin/groups.js
|
|
1171
|
+
|
|
1172
|
+
# Get specific group details
|
|
1173
|
+
yoto-groups --group-id abc123
|
|
1174
|
+
```
|
|
1175
|
+
|
|
1176
|
+
### Icons
|
|
1177
|
+
|
|
1178
|
+
```bash
|
|
1179
|
+
# List both public and user icons
|
|
1180
|
+
yoto-icons
|
|
1181
|
+
# or: node bin/icons.js
|
|
1182
|
+
|
|
1183
|
+
# List only public Yoto icons
|
|
1184
|
+
yoto-icons --public
|
|
1185
|
+
|
|
1186
|
+
# List only user custom icons
|
|
1187
|
+
yoto-icons --user
|
|
696
1188
|
```
|
|
697
1189
|
|
|
698
1190
|
## See also
|
|
@@ -734,3 +1226,4 @@ MIT
|
|
|
734
1226
|
[api-upload-icon]: https://yoto.dev/api/uploadcustomicon/
|
|
735
1227
|
[api-audio-upload]: https://yoto.dev/api/getanuploadurl/
|
|
736
1228
|
[api-cover-image]: https://yoto.dev/api/uploadcoverimage/
|
|
1229
|
+
[undici]: https://undici.nodejs.org/#/docs/api/Client.md
|