yoto-nodejs-client 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +736 -0
- package/bin/auth.d.ts +3 -0
- package/bin/auth.d.ts.map +1 -0
- package/bin/auth.js +130 -0
- package/bin/content.d.ts +3 -0
- package/bin/content.d.ts.map +1 -0
- package/bin/content.js +117 -0
- package/bin/devices.d.ts +3 -0
- package/bin/devices.d.ts.map +1 -0
- package/bin/devices.js +239 -0
- package/bin/groups.d.ts +3 -0
- package/bin/groups.d.ts.map +1 -0
- package/bin/groups.js +80 -0
- package/bin/icons.d.ts +3 -0
- package/bin/icons.d.ts.map +1 -0
- package/bin/icons.js +100 -0
- package/bin/lib/cli-helpers.d.ts +21 -0
- package/bin/lib/cli-helpers.d.ts.map +1 -0
- package/bin/lib/cli-helpers.js +140 -0
- package/bin/lib/token-helpers.d.ts +14 -0
- package/bin/lib/token-helpers.d.ts.map +1 -0
- package/bin/lib/token-helpers.js +151 -0
- package/bin/refresh-token.d.ts +3 -0
- package/bin/refresh-token.d.ts.map +1 -0
- package/bin/refresh-token.js +168 -0
- package/bin/token-info.d.ts +3 -0
- package/bin/token-info.d.ts.map +1 -0
- package/bin/token-info.js +351 -0
- package/index.d.ts +218 -0
- package/index.d.ts.map +1 -0
- package/index.js +689 -0
- package/lib/api-endpoints/auth.d.ts +56 -0
- package/lib/api-endpoints/auth.d.ts.map +1 -0
- package/lib/api-endpoints/auth.js +209 -0
- package/lib/api-endpoints/auth.test.js +27 -0
- package/lib/api-endpoints/constants.d.ts +6 -0
- package/lib/api-endpoints/constants.d.ts.map +1 -0
- package/lib/api-endpoints/constants.js +31 -0
- package/lib/api-endpoints/content.d.ts +275 -0
- package/lib/api-endpoints/content.d.ts.map +1 -0
- package/lib/api-endpoints/content.js +518 -0
- package/lib/api-endpoints/content.test.js +250 -0
- package/lib/api-endpoints/devices.d.ts +202 -0
- package/lib/api-endpoints/devices.d.ts.map +1 -0
- package/lib/api-endpoints/devices.js +404 -0
- package/lib/api-endpoints/devices.test.js +483 -0
- package/lib/api-endpoints/family-library-groups.d.ts +75 -0
- package/lib/api-endpoints/family-library-groups.d.ts.map +1 -0
- package/lib/api-endpoints/family-library-groups.js +247 -0
- package/lib/api-endpoints/family-library-groups.test.js +272 -0
- package/lib/api-endpoints/family.d.ts +39 -0
- package/lib/api-endpoints/family.d.ts.map +1 -0
- package/lib/api-endpoints/family.js +166 -0
- package/lib/api-endpoints/family.test.js +184 -0
- package/lib/api-endpoints/helpers.d.ts +29 -0
- package/lib/api-endpoints/helpers.d.ts.map +1 -0
- package/lib/api-endpoints/helpers.js +104 -0
- package/lib/api-endpoints/icons.d.ts +62 -0
- package/lib/api-endpoints/icons.d.ts.map +1 -0
- package/lib/api-endpoints/icons.js +201 -0
- package/lib/api-endpoints/icons.test.js +118 -0
- package/lib/api-endpoints/media.d.ts +37 -0
- package/lib/api-endpoints/media.d.ts.map +1 -0
- package/lib/api-endpoints/media.js +155 -0
- package/lib/api-endpoints/test-helpers.d.ts +7 -0
- package/lib/api-endpoints/test-helpers.d.ts.map +1 -0
- package/lib/api-endpoints/test-helpers.js +64 -0
- package/lib/mqtt/client.d.ts +124 -0
- package/lib/mqtt/client.d.ts.map +1 -0
- package/lib/mqtt/client.js +558 -0
- package/lib/mqtt/commands.d.ts +69 -0
- package/lib/mqtt/commands.d.ts.map +1 -0
- package/lib/mqtt/commands.js +238 -0
- package/lib/mqtt/factory.d.ts +12 -0
- package/lib/mqtt/factory.d.ts.map +1 -0
- package/lib/mqtt/factory.js +107 -0
- package/lib/mqtt/index.d.ts +5 -0
- package/lib/mqtt/index.d.ts.map +1 -0
- package/lib/mqtt/index.js +81 -0
- package/lib/mqtt/mqtt.test.js +168 -0
- package/lib/mqtt/topics.d.ts +34 -0
- package/lib/mqtt/topics.d.ts.map +1 -0
- package/lib/mqtt/topics.js +295 -0
- package/lib/pkg.cjs +3 -0
- package/lib/pkg.d.cts +70 -0
- package/lib/pkg.d.cts.map +1 -0
- package/lib/token.d.ts +29 -0
- package/lib/token.d.ts.map +1 -0
- package/lib/token.js +240 -0
- package/package.json +91 -0
- package/yoto.png +0 -0
|
@@ -0,0 +1,483 @@
|
|
|
1
|
+
import test from 'node:test'
|
|
2
|
+
import assert from 'node:assert'
|
|
3
|
+
import { getDevices, getDeviceStatus, getDeviceConfig } from './devices.js'
|
|
4
|
+
import { YotoAPIError } from './helpers.js'
|
|
5
|
+
import { loadTestTokens, logResponse } from './test-helpers.js'
|
|
6
|
+
|
|
7
|
+
const { accessToken } = loadTestTokens()
|
|
8
|
+
|
|
9
|
+
test('getDevices', async (t) => {
|
|
10
|
+
await t.test('should fetch user devices', async () => {
|
|
11
|
+
const response = await getDevices({
|
|
12
|
+
accessToken
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
// Log response for type verification and documentation
|
|
16
|
+
logResponse('GET /device-v2/devices/mine', response)
|
|
17
|
+
|
|
18
|
+
// Validate response structure matches YotoDevicesResponse
|
|
19
|
+
assert.ok(response, 'Response should exist')
|
|
20
|
+
assert.ok(Array.isArray(response.devices), 'Response should have devices array')
|
|
21
|
+
assert.ok(response.devices.length > 0, 'User should have at least one device')
|
|
22
|
+
|
|
23
|
+
// Validate device structure
|
|
24
|
+
const device = response.devices[0]
|
|
25
|
+
assert.ok(device, 'Device should exist')
|
|
26
|
+
assert.ok(typeof device.deviceId === 'string', 'Device should have deviceId string')
|
|
27
|
+
assert.ok(typeof device.name === 'string', 'Device should have name string')
|
|
28
|
+
assert.ok(typeof device.description === 'string', 'Device should have description string')
|
|
29
|
+
assert.ok(typeof device.online === 'boolean', 'Device should have online boolean')
|
|
30
|
+
assert.ok(typeof device.releaseChannel === 'string', 'Device should have releaseChannel string')
|
|
31
|
+
assert.ok(typeof device.deviceType === 'string', 'Device should have deviceType string')
|
|
32
|
+
assert.ok(typeof device.deviceFamily === 'string', 'Device should have deviceFamily string')
|
|
33
|
+
assert.ok(typeof device.deviceGroup === 'string', 'Device should have deviceGroup string')
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
await t.test('should fail with invalid token', async () => {
|
|
37
|
+
await assert.rejects(
|
|
38
|
+
async () => {
|
|
39
|
+
await getDevices({
|
|
40
|
+
accessToken: 'invalid-token'
|
|
41
|
+
})
|
|
42
|
+
},
|
|
43
|
+
(err) => {
|
|
44
|
+
assert.ok(err instanceof YotoAPIError, 'Should throw YotoAPIError')
|
|
45
|
+
assert.ok(err.statusCode === 401 || err.statusCode === 403, 'Should return 401 or 403 for invalid token')
|
|
46
|
+
assert.ok(err.body, 'Error should have body')
|
|
47
|
+
return true
|
|
48
|
+
}
|
|
49
|
+
)
|
|
50
|
+
})
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
test('getDeviceStatus', async (t) => {
|
|
54
|
+
/** @type {string[]} */
|
|
55
|
+
let testDeviceIds = []
|
|
56
|
+
/** @type {string | undefined} */
|
|
57
|
+
let onlineDeviceId
|
|
58
|
+
/** @type {string | undefined} */
|
|
59
|
+
let offlineDeviceId
|
|
60
|
+
|
|
61
|
+
// Get real device IDs to test with
|
|
62
|
+
await t.test('setup - get device IDs', async () => {
|
|
63
|
+
const response = await getDevices({
|
|
64
|
+
accessToken
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
assert.ok(response.devices.length > 0, 'User should have at least one device for getDeviceStatus tests')
|
|
68
|
+
|
|
69
|
+
// Try to find one online and one offline device
|
|
70
|
+
onlineDeviceId = response.devices.find(d => d.online)?.deviceId
|
|
71
|
+
offlineDeviceId = response.devices.find(d => !d.online)?.deviceId
|
|
72
|
+
|
|
73
|
+
testDeviceIds = response.devices.slice(0, 2).map(device => device.deviceId).filter(Boolean)
|
|
74
|
+
assert.ok(testDeviceIds.length > 0, 'Should have extracted at least one device ID')
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
await t.test('should fetch device status for valid device ID', async () => {
|
|
78
|
+
const deviceId = testDeviceIds[0]
|
|
79
|
+
assert.ok(deviceId, 'Device ID should exist')
|
|
80
|
+
|
|
81
|
+
const status = await getDeviceStatus({
|
|
82
|
+
accessToken,
|
|
83
|
+
deviceId
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
// Log response for type verification and documentation
|
|
87
|
+
logResponse('GET /device-v2/{deviceId}/status', status)
|
|
88
|
+
|
|
89
|
+
// Validate response structure matches YotoDeviceStatusResponse
|
|
90
|
+
assert.ok(status, 'Response should exist')
|
|
91
|
+
assert.strictEqual(status.deviceId, deviceId, 'Returned device ID should match requested ID')
|
|
92
|
+
assert.ok(typeof status.updatedAt === 'string', 'Status should have updatedAt string')
|
|
93
|
+
|
|
94
|
+
// Optional fields - only validate type if present
|
|
95
|
+
if (status.batteryLevelPercentage !== undefined) {
|
|
96
|
+
assert.ok(typeof status.batteryLevelPercentage === 'number', 'Battery level should be number')
|
|
97
|
+
}
|
|
98
|
+
if (status.isCharging !== undefined) {
|
|
99
|
+
assert.ok(typeof status.isCharging === 'boolean', 'isCharging should be boolean')
|
|
100
|
+
}
|
|
101
|
+
if (status.isOnline !== undefined) {
|
|
102
|
+
assert.ok(typeof status.isOnline === 'boolean', 'isOnline should be boolean')
|
|
103
|
+
}
|
|
104
|
+
if (status.userVolumePercentage !== undefined) {
|
|
105
|
+
assert.ok(typeof status.userVolumePercentage === 'number', 'User volume should be number')
|
|
106
|
+
}
|
|
107
|
+
if (status.systemVolumePercentage !== undefined) {
|
|
108
|
+
assert.ok(typeof status.systemVolumePercentage === 'number', 'System volume should be number')
|
|
109
|
+
}
|
|
110
|
+
if (status.temperatureCelcius !== undefined && status.temperatureCelcius !== null) {
|
|
111
|
+
assert.ok(
|
|
112
|
+
typeof status.temperatureCelcius === 'number' || typeof status.temperatureCelcius === 'string',
|
|
113
|
+
'Temperature should be number or string'
|
|
114
|
+
)
|
|
115
|
+
}
|
|
116
|
+
if (status.wifiStrength !== undefined) {
|
|
117
|
+
assert.ok(typeof status.wifiStrength === 'number', 'WiFi strength should be number')
|
|
118
|
+
}
|
|
119
|
+
if (status.cardInsertionState !== undefined) {
|
|
120
|
+
assert.ok([0, 1, 2].includes(status.cardInsertionState), 'Card insertion state should be 0, 1, or 2')
|
|
121
|
+
}
|
|
122
|
+
if (status.dayMode !== undefined) {
|
|
123
|
+
assert.ok([-1, 0, 1].includes(status.dayMode), 'Day mode should be -1, 0, or 1')
|
|
124
|
+
}
|
|
125
|
+
if (status.powerSource !== undefined) {
|
|
126
|
+
assert.ok([0, 1, 2, 3].includes(status.powerSource), 'Power source should be 0, 1, 2, or 3')
|
|
127
|
+
}
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
await t.test('should work with multiple device IDs', async () => {
|
|
131
|
+
// Test with multiple devices to ensure consistency
|
|
132
|
+
for (const deviceId of testDeviceIds) {
|
|
133
|
+
const status = await getDeviceStatus({
|
|
134
|
+
accessToken,
|
|
135
|
+
deviceId
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
assert.ok(status, 'Response should exist')
|
|
139
|
+
assert.strictEqual(status.deviceId, deviceId, 'Returned device ID should match requested ID')
|
|
140
|
+
}
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
await t.test('should fetch status for online device if available', async () => {
|
|
144
|
+
if (!onlineDeviceId) {
|
|
145
|
+
return // Skip if no online devices
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const status = await getDeviceStatus({
|
|
149
|
+
accessToken,
|
|
150
|
+
deviceId: onlineDeviceId
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
// Log online device status
|
|
154
|
+
logResponse('GET /device-v2/{deviceId}/status (ONLINE)', status)
|
|
155
|
+
|
|
156
|
+
assert.ok(status, 'Response should exist')
|
|
157
|
+
assert.strictEqual(status.deviceId, onlineDeviceId, 'Returned device ID should match')
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
await t.test('should fetch status for offline device if available', async () => {
|
|
161
|
+
if (!offlineDeviceId) {
|
|
162
|
+
return // Skip if no offline devices
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const status = await getDeviceStatus({
|
|
166
|
+
accessToken,
|
|
167
|
+
deviceId: offlineDeviceId
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
// Log offline device status
|
|
171
|
+
logResponse('GET /device-v2/{deviceId}/status (OFFLINE)', status)
|
|
172
|
+
|
|
173
|
+
assert.ok(status, 'Response should exist')
|
|
174
|
+
assert.strictEqual(status.deviceId, offlineDeviceId, 'Returned device ID should match')
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
await t.test('should fail with invalid device ID', async () => {
|
|
178
|
+
await assert.rejects(
|
|
179
|
+
async () => {
|
|
180
|
+
await getDeviceStatus({
|
|
181
|
+
accessToken,
|
|
182
|
+
deviceId: 'invalid-device-id-12345'
|
|
183
|
+
})
|
|
184
|
+
},
|
|
185
|
+
(err) => {
|
|
186
|
+
assert.ok(err instanceof YotoAPIError, 'Should throw YotoAPIError')
|
|
187
|
+
assert.ok(err.statusCode === 404, 'Should return 404 for invalid device ID')
|
|
188
|
+
assert.ok(err.body, 'Error should have body')
|
|
189
|
+
return true
|
|
190
|
+
}
|
|
191
|
+
)
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
await t.test('should fail with invalid token', async () => {
|
|
195
|
+
const deviceId = testDeviceIds[0]
|
|
196
|
+
assert.ok(deviceId, 'Device ID should exist')
|
|
197
|
+
|
|
198
|
+
await assert.rejects(
|
|
199
|
+
async () => {
|
|
200
|
+
await getDeviceStatus({
|
|
201
|
+
accessToken: 'invalid-token',
|
|
202
|
+
deviceId
|
|
203
|
+
})
|
|
204
|
+
},
|
|
205
|
+
(err) => {
|
|
206
|
+
assert.ok(err instanceof YotoAPIError, 'Should throw YotoAPIError')
|
|
207
|
+
assert.ok(err.statusCode === 401 || err.statusCode === 403, 'Should return 401 or 403 for invalid token')
|
|
208
|
+
assert.ok(err.body, 'Error should have body')
|
|
209
|
+
return true
|
|
210
|
+
}
|
|
211
|
+
)
|
|
212
|
+
})
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
test('getDeviceConfig', async (t) => {
|
|
216
|
+
/** @type {string[]} */
|
|
217
|
+
let testDeviceIds = []
|
|
218
|
+
/** @type {Map<string, string>} */
|
|
219
|
+
const deviceTypeMap = new Map()
|
|
220
|
+
/** @type {string | undefined} */
|
|
221
|
+
let onlineDeviceId
|
|
222
|
+
/** @type {string | undefined} */
|
|
223
|
+
let deviceWithAlarmsId
|
|
224
|
+
|
|
225
|
+
// Get real device IDs to test with
|
|
226
|
+
await t.test('setup - get device IDs', async () => {
|
|
227
|
+
const response = await getDevices({
|
|
228
|
+
accessToken
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
assert.ok(response.devices.length > 0, 'User should have at least one device for getDeviceConfig tests')
|
|
232
|
+
|
|
233
|
+
// Try to get different device types for testing
|
|
234
|
+
for (const device of response.devices) {
|
|
235
|
+
if (!deviceTypeMap.has(device.deviceType)) {
|
|
236
|
+
deviceTypeMap.set(device.deviceType, device.deviceId)
|
|
237
|
+
}
|
|
238
|
+
// Also track an online device
|
|
239
|
+
if (device.online && !onlineDeviceId) {
|
|
240
|
+
onlineDeviceId = device.deviceId
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
testDeviceIds = response.devices.slice(0, 2).map(device => device.deviceId).filter(Boolean)
|
|
245
|
+
assert.ok(testDeviceIds.length > 0, 'Should have extracted at least one device ID')
|
|
246
|
+
|
|
247
|
+
console.log(`\nTesting device types: ${Array.from(deviceTypeMap.keys()).join(', ')}`)
|
|
248
|
+
console.log(`Online device for testing: ${onlineDeviceId || 'none'}`)
|
|
249
|
+
|
|
250
|
+
// Try to find a device with alarms configured
|
|
251
|
+
for (const deviceId of testDeviceIds) {
|
|
252
|
+
try {
|
|
253
|
+
const config = await getDeviceConfig({
|
|
254
|
+
accessToken,
|
|
255
|
+
deviceId
|
|
256
|
+
})
|
|
257
|
+
if (config.device.config.alarms && config.device.config.alarms.length > 0) {
|
|
258
|
+
deviceWithAlarmsId = deviceId
|
|
259
|
+
console.log(`Device with alarms found: ${deviceId}`)
|
|
260
|
+
break
|
|
261
|
+
}
|
|
262
|
+
} catch (err) {
|
|
263
|
+
// Skip devices that error
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
if (!deviceWithAlarmsId) {
|
|
267
|
+
console.log('No devices with alarms found')
|
|
268
|
+
}
|
|
269
|
+
})
|
|
270
|
+
|
|
271
|
+
await t.test('should fetch device config for valid device ID', async () => {
|
|
272
|
+
const deviceId = testDeviceIds[0]
|
|
273
|
+
assert.ok(deviceId, 'Device ID should exist')
|
|
274
|
+
|
|
275
|
+
const config = await getDeviceConfig({
|
|
276
|
+
accessToken,
|
|
277
|
+
deviceId
|
|
278
|
+
})
|
|
279
|
+
|
|
280
|
+
// Log response for type verification and documentation
|
|
281
|
+
logResponse('GET /device-v2/{deviceId}/config', config)
|
|
282
|
+
|
|
283
|
+
// Validate response structure matches YotoDeviceConfigResponse
|
|
284
|
+
assert.ok(config, 'Response should exist')
|
|
285
|
+
assert.ok(config.device, 'Response should have device property')
|
|
286
|
+
assert.strictEqual(config.device.deviceId, deviceId, 'Returned device ID should match requested ID')
|
|
287
|
+
|
|
288
|
+
// Validate device structure
|
|
289
|
+
assert.ok(typeof config.device.deviceFamily === 'string', 'Device should have deviceFamily string')
|
|
290
|
+
assert.ok(typeof config.device.deviceType === 'string', 'Device should have deviceType string')
|
|
291
|
+
assert.ok(typeof config.device.online === 'boolean', 'Device should have online boolean')
|
|
292
|
+
|
|
293
|
+
// Validate config object exists
|
|
294
|
+
assert.ok(config.device.config, 'Device should have config object')
|
|
295
|
+
assert.ok(typeof config.device.config === 'object', 'Config should be an object')
|
|
296
|
+
|
|
297
|
+
// Validate some config fields
|
|
298
|
+
if (config.device.config.ambientColour !== undefined) {
|
|
299
|
+
assert.ok(typeof config.device.config.ambientColour === 'string', 'ambientColour should be string')
|
|
300
|
+
}
|
|
301
|
+
if (config.device.config.clockFace !== undefined) {
|
|
302
|
+
assert.ok(typeof config.device.config.clockFace === 'string', 'clockFace should be string')
|
|
303
|
+
}
|
|
304
|
+
if (config.device.config.repeatAll !== undefined) {
|
|
305
|
+
assert.ok(typeof config.device.config.repeatAll === 'boolean', 'repeatAll should be boolean')
|
|
306
|
+
}
|
|
307
|
+
if (config.device.config.volumeLevel !== undefined) {
|
|
308
|
+
assert.ok(typeof config.device.config.volumeLevel === 'string', 'volumeLevel should be string')
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Validate shortcuts if present (beta feature)
|
|
312
|
+
if (config.device.shortcuts) {
|
|
313
|
+
assert.ok(typeof config.device.shortcuts === 'object', 'Shortcuts should be an object')
|
|
314
|
+
if (config.device.shortcuts.versionId) {
|
|
315
|
+
assert.ok(typeof config.device.shortcuts.versionId === 'string', 'versionId should be string')
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
})
|
|
319
|
+
|
|
320
|
+
await t.test('should work with multiple device IDs and types', async () => {
|
|
321
|
+
// Test with different device types to ensure config properties vary appropriately
|
|
322
|
+
for (const [deviceType, deviceId] of deviceTypeMap) {
|
|
323
|
+
const config = await getDeviceConfig({
|
|
324
|
+
accessToken,
|
|
325
|
+
deviceId
|
|
326
|
+
})
|
|
327
|
+
|
|
328
|
+
console.log(`\nDevice type: ${deviceType}, Device ID: ${deviceId}`)
|
|
329
|
+
console.log(` Config keys: ${Object.keys(config.device.config).join(', ')}`)
|
|
330
|
+
|
|
331
|
+
assert.ok(config, 'Response should exist')
|
|
332
|
+
assert.ok(config.device, 'Response should have device property')
|
|
333
|
+
assert.strictEqual(config.device.deviceId, deviceId, 'Returned device ID should match requested ID')
|
|
334
|
+
assert.strictEqual(config.device.deviceType, deviceType, 'Returned device type should match')
|
|
335
|
+
|
|
336
|
+
// Config should always be present but properties may vary by device type
|
|
337
|
+
assert.ok(config.device.config, 'Config should exist for all device types')
|
|
338
|
+
assert.ok(typeof config.device.config === 'object', 'Config should be object')
|
|
339
|
+
}
|
|
340
|
+
})
|
|
341
|
+
|
|
342
|
+
await t.test('should log config for each device type', async () => {
|
|
343
|
+
// Log configurations for different device types to see variations
|
|
344
|
+
for (const [deviceType, deviceId] of deviceTypeMap) {
|
|
345
|
+
const config = await getDeviceConfig({
|
|
346
|
+
accessToken,
|
|
347
|
+
deviceId
|
|
348
|
+
})
|
|
349
|
+
|
|
350
|
+
logResponse(`GET /device-v2/{deviceId}/config (${deviceType})`, config)
|
|
351
|
+
}
|
|
352
|
+
})
|
|
353
|
+
|
|
354
|
+
await t.test('should fetch config for online device if available', async () => {
|
|
355
|
+
if (!onlineDeviceId) {
|
|
356
|
+
console.log('Skipping online device config test - no online devices found')
|
|
357
|
+
return // Skip if no online devices
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const config = await getDeviceConfig({
|
|
361
|
+
accessToken,
|
|
362
|
+
deviceId: onlineDeviceId
|
|
363
|
+
})
|
|
364
|
+
|
|
365
|
+
// Log online device config
|
|
366
|
+
logResponse('GET /device-v2/{deviceId}/config (ONLINE)', config)
|
|
367
|
+
|
|
368
|
+
assert.ok(config, 'Response should exist')
|
|
369
|
+
assert.ok(config.device, 'Response should have device property')
|
|
370
|
+
assert.strictEqual(config.device.online, true, 'Device should be online')
|
|
371
|
+
assert.strictEqual(config.device.deviceId, onlineDeviceId, 'Device ID should match')
|
|
372
|
+
})
|
|
373
|
+
|
|
374
|
+
await t.test('should fetch config with alarms if available', async () => {
|
|
375
|
+
if (!deviceWithAlarmsId) {
|
|
376
|
+
console.log('Skipping alarm test - no devices with alarms found')
|
|
377
|
+
return // Skip if no devices with alarms
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const config = await getDeviceConfig({
|
|
381
|
+
accessToken,
|
|
382
|
+
deviceId: deviceWithAlarmsId
|
|
383
|
+
})
|
|
384
|
+
|
|
385
|
+
// Log device with alarms
|
|
386
|
+
logResponse('GET /device-v2/{deviceId}/config (WITH ALARMS)', config)
|
|
387
|
+
|
|
388
|
+
assert.ok(config, 'Response should exist')
|
|
389
|
+
assert.ok(config.device, 'Response should have device property')
|
|
390
|
+
assert.ok(config.device.config, 'Config should exist')
|
|
391
|
+
assert.ok(Array.isArray(config.device.config.alarms), 'Alarms should be an array')
|
|
392
|
+
assert.ok(config.device.config.alarms.length > 0, 'Should have at least one alarm')
|
|
393
|
+
|
|
394
|
+
// Validate alarm structure
|
|
395
|
+
const alarm = config.device.config.alarms[0]
|
|
396
|
+
console.log(`\nAlarm structure: ${JSON.stringify(alarm, null, 2)}`)
|
|
397
|
+
})
|
|
398
|
+
|
|
399
|
+
// TODO: Add tests for updateDeviceConfig (mutation endpoint)
|
|
400
|
+
// - should update device config successfully
|
|
401
|
+
// - should return status 'ok' on successful update
|
|
402
|
+
// - should update device name
|
|
403
|
+
// - should update individual config properties (locale, bluetoothEnabled, etc.)
|
|
404
|
+
// - should update day/night mode settings
|
|
405
|
+
// - should update volume limits
|
|
406
|
+
// - should update display brightness settings
|
|
407
|
+
// - should update alarms array
|
|
408
|
+
// - should validate response structure matches YotoUpdateDeviceConfigResponse
|
|
409
|
+
// - should fail with invalid device ID
|
|
410
|
+
// - should fail with invalid token
|
|
411
|
+
// - should fail with invalid config values
|
|
412
|
+
|
|
413
|
+
// TODO: Add tests for updateDeviceShortcuts (mutation endpoint - beta)
|
|
414
|
+
// - should update shortcuts successfully
|
|
415
|
+
// - should return status 'ok' on successful update
|
|
416
|
+
// - should update day mode shortcuts
|
|
417
|
+
// - should update night mode shortcuts
|
|
418
|
+
// - should update shortcuts with track-play commands
|
|
419
|
+
// - should set card/chapter/track parameters correctly
|
|
420
|
+
// - should handle Yoto Daily dynamic tracks (<yyyymmdd>)
|
|
421
|
+
// - should allow empty content arrays
|
|
422
|
+
// - should validate response structure matches YotoUpdateShortcutsResponse
|
|
423
|
+
// - should fail with invalid device ID
|
|
424
|
+
// - should fail with invalid token
|
|
425
|
+
// - should fail with invalid shortcuts structure
|
|
426
|
+
|
|
427
|
+
// TODO: Add tests for sendDeviceCommand (MQTT mutation endpoint)
|
|
428
|
+
// - should send volume/set command successfully
|
|
429
|
+
// - should send ambients/set command with RGB values
|
|
430
|
+
// - should send sleep-timer/set command
|
|
431
|
+
// - should send card/start command with URI
|
|
432
|
+
// - should send card/stop command
|
|
433
|
+
// - should send card/pause command
|
|
434
|
+
// - should send card/resume command
|
|
435
|
+
// - should send bluetooth/on command
|
|
436
|
+
// - should send bluetooth/off command
|
|
437
|
+
// - should send reboot command
|
|
438
|
+
// - should send status/request command
|
|
439
|
+
// - should send events/request command
|
|
440
|
+
// - should send display/preview command
|
|
441
|
+
// - should return status 'ok' on successful command
|
|
442
|
+
// - should validate response structure matches YotoDeviceCommandResponse
|
|
443
|
+
// - should fail with invalid device ID
|
|
444
|
+
// - should fail with invalid token
|
|
445
|
+
// - should fail with invalid command payload
|
|
446
|
+
|
|
447
|
+
await t.test('should fail with invalid device ID', async () => {
|
|
448
|
+
await assert.rejects(
|
|
449
|
+
async () => {
|
|
450
|
+
await getDeviceConfig({
|
|
451
|
+
accessToken,
|
|
452
|
+
deviceId: 'invalid-device-id-12345'
|
|
453
|
+
})
|
|
454
|
+
},
|
|
455
|
+
(err) => {
|
|
456
|
+
assert.ok(err instanceof YotoAPIError, 'Should throw YotoAPIError')
|
|
457
|
+
assert.ok(err.statusCode >= 400, 'Should return error status code for invalid device ID')
|
|
458
|
+
assert.ok(err.body, 'Error should have body')
|
|
459
|
+
return true
|
|
460
|
+
}
|
|
461
|
+
)
|
|
462
|
+
})
|
|
463
|
+
|
|
464
|
+
await t.test('should fail with invalid token', async () => {
|
|
465
|
+
const deviceId = testDeviceIds[0]
|
|
466
|
+
assert.ok(deviceId, 'Device ID should exist')
|
|
467
|
+
|
|
468
|
+
await assert.rejects(
|
|
469
|
+
async () => {
|
|
470
|
+
await getDeviceConfig({
|
|
471
|
+
accessToken: 'invalid-token',
|
|
472
|
+
deviceId
|
|
473
|
+
})
|
|
474
|
+
},
|
|
475
|
+
(err) => {
|
|
476
|
+
assert.ok(err instanceof YotoAPIError, 'Should throw YotoAPIError')
|
|
477
|
+
assert.ok(err.statusCode === 401 || err.statusCode === 403, 'Should return 401 or 403 for invalid token')
|
|
478
|
+
assert.ok(err.body, 'Error should have body')
|
|
479
|
+
return true
|
|
480
|
+
}
|
|
481
|
+
)
|
|
482
|
+
})
|
|
483
|
+
})
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
export function getGroups({ accessToken, userAgent, requestOptions }: {
|
|
2
|
+
accessToken: string;
|
|
3
|
+
userAgent?: string | undefined;
|
|
4
|
+
requestOptions?: ({
|
|
5
|
+
dispatcher?: import("undici").Dispatcher;
|
|
6
|
+
} & Omit<import("undici").Dispatcher.RequestOptions<unknown>, "origin" | "path" | "method"> & Partial<Pick<import("undici").Dispatcher.RequestOptions<null>, "method">>) | undefined;
|
|
7
|
+
}): Promise<YotoGroup[]>;
|
|
8
|
+
export function createGroup({ token, userAgent, group, requestOptions }: {
|
|
9
|
+
token: string;
|
|
10
|
+
group: YotoCreateGroupRequest;
|
|
11
|
+
userAgent?: string | undefined;
|
|
12
|
+
requestOptions?: ({
|
|
13
|
+
dispatcher?: import("undici").Dispatcher;
|
|
14
|
+
} & Omit<import("undici").Dispatcher.RequestOptions<unknown>, "origin" | "path" | "method"> & Partial<Pick<import("undici").Dispatcher.RequestOptions<null>, "method">>) | undefined;
|
|
15
|
+
}): Promise<YotoGroup>;
|
|
16
|
+
export function getGroup({ accessToken, userAgent, groupId, requestOptions }: {
|
|
17
|
+
accessToken: string;
|
|
18
|
+
groupId: string;
|
|
19
|
+
userAgent?: string | undefined;
|
|
20
|
+
requestOptions?: ({
|
|
21
|
+
dispatcher?: import("undici").Dispatcher;
|
|
22
|
+
} & Omit<import("undici").Dispatcher.RequestOptions<unknown>, "origin" | "path" | "method"> & Partial<Pick<import("undici").Dispatcher.RequestOptions<null>, "method">>) | undefined;
|
|
23
|
+
}): Promise<YotoGroup>;
|
|
24
|
+
export function updateGroup({ accessToken, userAgent, groupId, group, requestOptions }: {
|
|
25
|
+
accessToken: string;
|
|
26
|
+
groupId: string;
|
|
27
|
+
group: YotoUpdateGroupRequest;
|
|
28
|
+
userAgent?: string | undefined;
|
|
29
|
+
requestOptions?: ({
|
|
30
|
+
dispatcher?: import("undici").Dispatcher;
|
|
31
|
+
} & Omit<import("undici").Dispatcher.RequestOptions<unknown>, "origin" | "path" | "method"> & Partial<Pick<import("undici").Dispatcher.RequestOptions<null>, "method">>) | undefined;
|
|
32
|
+
}): Promise<YotoGroup>;
|
|
33
|
+
export function deleteGroup({ accessToken, userAgent, groupId, requestOptions }: {
|
|
34
|
+
accessToken: string;
|
|
35
|
+
groupId: string;
|
|
36
|
+
userAgent?: string | undefined;
|
|
37
|
+
requestOptions?: ({
|
|
38
|
+
dispatcher?: import("undici").Dispatcher;
|
|
39
|
+
} & Omit<import("undici").Dispatcher.RequestOptions<unknown>, "origin" | "path" | "method"> & Partial<Pick<import("undici").Dispatcher.RequestOptions<null>, "method">>) | undefined;
|
|
40
|
+
}): Promise<YotoDeleteGroupResponse>;
|
|
41
|
+
export type YotoGroupsResponse = {
|
|
42
|
+
groups: YotoGroup[];
|
|
43
|
+
};
|
|
44
|
+
export type YotoGroup = {
|
|
45
|
+
id: string;
|
|
46
|
+
name: string;
|
|
47
|
+
familyId: string;
|
|
48
|
+
imageId: string;
|
|
49
|
+
imageUrl: string;
|
|
50
|
+
items: YotoGroupItem[];
|
|
51
|
+
cards: any[];
|
|
52
|
+
createdAt: string;
|
|
53
|
+
lastModifiedAt: string;
|
|
54
|
+
};
|
|
55
|
+
export type YotoGroupItem = {
|
|
56
|
+
contentId: string;
|
|
57
|
+
addedAt: string;
|
|
58
|
+
};
|
|
59
|
+
export type YotoCreateGroupRequest = {
|
|
60
|
+
name: string;
|
|
61
|
+
imageId: string;
|
|
62
|
+
items: YotoGroupItemInput[];
|
|
63
|
+
};
|
|
64
|
+
export type YotoGroupItemInput = {
|
|
65
|
+
contentId: string;
|
|
66
|
+
};
|
|
67
|
+
export type YotoUpdateGroupRequest = {
|
|
68
|
+
name: string;
|
|
69
|
+
imageId: string;
|
|
70
|
+
items: YotoGroupItemInput[];
|
|
71
|
+
};
|
|
72
|
+
export type YotoDeleteGroupResponse = {
|
|
73
|
+
id: string;
|
|
74
|
+
};
|
|
75
|
+
//# sourceMappingURL=family-library-groups.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"family-library-groups.d.ts","sourceRoot":"","sources":["family-library-groups.js"],"names":[],"mappings":"AA2DA,sEAhBG;IAAyB,WAAW,EAA3B,MAAM;IACW,SAAS;IACD,cAAc;;;CAChD,GAAS,OAAO,CAAC,SAAS,EAAE,CAAC,CA6B/B;AAwCD,yEAnBG;IAAyB,KAAK,EAArB,MAAM;IAC0B,KAAK,EAArC,sBAAsB;IACL,SAAS;IACD,cAAc;;;CAChD,GAAS,OAAO,CAAC,SAAS,CAAC,CAoC7B;AAaD,8EANG;IAAyB,WAAW,EAA3B,MAAM;IACU,OAAO,EAAvB,MAAM;IACW,SAAS;IACD,cAAc;;;CAChD,GAAS,OAAO,CAAC,SAAS,CAAC,CAmB7B;AAsBD,wFAPG;IAAyB,WAAW,EAA3B,MAAM;IACU,OAAO,EAAvB,MAAM;IAC0B,KAAK,EAArC,sBAAsB;IACL,SAAS;IACD,cAAc;;;CAChD,GAAS,OAAO,CAAC,SAAS,CAAC,CAwB7B;AAmBD,iFANG;IAAyB,WAAW,EAA3B,MAAM;IACU,OAAO,EAAvB,MAAM;IACW,SAAS;IACD,cAAc;;;CAChD,GAAS,OAAO,CAAC,uBAAuB,CAAC,CAmB3C;;YAxOa,SAAS,EAAE;;;QAMX,MAAM;UACN,MAAM;cACN,MAAM;aACN,MAAM;cACN,MAAM;WACN,aAAa,EAAE;WACf,GAAG,EAAE;eACL,MAAM;oBACN,MAAM;;;eAMN,MAAM;aACN,MAAM;;;UA6CN,MAAM;aACN,MAAM;WACN,kBAAkB,EAAE;;;eAMpB,MAAM;;;UAmFN,MAAM;aACN,MAAM;WACN,kBAAkB,EAAE;;;QA0CpB,MAAM"}
|