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
package/README.md
ADDED
|
@@ -0,0 +1,736 @@
|
|
|
1
|
+
# yoto-nodejs-client
|
|
2
|
+
[](https://www.npmjs.com/package/yoto-nodejs-client)
|
|
3
|
+
[](https://github.com/bcomnes/yoto-nodejs-client/actions)
|
|
4
|
+
|
|
5
|
+
[](https://npmtrends.com/yoto-nodejs-client)
|
|
6
|
+

|
|
7
|
+
[](https://github.com/neostandard/neostandard)
|
|
8
|
+
[](https://socket.dev/npm/package/yoto-nodejs-client)
|
|
9
|
+
|
|
10
|
+
A comprehensive Node.js client for the [Yoto API][yoto-api] with automatic token refresh, MQTT device communication, and full TypeScript support.
|
|
11
|
+
|
|
12
|
+
<p align="center">
|
|
13
|
+
<img src="yoto.png" alt="Yoto" width="200">
|
|
14
|
+
</p>
|
|
15
|
+
|
|
16
|
+
```console
|
|
17
|
+
npm install yoto-nodejs-client
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Usage
|
|
21
|
+
|
|
22
|
+
```js
|
|
23
|
+
import { YotoClient } from 'yoto-nodejs-client'
|
|
24
|
+
|
|
25
|
+
// Authenticate using device flow (CLI/server applications)
|
|
26
|
+
const deviceCodeResponse = await YotoClient.requestDeviceCode({
|
|
27
|
+
clientId: 'your-client-id'
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
console.log(`Visit ${deviceCodeResponse.verification_uri_complete}`)
|
|
31
|
+
console.log(`Enter code: ${deviceCodeResponse.user_code}`)
|
|
32
|
+
|
|
33
|
+
// Poll for token
|
|
34
|
+
const tokenResponse = await YotoClient.exchangeToken({
|
|
35
|
+
grantType: 'urn:ietf:params:oauth:grant-type:device_code',
|
|
36
|
+
deviceCode: deviceCodeResponse.device_code,
|
|
37
|
+
clientId: 'your-client-id'
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
// Create client with automatic token refresh
|
|
41
|
+
const client = new YotoClient({
|
|
42
|
+
clientId: 'your-client-id',
|
|
43
|
+
refreshToken: tokenResponse.refresh_token,
|
|
44
|
+
accessToken: tokenResponse.access_token,
|
|
45
|
+
onTokenRefresh: async (event) => {
|
|
46
|
+
// REQUIRED: Persist tokens when they refresh
|
|
47
|
+
await saveTokens({
|
|
48
|
+
accessToken: event.accessToken,
|
|
49
|
+
refreshToken: event.refreshToken,
|
|
50
|
+
expiresAt: event.expiresAt
|
|
51
|
+
})
|
|
52
|
+
}
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
// Get devices
|
|
56
|
+
const { devices } = await client.getDevices()
|
|
57
|
+
console.log('Your devices:', devices)
|
|
58
|
+
|
|
59
|
+
// Get device status
|
|
60
|
+
const status = await client.getDeviceStatus({
|
|
61
|
+
deviceId: devices[0].deviceId
|
|
62
|
+
})
|
|
63
|
+
console.log('Battery:', status.batteryLevelPercentage, '%')
|
|
64
|
+
|
|
65
|
+
// Get user's MYO content
|
|
66
|
+
const myoContent = await client.getUserMyoContent()
|
|
67
|
+
console.log('Your content:', myoContent)
|
|
68
|
+
|
|
69
|
+
// Connect to device via MQTT for real-time control
|
|
70
|
+
const mqtt = await client.createMqttClient({
|
|
71
|
+
deviceId: devices[0].deviceId
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
mqtt.on('events', (message) => {
|
|
75
|
+
console.log('Playing:', message.trackTitle)
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
mqtt.on('status', (message) => {
|
|
79
|
+
console.log('Volume:', message.volume, '%')
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
await mqtt.connect()
|
|
83
|
+
await mqtt.setVolume(50)
|
|
84
|
+
await mqtt.setAmbientHex('#FF0000')
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## API
|
|
88
|
+
|
|
89
|
+
### Authentication
|
|
90
|
+
|
|
91
|
+
#### `YotoClient.requestDeviceCode({ clientId, [scope], [audience] })`
|
|
92
|
+
|
|
93
|
+
Start the OAuth2 Device Authorization flow for CLI/server applications. Returns a device code and user verification URL.
|
|
94
|
+
|
|
95
|
+
- **clientId** - Your OAuth client ID
|
|
96
|
+
- **scope** - OAuth scopes (default: `'openid profile offline_access'`)
|
|
97
|
+
- **audience** - Token audience (default: `'https://api.yotoplay.com'`)
|
|
98
|
+
|
|
99
|
+
See [Yoto API: Device Code][api-device-code]
|
|
100
|
+
|
|
101
|
+
```js
|
|
102
|
+
const response = await YotoClient.requestDeviceCode({
|
|
103
|
+
clientId: 'your-client-id'
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
console.log(`Visit: ${response.verification_uri_complete}`)
|
|
107
|
+
console.log(`Or go to ${response.verification_uri} and enter: ${response.user_code}`)
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
#### `YotoClient.exchangeToken({ grantType, ...params })`
|
|
111
|
+
|
|
112
|
+
Exchange authorization code, refresh token, or device code for access tokens.
|
|
113
|
+
|
|
114
|
+
- **grantType** - `'authorization_code'`, `'refresh_token'`, or `'urn:ietf:params:oauth:grant-type:device_code'`
|
|
115
|
+
- **code** - Authorization code (for `authorization_code` grant)
|
|
116
|
+
- **refreshToken** - Refresh token (for `refresh_token` grant)
|
|
117
|
+
- **deviceCode** - Device code (for device code grant)
|
|
118
|
+
- **clientId** - OAuth client ID
|
|
119
|
+
- **redirectUri** - Redirect URI (for `authorization_code` grant)
|
|
120
|
+
- **codeVerifier** - PKCE code verifier (optional)
|
|
121
|
+
|
|
122
|
+
See [Yoto API: Token Exchange][api-token]
|
|
123
|
+
|
|
124
|
+
```js
|
|
125
|
+
// Exchange device code
|
|
126
|
+
const tokens = await YotoClient.exchangeToken({
|
|
127
|
+
grantType: 'urn:ietf:params:oauth:grant-type:device_code',
|
|
128
|
+
deviceCode: response.device_code,
|
|
129
|
+
clientId: 'your-client-id'
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
// Refresh token
|
|
133
|
+
const refreshed = await YotoClient.exchangeToken({
|
|
134
|
+
grantType: 'refresh_token',
|
|
135
|
+
refreshToken: tokens.refresh_token,
|
|
136
|
+
clientId: 'your-client-id'
|
|
137
|
+
})
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
#### `YotoClient.getAuthorizeUrl({ clientId, redirectUri, responseType, state, ...params })`
|
|
141
|
+
|
|
142
|
+
Get authorization URL for browser-based OAuth flow. Returns a URL string to redirect users to.
|
|
143
|
+
|
|
144
|
+
See [Yoto API: Authorization][api-authorize]
|
|
145
|
+
|
|
146
|
+
### Client Instance
|
|
147
|
+
|
|
148
|
+
#### `new YotoClient({ clientId, refreshToken, accessToken, onTokenRefresh, [options] })`
|
|
149
|
+
|
|
150
|
+
Create a new Yoto API client with automatic token refresh.
|
|
151
|
+
|
|
152
|
+
- **clientId** - OAuth client ID
|
|
153
|
+
- **refreshToken** - OAuth refresh token
|
|
154
|
+
- **accessToken** - Initial access token (JWT)
|
|
155
|
+
- **onTokenRefresh** - **REQUIRED** callback for token refresh events. You MUST persist tokens here.
|
|
156
|
+
- **bufferSeconds** - Seconds before expiration to refresh (default: 30)
|
|
157
|
+
- **userAgent** - Optional user agent string to identify your application
|
|
158
|
+
- **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
|
|
162
|
+
|
|
163
|
+
```js
|
|
164
|
+
const client = new YotoClient({
|
|
165
|
+
clientId: 'your-client-id',
|
|
166
|
+
refreshToken: 'stored-refresh-token',
|
|
167
|
+
accessToken: 'stored-access-token',
|
|
168
|
+
userAgent: 'MyApp/1.0.0', // Optional - identifies your application
|
|
169
|
+
defaultRequestOptions: { // Optional - undici request options for all requests
|
|
170
|
+
bodyTimeout: 30000, // 30 second timeout
|
|
171
|
+
headersTimeout: 10000, // 10 second header timeout
|
|
172
|
+
// dispatcher, signal, etc.
|
|
173
|
+
},
|
|
174
|
+
onTokenRefresh: async ({ accessToken, refreshToken, expiresAt }) => {
|
|
175
|
+
// Save to database, file, etc.
|
|
176
|
+
await db.saveTokens({ accessToken, refreshToken, expiresAt })
|
|
177
|
+
}
|
|
178
|
+
})
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
#### Request Options
|
|
182
|
+
|
|
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.
|
|
184
|
+
|
|
185
|
+
```js
|
|
186
|
+
// Override timeout for a specific request
|
|
187
|
+
const content = await client.getContent({
|
|
188
|
+
cardId: '5WsQg',
|
|
189
|
+
requestOptions: {
|
|
190
|
+
bodyTimeout: 60000, // 60 second timeout for this request only
|
|
191
|
+
headersTimeout: 20000
|
|
192
|
+
}
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
// Use AbortSignal to cancel requests
|
|
196
|
+
const controller = new AbortController()
|
|
197
|
+
setTimeout(() => controller.abort(), 5000)
|
|
198
|
+
|
|
199
|
+
try {
|
|
200
|
+
const devices = await client.getDevices({
|
|
201
|
+
requestOptions: { signal: controller.signal }
|
|
202
|
+
})
|
|
203
|
+
} catch (err) {
|
|
204
|
+
if (err.name === 'AbortError') {
|
|
205
|
+
console.log('Request was aborted')
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Use custom dispatcher for connection pooling
|
|
210
|
+
import { Agent } from 'undici'
|
|
211
|
+
const agent = new Agent({ connections: 10 })
|
|
212
|
+
|
|
213
|
+
const status = await client.getDeviceStatus({
|
|
214
|
+
deviceId: 'abc123',
|
|
215
|
+
requestOptions: { dispatcher: agent }
|
|
216
|
+
})
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
**Available Request Options:**
|
|
220
|
+
- `bodyTimeout` - Body timeout in milliseconds
|
|
221
|
+
- `headersTimeout` - Headers timeout in milliseconds
|
|
222
|
+
- `signal` - AbortSignal to cancel the request
|
|
223
|
+
- `dispatcher` - Custom undici dispatcher (for connection pooling, proxies, etc.)
|
|
224
|
+
- `reset` - Reset connection after request
|
|
225
|
+
- `throwOnError` - Throw on HTTP error status codes
|
|
226
|
+
- `idempotent` - Whether the requests can be safely retried
|
|
227
|
+
- `blocking` - Whether the response is expected to take a long time
|
|
228
|
+
- Other undici RequestOptions (see [undici documentation](https://undici.nodejs.org/))
|
|
229
|
+
|
|
230
|
+
### Content API
|
|
231
|
+
|
|
232
|
+
#### `await client.getContent({ cardId, [timezone], [signingType], [playable] })`
|
|
233
|
+
|
|
234
|
+
Get content/card details including metadata, chapters, and optionally playback URLs.
|
|
235
|
+
|
|
236
|
+
See [Yoto API: Get Content][api-get-content]
|
|
237
|
+
|
|
238
|
+
```js
|
|
239
|
+
const content = await client.getContent({
|
|
240
|
+
cardId: '5WsQg',
|
|
241
|
+
playable: true // Include signed playback URLs
|
|
242
|
+
})
|
|
243
|
+
|
|
244
|
+
console.log(content.title)
|
|
245
|
+
console.log(content.chapters)
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
#### `await client.getUserMyoContent({ [showDeleted] })`
|
|
249
|
+
|
|
250
|
+
Get user's MYO (Make Your Own) content library.
|
|
251
|
+
|
|
252
|
+
See [Yoto API: Get User's MYO Content][api-get-myo]
|
|
253
|
+
|
|
254
|
+
```js
|
|
255
|
+
const myoContent = await client.getUserMyoContent({
|
|
256
|
+
showDeleted: false
|
|
257
|
+
})
|
|
258
|
+
|
|
259
|
+
myoContent.cards.forEach(card => {
|
|
260
|
+
console.log(`${card.metadata.title} - ${card.chapters.length} chapters`)
|
|
261
|
+
})
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
#### `await client.createOrUpdateContent({ content })`
|
|
265
|
+
|
|
266
|
+
Create new content or update existing content by cardId.
|
|
267
|
+
|
|
268
|
+
See [Yoto API: Create or Update Content][api-create-content]
|
|
269
|
+
|
|
270
|
+
```js
|
|
271
|
+
const newCard = await client.createOrUpdateContent({
|
|
272
|
+
content: {
|
|
273
|
+
title: 'My Story',
|
|
274
|
+
chapters: [
|
|
275
|
+
{
|
|
276
|
+
title: 'Chapter 1',
|
|
277
|
+
tracks: [
|
|
278
|
+
{
|
|
279
|
+
title: 'Part 1',
|
|
280
|
+
key: 'upload-id-from-audio-upload'
|
|
281
|
+
}
|
|
282
|
+
]
|
|
283
|
+
}
|
|
284
|
+
]
|
|
285
|
+
}
|
|
286
|
+
})
|
|
287
|
+
|
|
288
|
+
console.log('Created card:', newCard.cardId)
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
#### `await client.deleteContent({ cardId })`
|
|
292
|
+
|
|
293
|
+
Delete content/card.
|
|
294
|
+
|
|
295
|
+
See [Yoto API: Delete Content][api-delete-content]
|
|
296
|
+
|
|
297
|
+
```js
|
|
298
|
+
await client.deleteContent({ cardId: '5WsQg' })
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
### Devices API
|
|
302
|
+
|
|
303
|
+
#### `await client.getDevices()`
|
|
304
|
+
|
|
305
|
+
Get all devices for authenticated user.
|
|
306
|
+
|
|
307
|
+
See [Yoto API: Get Devices][api-get-devices]
|
|
308
|
+
|
|
309
|
+
```js
|
|
310
|
+
const { devices } = await client.getDevices()
|
|
311
|
+
|
|
312
|
+
devices.forEach(device => {
|
|
313
|
+
console.log(`${device.name} (${device.deviceId}) - ${device.online ? 'online' : 'offline'}`)
|
|
314
|
+
})
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
#### `await client.getDeviceStatus({ deviceId })`
|
|
318
|
+
|
|
319
|
+
Get current status of a specific device including battery, volume, active card, etc.
|
|
320
|
+
|
|
321
|
+
See [Yoto API: Get Device Status][api-device-status]
|
|
322
|
+
|
|
323
|
+
```js
|
|
324
|
+
const status = await client.getDeviceStatus({
|
|
325
|
+
deviceId: 'abc123'
|
|
326
|
+
})
|
|
327
|
+
|
|
328
|
+
console.log('Battery:', status.batteryLevelPercentage, '%')
|
|
329
|
+
console.log('Charging:', status.isCharging)
|
|
330
|
+
console.log('Volume:', status.userVolumePercentage, '%')
|
|
331
|
+
console.log('Active card:', status.activeCard)
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
#### `await client.getDeviceConfig({ deviceId })`
|
|
335
|
+
|
|
336
|
+
Get device configuration including settings, timezone, shortcuts, etc.
|
|
337
|
+
|
|
338
|
+
See [Yoto API: Get Device Config][api-device-config]
|
|
339
|
+
|
|
340
|
+
```js
|
|
341
|
+
const config = await client.getDeviceConfig({
|
|
342
|
+
deviceId: 'abc123'
|
|
343
|
+
})
|
|
344
|
+
|
|
345
|
+
console.log('Name:', config.device.name)
|
|
346
|
+
console.log('Day time:', config.device.config.dayTime)
|
|
347
|
+
console.log('Night time:', config.device.config.nightTime)
|
|
348
|
+
console.log('Max volume:', config.device.config.maxVolumeLimit)
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
#### `await client.updateDeviceConfig({ deviceId, configUpdate })`
|
|
352
|
+
|
|
353
|
+
Update device configuration settings.
|
|
354
|
+
|
|
355
|
+
See [Yoto API: Update Device Config][api-update-config]
|
|
356
|
+
|
|
357
|
+
```js
|
|
358
|
+
await client.updateDeviceConfig({
|
|
359
|
+
deviceId: 'abc123',
|
|
360
|
+
configUpdate: {
|
|
361
|
+
name: 'Bedroom Player',
|
|
362
|
+
config: {
|
|
363
|
+
dayTime: '07:00',
|
|
364
|
+
nightTime: '19:00',
|
|
365
|
+
maxVolumeLimit: '80'
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
})
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
#### `await client.updateDeviceShortcuts({ deviceId, shortcutsUpdate })`
|
|
372
|
+
|
|
373
|
+
Update device shortcuts configuration (beta feature).
|
|
374
|
+
|
|
375
|
+
See [Yoto API: Update Shortcuts][api-update-shortcuts]
|
|
376
|
+
|
|
377
|
+
#### `await client.sendDeviceCommand({ deviceId, command })`
|
|
378
|
+
|
|
379
|
+
Send MQTT command to device via HTTP API (alternative to MQTT client).
|
|
380
|
+
|
|
381
|
+
See [Yoto API: Send Device Command][api-send-command]
|
|
382
|
+
|
|
383
|
+
```js
|
|
384
|
+
await client.sendDeviceCommand({
|
|
385
|
+
deviceId: 'abc123',
|
|
386
|
+
command: {
|
|
387
|
+
volume: 50
|
|
388
|
+
}
|
|
389
|
+
})
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
### Family Library Groups API
|
|
393
|
+
|
|
394
|
+
#### `await client.getGroups()`
|
|
395
|
+
|
|
396
|
+
Get all family library groups.
|
|
397
|
+
|
|
398
|
+
See [Yoto API: Get Groups][api-get-groups]
|
|
399
|
+
|
|
400
|
+
```js
|
|
401
|
+
const groups = await client.getGroups()
|
|
402
|
+
|
|
403
|
+
groups.forEach(group => {
|
|
404
|
+
console.log(`${group.name}: ${group.items.length} items`)
|
|
405
|
+
})
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
#### `await client.createGroup({ group })`
|
|
409
|
+
|
|
410
|
+
Create a new family library group.
|
|
411
|
+
|
|
412
|
+
See [Yoto API: Create Group][api-create-group]
|
|
413
|
+
|
|
414
|
+
```js
|
|
415
|
+
const group = await client.createGroup({
|
|
416
|
+
group: {
|
|
417
|
+
name: 'Bedtime Stories',
|
|
418
|
+
imageId: 'fp-cards',
|
|
419
|
+
items: [
|
|
420
|
+
{ contentId: '5WsQg' },
|
|
421
|
+
{ contentId: '7KpLq' }
|
|
422
|
+
]
|
|
423
|
+
}
|
|
424
|
+
})
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
#### `await client.getGroup({ groupId })`
|
|
428
|
+
|
|
429
|
+
Get a specific group by ID.
|
|
430
|
+
|
|
431
|
+
See [Yoto API: Get a Group][api-get-group]
|
|
432
|
+
|
|
433
|
+
#### `await client.updateGroup({ groupId, group })`
|
|
434
|
+
|
|
435
|
+
Update an existing group.
|
|
436
|
+
|
|
437
|
+
See [Yoto API: Update Group][api-update-group]
|
|
438
|
+
|
|
439
|
+
#### `await client.deleteGroup({ groupId })`
|
|
440
|
+
|
|
441
|
+
Delete a group permanently.
|
|
442
|
+
|
|
443
|
+
See [Yoto API: Delete Group][api-delete-group]
|
|
444
|
+
|
|
445
|
+
### Family API
|
|
446
|
+
|
|
447
|
+
#### `await client.getFamilyImages()`
|
|
448
|
+
|
|
449
|
+
Get list of uploaded family images.
|
|
450
|
+
|
|
451
|
+
See [Yoto API: Get Family Images][api-family-images]
|
|
452
|
+
|
|
453
|
+
```js
|
|
454
|
+
const { images } = await client.getFamilyImages()
|
|
455
|
+
|
|
456
|
+
images.forEach(image => {
|
|
457
|
+
console.log(`${image.name || 'Unnamed'}: ${image.imageId}`)
|
|
458
|
+
})
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
#### `await client.getAFamilyImage({ imageId, size })`
|
|
462
|
+
|
|
463
|
+
Get signed URL for a family image.
|
|
464
|
+
|
|
465
|
+
See [Yoto API: Get a Family Image][api-get-family-image]
|
|
466
|
+
|
|
467
|
+
```js
|
|
468
|
+
const { imageUrl } = await client.getAFamilyImage({
|
|
469
|
+
imageId: 'abc123hash',
|
|
470
|
+
size: '640x480' // or '320x320'
|
|
471
|
+
})
|
|
472
|
+
|
|
473
|
+
console.log('Image URL:', imageUrl)
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
#### `await client.uploadAFamilyImage({ imageData })`
|
|
477
|
+
|
|
478
|
+
Upload a family image for use across Yoto features.
|
|
479
|
+
|
|
480
|
+
See [Yoto API: Upload Family Image][api-upload-family-image]
|
|
481
|
+
|
|
482
|
+
```js
|
|
483
|
+
import { readFile } from 'fs/promises'
|
|
484
|
+
|
|
485
|
+
const imageData = await readFile('./family-photo.jpg')
|
|
486
|
+
const result = await client.uploadAFamilyImage({ imageData })
|
|
487
|
+
|
|
488
|
+
console.log('Image ID:', result.imageId)
|
|
489
|
+
```
|
|
490
|
+
|
|
491
|
+
### Icons API
|
|
492
|
+
|
|
493
|
+
#### `await client.getPublicIcons()`
|
|
494
|
+
|
|
495
|
+
Get list of public display icons available to all users.
|
|
496
|
+
|
|
497
|
+
See [Yoto API: Get Public Icons][api-public-icons]
|
|
498
|
+
|
|
499
|
+
```js
|
|
500
|
+
const { displayIcons } = await client.getPublicIcons()
|
|
501
|
+
|
|
502
|
+
displayIcons.forEach(icon => {
|
|
503
|
+
console.log(`${icon.title}: ${icon.displayIconId}`)
|
|
504
|
+
})
|
|
505
|
+
```
|
|
506
|
+
|
|
507
|
+
#### `await client.getUserIcons()`
|
|
508
|
+
|
|
509
|
+
Get user's custom uploaded icons.
|
|
510
|
+
|
|
511
|
+
See [Yoto API: Get User Icons][api-user-icons]
|
|
512
|
+
|
|
513
|
+
#### `await client.uploadIcon({ imageData, [autoConvert], [filename] })`
|
|
514
|
+
|
|
515
|
+
Upload a custom 16×16px display icon.
|
|
516
|
+
|
|
517
|
+
See [Yoto API: Upload Custom Icon][api-upload-icon]
|
|
518
|
+
|
|
519
|
+
```js
|
|
520
|
+
import { readFile } from 'fs/promises'
|
|
521
|
+
|
|
522
|
+
const imageData = await readFile('./my-icon.png')
|
|
523
|
+
const result = await client.uploadIcon({
|
|
524
|
+
imageData,
|
|
525
|
+
autoConvert: true, // Auto-resize and process
|
|
526
|
+
filename: 'my-custom-icon'
|
|
527
|
+
})
|
|
528
|
+
|
|
529
|
+
console.log('Icon ID:', result.displayIcon.displayIconId)
|
|
530
|
+
```
|
|
531
|
+
|
|
532
|
+
### Media API
|
|
533
|
+
|
|
534
|
+
#### `await client.getAudioUploadUrl({ sha256, [filename] })`
|
|
535
|
+
|
|
536
|
+
Get signed URL for uploading audio files. Files are deduplicated by SHA256 hash.
|
|
537
|
+
|
|
538
|
+
See [Yoto API: Get Audio Upload URL][api-audio-upload]
|
|
539
|
+
|
|
540
|
+
```js
|
|
541
|
+
import { createHash } from 'crypto'
|
|
542
|
+
import { readFile } from 'fs/promises'
|
|
543
|
+
|
|
544
|
+
const audioData = await readFile('./story.mp3')
|
|
545
|
+
const sha256 = createHash('sha256').update(audioData).digest('hex')
|
|
546
|
+
|
|
547
|
+
const { upload } = await client.getAudioUploadUrl({
|
|
548
|
+
sha256,
|
|
549
|
+
filename: 'story.mp3'
|
|
550
|
+
})
|
|
551
|
+
|
|
552
|
+
if (upload.uploadUrl) {
|
|
553
|
+
// File doesn't exist, upload it
|
|
554
|
+
await fetch(upload.uploadUrl, {
|
|
555
|
+
method: 'PUT',
|
|
556
|
+
body: audioData
|
|
557
|
+
})
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
// Use upload.uploadId in content creation
|
|
561
|
+
```
|
|
562
|
+
|
|
563
|
+
#### `await client.uploadCoverImage({ [imageData], [imageUrl], [coverType], [autoConvert], [filename] })`
|
|
564
|
+
|
|
565
|
+
Upload a cover image for content cards.
|
|
566
|
+
|
|
567
|
+
See [Yoto API: Upload Cover Image][api-cover-image]
|
|
568
|
+
|
|
569
|
+
```js
|
|
570
|
+
import { readFile } from 'fs/promises'
|
|
571
|
+
|
|
572
|
+
const imageData = await readFile('./cover.jpg')
|
|
573
|
+
const { coverImage } = await client.uploadCoverImage({
|
|
574
|
+
imageData,
|
|
575
|
+
coverType: 'default', // 638×1011px
|
|
576
|
+
autoConvert: true
|
|
577
|
+
})
|
|
578
|
+
|
|
579
|
+
console.log('Cover image ID:', coverImage.mediaId)
|
|
580
|
+
```
|
|
581
|
+
|
|
582
|
+
### MQTT Client
|
|
583
|
+
|
|
584
|
+
#### `await client.createMqttClient({ deviceId, [options] })`
|
|
585
|
+
|
|
586
|
+
Create an MQTT client for real-time device communication and control.
|
|
587
|
+
|
|
588
|
+
See [Yoto MQTT Documentation][mqtt-docs]
|
|
589
|
+
|
|
590
|
+
```js
|
|
591
|
+
const mqtt = await client.createMqttClient({
|
|
592
|
+
deviceId: 'abc123',
|
|
593
|
+
autoResubscribe: true,
|
|
594
|
+
keepAliveSeconds: 1200
|
|
595
|
+
})
|
|
596
|
+
|
|
597
|
+
// Listen for real-time events
|
|
598
|
+
mqtt.on('events', (message) => {
|
|
599
|
+
console.log('Track:', message.trackTitle)
|
|
600
|
+
console.log('Card:', message.cardTitle)
|
|
601
|
+
console.log('Status:', message.playbackStatus)
|
|
602
|
+
})
|
|
603
|
+
|
|
604
|
+
// Listen for status updates
|
|
605
|
+
mqtt.on('status', (message) => {
|
|
606
|
+
console.log('Volume:', message.volume)
|
|
607
|
+
console.log('Battery:', message.batteryLevel)
|
|
608
|
+
console.log('Charging:', message.charging)
|
|
609
|
+
})
|
|
610
|
+
|
|
611
|
+
// Listen for command responses
|
|
612
|
+
mqtt.on('response', (message) => {
|
|
613
|
+
console.log('Command response:', message)
|
|
614
|
+
})
|
|
615
|
+
|
|
616
|
+
// Connect to device
|
|
617
|
+
await mqtt.connect()
|
|
618
|
+
|
|
619
|
+
// Control device
|
|
620
|
+
await mqtt.setVolume(50)
|
|
621
|
+
await mqtt.setAmbientHex('#FF0000')
|
|
622
|
+
await mqtt.setSleepTimer(30) // 30 minutes
|
|
623
|
+
await mqtt.startCard({ cardId: '5WsQg' })
|
|
624
|
+
await mqtt.pauseCard()
|
|
625
|
+
await mqtt.resumeCard()
|
|
626
|
+
await mqtt.stopCard()
|
|
627
|
+
|
|
628
|
+
// Disconnect when done
|
|
629
|
+
await mqtt.disconnect()
|
|
630
|
+
```
|
|
631
|
+
|
|
632
|
+
#### MQTT Events
|
|
633
|
+
|
|
634
|
+
The MQTT client emits three types of messages:
|
|
635
|
+
|
|
636
|
+
- **`events`** - Real-time playback events (track changes, play/pause, volume adjustments)
|
|
637
|
+
- **`status`** - Device status updates (battery, configuration, online state)
|
|
638
|
+
- **`response`** - Command confirmation responses
|
|
639
|
+
|
|
640
|
+
#### MQTT Methods
|
|
641
|
+
|
|
642
|
+
- `await mqtt.connect()` - Connect to device MQTT broker
|
|
643
|
+
- `await mqtt.disconnect()` - Disconnect from broker
|
|
644
|
+
- `await mqtt.setVolume(volume)` - Set volume (0-100)
|
|
645
|
+
- `await mqtt.setAmbientHex(hex)` - Set ambient light color (e.g., '#FF0000')
|
|
646
|
+
- `await mqtt.setSleepTimer(minutes)` - Set sleep timer (0 to disable)
|
|
647
|
+
- `await mqtt.startCard({ cardId, chapterKey, trackKey })` - Start playing a card
|
|
648
|
+
- `await mqtt.pauseCard()` - Pause current playback
|
|
649
|
+
- `await mqtt.resumeCard()` - Resume playback
|
|
650
|
+
- `await mqtt.stopCard()` - Stop playback
|
|
651
|
+
- `await mqtt.reboot()` - Reboot device
|
|
652
|
+
|
|
653
|
+
## CLI Tools
|
|
654
|
+
|
|
655
|
+
The library includes CLI tools for authentication and data inspection:
|
|
656
|
+
|
|
657
|
+
### Authentication
|
|
658
|
+
|
|
659
|
+
```bash
|
|
660
|
+
# Get initial tokens (device flow)
|
|
661
|
+
node bin/auth.js --output .env
|
|
662
|
+
|
|
663
|
+
# Refresh tokens
|
|
664
|
+
node bin/refresh-token.js
|
|
665
|
+
|
|
666
|
+
# Show token info
|
|
667
|
+
node bin/token-info.js
|
|
668
|
+
```
|
|
669
|
+
|
|
670
|
+
### Data Inspection
|
|
671
|
+
|
|
672
|
+
```bash
|
|
673
|
+
# List all devices
|
|
674
|
+
node bin/devices.js
|
|
675
|
+
|
|
676
|
+
# Get device details with config
|
|
677
|
+
node bin/devices.js --device-id abc123
|
|
678
|
+
|
|
679
|
+
# Get device status only
|
|
680
|
+
node bin/devices.js --device-id abc123 --status
|
|
681
|
+
|
|
682
|
+
# Connect to MQTT and listen for messages
|
|
683
|
+
node bin/devices.js --device-id abc123 --mqtt
|
|
684
|
+
|
|
685
|
+
# Sample MQTT messages for 10 seconds
|
|
686
|
+
node bin/devices.js --device-id abc123 --mqtt --mqtt-stop-after-seconds 10
|
|
687
|
+
|
|
688
|
+
# List all MYO content
|
|
689
|
+
node bin/content.js
|
|
690
|
+
|
|
691
|
+
# Get specific card details
|
|
692
|
+
node bin/content.js --card-id 5WsQg
|
|
693
|
+
|
|
694
|
+
# Get card with playable URLs
|
|
695
|
+
node bin/content.js --card-id 5WsQg --playable
|
|
696
|
+
```
|
|
697
|
+
|
|
698
|
+
## See also
|
|
699
|
+
|
|
700
|
+
- [Yoto API Documentation][yoto-api]
|
|
701
|
+
- [Yoto MQTT Documentation][mqtt-docs]
|
|
702
|
+
- [Yoto Developer Portal][yoto-dev]
|
|
703
|
+
|
|
704
|
+
## License
|
|
705
|
+
|
|
706
|
+
MIT
|
|
707
|
+
|
|
708
|
+
[yoto-api]: https://yoto.dev/api/
|
|
709
|
+
[yoto-dev]: https://yoto.dev/
|
|
710
|
+
[mqtt-docs]: https://yoto.dev/players-mqtt/mqtt-docs/
|
|
711
|
+
[api-device-code]: https://yoto.dev/api/post-oauth-device-code/
|
|
712
|
+
[api-token]: https://yoto.dev/api/post-oauth-token/
|
|
713
|
+
[api-authorize]: https://yoto.dev/api/get-authorize/
|
|
714
|
+
[api-get-content]: https://yoto.dev/api/getcontent/
|
|
715
|
+
[api-get-myo]: https://yoto.dev/api/getusersmyocontent/
|
|
716
|
+
[api-create-content]: https://yoto.dev/api/createorupdatecontent/
|
|
717
|
+
[api-delete-content]: https://yoto.dev/api/deletecontent/
|
|
718
|
+
[api-get-devices]: https://yoto.dev/api/getdevices/
|
|
719
|
+
[api-device-status]: https://yoto.dev/api/getdevicestatus/
|
|
720
|
+
[api-device-config]: https://yoto.dev/api/getdeviceconfig/
|
|
721
|
+
[api-update-config]: https://yoto.dev/api/updatedeviceconfig/
|
|
722
|
+
[api-update-shortcuts]: https://yoto.dev/api/updateshortcutsbeta/
|
|
723
|
+
[api-send-command]: https://yoto.dev/api/senddevicecommand/
|
|
724
|
+
[api-get-groups]: https://yoto.dev/api/getgroups/
|
|
725
|
+
[api-create-group]: https://yoto.dev/api/createagroup/
|
|
726
|
+
[api-get-group]: https://yoto.dev/api/getagroup/
|
|
727
|
+
[api-update-group]: https://yoto.dev/api/updateagroup/
|
|
728
|
+
[api-delete-group]: https://yoto.dev/api/deleteagroup/
|
|
729
|
+
[api-family-images]: https://yoto.dev/api/getfamilyimages/
|
|
730
|
+
[api-get-family-image]: https://yoto.dev/api/getafamilyimage/
|
|
731
|
+
[api-upload-family-image]: https://yoto.dev/api/uploadafamilyimage/
|
|
732
|
+
[api-public-icons]: https://yoto.dev/api/getpublicicons/
|
|
733
|
+
[api-user-icons]: https://yoto.dev/api/getusericons/
|
|
734
|
+
[api-upload-icon]: https://yoto.dev/api/uploadcustomicon/
|
|
735
|
+
[api-audio-upload]: https://yoto.dev/api/getanuploadurl/
|
|
736
|
+
[api-cover-image]: https://yoto.dev/api/uploadcoverimage/
|