yoto-nodejs-client 0.0.6 → 0.0.8
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 +52 -36
- package/bin/content.js +0 -0
- package/bin/devices.js +0 -0
- package/bin/groups.js +0 -0
- package/bin/icons.js +0 -0
- package/bin/refresh-token.js +0 -0
- package/lib/test-helpers/device-model-test-helpers.d.ts +29 -0
- package/lib/test-helpers/device-model-test-helpers.d.ts.map +1 -0
- package/lib/test-helpers/device-model-test-helpers.js +116 -0
- package/lib/yoto-account.d.ts +339 -9
- package/lib/yoto-account.d.ts.map +1 -1
- package/lib/yoto-account.js +411 -39
- package/lib/yoto-account.test.js +139 -0
- package/lib/yoto-device.d.ts +62 -18
- package/lib/yoto-device.d.ts.map +1 -1
- package/lib/yoto-device.js +62 -19
- package/lib/yoto-device.test.js +6 -111
- package/package.json +14 -12
package/README.md
CHANGED
|
@@ -211,24 +211,21 @@ account.on('started', (metadata) => {
|
|
|
211
211
|
|
|
212
212
|
|
|
213
213
|
|
|
214
|
-
// Listen for device
|
|
215
|
-
account.on('
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
console.log(`${deviceId} battery: ${status.batteryLevelPercentage}%`)
|
|
219
|
-
})
|
|
214
|
+
// Listen for device events across all devices via the unified bus
|
|
215
|
+
account.on('statusUpdate', ({ deviceId, status, source }) => {
|
|
216
|
+
console.log(`${deviceId} battery: ${status.batteryLevelPercentage}% (${source})`)
|
|
217
|
+
})
|
|
220
218
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
219
|
+
account.on('online', ({ deviceId }) => {
|
|
220
|
+
console.log(`${deviceId} came online`)
|
|
221
|
+
})
|
|
224
222
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
})
|
|
223
|
+
account.on('offline', ({ deviceId }) => {
|
|
224
|
+
console.log(`${deviceId} went offline`)
|
|
228
225
|
})
|
|
229
226
|
|
|
230
227
|
// Unified error handling
|
|
231
|
-
account.on('error', (error, context) => {
|
|
228
|
+
account.on('error', ({ error, context }) => {
|
|
232
229
|
console.error(`Error in ${context.source}:`, error.message)
|
|
233
230
|
if (context.deviceId) {
|
|
234
231
|
console.error(`Device: ${context.deviceId}`)
|
|
@@ -961,6 +958,7 @@ Create a stateful device client that manages device state primarily from MQTT wi
|
|
|
961
958
|
- `await deviceClient.refreshConfig()` - Refresh config from HTTP API
|
|
962
959
|
- `await deviceClient.updateConfig(configUpdate)` - Update device configuration
|
|
963
960
|
- `await deviceClient.sendCommand(command)` - Send device command via HTTP
|
|
961
|
+
- `await deviceClient.startCard({ cardId, [chapterKey], [trackKey] })` - Start playing a card
|
|
964
962
|
|
|
965
963
|
**Events:**
|
|
966
964
|
- `started(metadata)` - Device client started, passes metadata object with device, config, shortcuts, status, playback, initialized, running
|
|
@@ -970,10 +968,17 @@ Create a stateful device client that manages device state primarily from MQTT wi
|
|
|
970
968
|
- `playbackUpdate(playback, changedFields)` - Playback state changed, passes (playback, changedFields)
|
|
971
969
|
- `online(metadata)` - Device came online, passes metadata with reason and optional upTime
|
|
972
970
|
- `offline(metadata)` - Device went offline, passes metadata with reason and optional shutDownReason or timeSinceLastSeen
|
|
973
|
-
- `mqttConnect()` - MQTT client connected
|
|
971
|
+
- `mqttConnect(metadata)` - MQTT client connected, passes CONNACK metadata
|
|
974
972
|
- `mqttDisconnect(metadata)` - MQTT disconnect packet received, passes metadata with disconnect packet
|
|
975
973
|
- `mqttClose(metadata)` - MQTT connection closed, passes metadata with close reason
|
|
976
974
|
- `mqttReconnect()` - MQTT client is reconnecting
|
|
975
|
+
- `mqttOffline()` - MQTT client goes offline
|
|
976
|
+
- `mqttEnd()` - MQTT client end is called
|
|
977
|
+
- `mqttStatus(topic, message)` - Raw MQTT status messages (documented status topic)
|
|
978
|
+
- `mqttEvents(topic, message)` - Raw MQTT events messages
|
|
979
|
+
- `mqttStatusLegacy(topic, message)` - Raw legacy MQTT status messages (undocumented status topic)
|
|
980
|
+
- `mqttResponse(topic, message)` - Raw MQTT response messages
|
|
981
|
+
- `mqttUnknown(topic, message)` - Raw MQTT messages that do not match known types
|
|
977
982
|
- `error(error)` - Error occurred, passes error
|
|
978
983
|
|
|
979
984
|
**Static Properties & Methods:**
|
|
@@ -1051,11 +1056,27 @@ Create an account manager that automatically discovers and manages all devices f
|
|
|
1051
1056
|
**Events:**
|
|
1052
1057
|
- `started(metadata)` - Account started (metadata: { deviceCount, devices })
|
|
1053
1058
|
- `stopped()` - Account stopped
|
|
1054
|
-
- `deviceAdded(deviceId
|
|
1055
|
-
- `deviceRemoved(deviceId)` - Device was removed
|
|
1056
|
-
- `
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
+
- `deviceAdded({ deviceId })` - Device was added
|
|
1060
|
+
- `deviceRemoved({ deviceId })` - Device was removed
|
|
1061
|
+
- `statusUpdate({ deviceId, status, source, changedFields })` - Re-emitted device status update
|
|
1062
|
+
- `configUpdate({ deviceId, config, changedFields })` - Re-emitted config update
|
|
1063
|
+
- `playbackUpdate({ deviceId, playback, changedFields })` - Re-emitted playback update
|
|
1064
|
+
- `online({ deviceId, metadata })` - Re-emitted online event
|
|
1065
|
+
- `offline({ deviceId, metadata })` - Re-emitted offline event
|
|
1066
|
+
- `mqttConnect({ deviceId, metadata })` - Re-emitted MQTT connect
|
|
1067
|
+
- `mqttDisconnect({ deviceId, metadata })` - Re-emitted MQTT disconnect
|
|
1068
|
+
- `mqttClose({ deviceId, metadata })` - Re-emitted MQTT close
|
|
1069
|
+
- `mqttReconnect({ deviceId })` - Re-emitted MQTT reconnect
|
|
1070
|
+
- `mqttOffline({ deviceId })` - Re-emitted MQTT offline
|
|
1071
|
+
- `mqttEnd({ deviceId })` - Re-emitted MQTT end
|
|
1072
|
+
- `mqttStatus({ deviceId, topic, message })` - Re-emitted raw MQTT status
|
|
1073
|
+
- `mqttEvents({ deviceId, topic, message })` - Re-emitted raw MQTT events
|
|
1074
|
+
- `mqttStatusLegacy({ deviceId, topic, message })` - Re-emitted raw MQTT legacy status
|
|
1075
|
+
- `mqttResponse({ deviceId, topic, message })` - Re-emitted raw MQTT response
|
|
1076
|
+
- `mqttUnknown({ deviceId, topic, message })` - Re-emitted raw MQTT unknown message
|
|
1077
|
+
- `error({ error, context })` - Error occurred (context: { source, deviceId, operation })
|
|
1078
|
+
|
|
1079
|
+
**Note:** You can still listen to individual device events by attaching listeners to each `YotoDeviceModel`, but the account now re-emits device and MQTT events with device context for unified handling.
|
|
1059
1080
|
|
|
1060
1081
|
```js
|
|
1061
1082
|
import { YotoAccount } from 'yoto-nodejs-client'
|
|
@@ -1075,26 +1096,21 @@ const account = new YotoAccount({
|
|
|
1075
1096
|
})
|
|
1076
1097
|
|
|
1077
1098
|
// Account-level error handling
|
|
1078
|
-
account.on('error', (error, context) => {
|
|
1099
|
+
account.on('error', ({ error, context }) => {
|
|
1079
1100
|
console.error(`Error in ${context.source}:`, error.message)
|
|
1080
1101
|
})
|
|
1081
1102
|
|
|
1082
|
-
//
|
|
1083
|
-
account.on('
|
|
1084
|
-
console.log(
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
})
|
|
1094
|
-
|
|
1095
|
-
deviceModel.on('offline', (metadata) => {
|
|
1096
|
-
console.log(`${deviceId} went offline (${metadata.reason})`)
|
|
1097
|
-
})
|
|
1103
|
+
// Unified device events across all devices
|
|
1104
|
+
account.on('statusUpdate', ({ deviceId, status, source }) => {
|
|
1105
|
+
console.log(`${deviceId} battery: ${status.batteryLevelPercentage}% (${source})`)
|
|
1106
|
+
})
|
|
1107
|
+
|
|
1108
|
+
account.on('online', ({ deviceId, metadata }) => {
|
|
1109
|
+
console.log(`${deviceId} came online (${metadata.reason})`)
|
|
1110
|
+
})
|
|
1111
|
+
|
|
1112
|
+
account.on('offline', ({ deviceId, metadata }) => {
|
|
1113
|
+
console.log(`${deviceId} went offline (${metadata.reason})`)
|
|
1098
1114
|
})
|
|
1099
1115
|
|
|
1100
1116
|
await account.start()
|
package/bin/content.js
CHANGED
|
File without changes
|
package/bin/devices.js
CHANGED
|
File without changes
|
package/bin/groups.js
CHANGED
|
File without changes
|
package/bin/icons.js
CHANGED
|
File without changes
|
package/bin/refresh-token.js
CHANGED
|
File without changes
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @param {Promise<unknown>} promise
|
|
3
|
+
* @param {number} timeoutMs
|
|
4
|
+
* @param {string} label
|
|
5
|
+
* @returns {Promise<unknown>}
|
|
6
|
+
*/
|
|
7
|
+
export function withTimeout(promise: Promise<unknown>, timeoutMs: number, label: string): Promise<unknown>;
|
|
8
|
+
/**
|
|
9
|
+
* @param {YotoDeviceModel} model
|
|
10
|
+
* @returns {Promise<void>}
|
|
11
|
+
*/
|
|
12
|
+
export function waitForModelReady(model: YotoDeviceModel): Promise<void>;
|
|
13
|
+
/**
|
|
14
|
+
* @param {YotoDeviceModelConfig} config
|
|
15
|
+
*/
|
|
16
|
+
export function assertConfigShape(config: YotoDeviceModelConfig): void;
|
|
17
|
+
/**
|
|
18
|
+
* @param {YotoPlaybackState} playback
|
|
19
|
+
*/
|
|
20
|
+
export function assertPlaybackShape(playback: YotoPlaybackState): void;
|
|
21
|
+
/**
|
|
22
|
+
* @param {string | undefined} value
|
|
23
|
+
* @returns {string}
|
|
24
|
+
*/
|
|
25
|
+
export function toLower(value: string | undefined): string;
|
|
26
|
+
import type { YotoDeviceModel } from '../yoto-device.js';
|
|
27
|
+
import type { YotoDeviceModelConfig } from '../yoto-device.js';
|
|
28
|
+
import type { YotoPlaybackState } from '../yoto-device.js';
|
|
29
|
+
//# sourceMappingURL=device-model-test-helpers.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"device-model-test-helpers.d.ts","sourceRoot":"","sources":["device-model-test-helpers.js"],"names":[],"mappings":"AAMA;;;;;GAKG;AACH,qCALW,OAAO,CAAC,OAAO,CAAC,aAChB,MAAM,SACN,MAAM,GACJ,OAAO,CAAC,OAAO,CAAC,CAS5B;AAED;;;GAGG;AACH,yCAHW,eAAe,GACb,OAAO,CAAC,IAAI,CAAC,CAUzB;AAED;;GAEG;AACH,0CAFW,qBAAqB,QAkD/B;AAED;;GAEG;AACH,8CAFW,iBAAiB,QAkB3B;AAED;;;GAGG;AACH,+BAHW,MAAM,GAAG,SAAS,GAChB,MAAM,CAIlB;qCAnH8E,mBAAmB;2CAAnB,mBAAmB;uCAAnB,mBAAmB"}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/** @import { YotoDeviceModel, YotoDeviceModelConfig, YotoPlaybackState } from '../yoto-device.js' */
|
|
2
|
+
|
|
3
|
+
import assert from 'node:assert/strict'
|
|
4
|
+
import { once } from 'node:events'
|
|
5
|
+
import { setTimeout as sleep } from 'node:timers/promises'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @param {Promise<unknown>} promise
|
|
9
|
+
* @param {number} timeoutMs
|
|
10
|
+
* @param {string} label
|
|
11
|
+
* @returns {Promise<unknown>}
|
|
12
|
+
*/
|
|
13
|
+
export function withTimeout (promise, timeoutMs, label) {
|
|
14
|
+
return Promise.race([
|
|
15
|
+
promise,
|
|
16
|
+
sleep(timeoutMs).then(() => {
|
|
17
|
+
throw new Error(`${label} timed out after ${timeoutMs}ms`)
|
|
18
|
+
})
|
|
19
|
+
])
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @param {YotoDeviceModel} model
|
|
24
|
+
* @returns {Promise<void>}
|
|
25
|
+
*/
|
|
26
|
+
export async function waitForModelReady (model) {
|
|
27
|
+
const started = withTimeout(once(model, 'started'), 15000, 'started')
|
|
28
|
+
const statusUpdated = withTimeout(once(model, 'statusUpdate'), 15000, 'statusUpdate')
|
|
29
|
+
const configUpdated = withTimeout(once(model, 'configUpdate'), 15000, 'configUpdate')
|
|
30
|
+
|
|
31
|
+
await model.start()
|
|
32
|
+
await Promise.all([started, statusUpdated, configUpdated])
|
|
33
|
+
await sleep(1500)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* @param {YotoDeviceModelConfig} config
|
|
38
|
+
*/
|
|
39
|
+
export function assertConfigShape (config) {
|
|
40
|
+
assert.ok(Array.isArray(config.alarms), 'alarms should be an array')
|
|
41
|
+
assert.equal(typeof config.ambientColour, 'string', 'ambientColour should be string')
|
|
42
|
+
assert.equal(typeof config.bluetoothEnabled, 'boolean', 'bluetoothEnabled should be boolean')
|
|
43
|
+
assert.equal(typeof config.btHeadphonesEnabled, 'boolean', 'btHeadphonesEnabled should be boolean')
|
|
44
|
+
assert.equal(typeof config.clockFace, 'string', 'clockFace should be string')
|
|
45
|
+
assert.equal(typeof config.dayDisplayBrightnessAuto, 'boolean', 'dayDisplayBrightnessAuto should be boolean')
|
|
46
|
+
if (config.dayDisplayBrightnessAuto) {
|
|
47
|
+
assert.equal(config.dayDisplayBrightness, null, 'dayDisplayBrightness should be null when auto')
|
|
48
|
+
} else {
|
|
49
|
+
assert.equal(typeof config.dayDisplayBrightness, 'number', 'dayDisplayBrightness should be number when not auto')
|
|
50
|
+
}
|
|
51
|
+
assert.equal(typeof config.dayTime, 'string', 'dayTime should be string')
|
|
52
|
+
assert.equal(typeof config.dayYotoDaily, 'string', 'dayYotoDaily should be string')
|
|
53
|
+
assert.equal(typeof config.dayYotoRadio, 'string', 'dayYotoRadio should be string')
|
|
54
|
+
assert.equal(typeof config.daySoundsOff, 'boolean', 'daySoundsOff should be boolean')
|
|
55
|
+
assert.equal(typeof config.displayDimBrightness, 'number', 'displayDimBrightness should be number')
|
|
56
|
+
assert.equal(typeof config.displayDimTimeout, 'number', 'displayDimTimeout should be number')
|
|
57
|
+
assert.equal(typeof config.headphonesVolumeLimited, 'boolean', 'headphonesVolumeLimited should be boolean')
|
|
58
|
+
assert.ok(config.hourFormat === 12 || config.hourFormat === 24, 'hourFormat should be 12 or 24')
|
|
59
|
+
assert.equal(typeof config.locale, 'string', 'locale should be string')
|
|
60
|
+
assert.equal(typeof config.logLevel, 'string', 'logLevel should be string')
|
|
61
|
+
assert.equal(typeof config.maxVolumeLimit, 'number', 'maxVolumeLimit should be number')
|
|
62
|
+
assert.equal(typeof config.nightAmbientColour, 'string', 'nightAmbientColour should be string')
|
|
63
|
+
assert.equal(typeof config.nightDisplayBrightnessAuto, 'boolean', 'nightDisplayBrightnessAuto should be boolean')
|
|
64
|
+
if (config.nightDisplayBrightnessAuto) {
|
|
65
|
+
assert.equal(config.nightDisplayBrightness, null, 'nightDisplayBrightness should be null when auto')
|
|
66
|
+
} else {
|
|
67
|
+
assert.equal(typeof config.nightDisplayBrightness, 'number', 'nightDisplayBrightness should be number when not auto')
|
|
68
|
+
}
|
|
69
|
+
assert.equal(typeof config.nightMaxVolumeLimit, 'number', 'nightMaxVolumeLimit should be number')
|
|
70
|
+
assert.equal(typeof config.nightTime, 'string', 'nightTime should be string')
|
|
71
|
+
assert.equal(typeof config.nightYotoDaily, 'string', 'nightYotoDaily should be string')
|
|
72
|
+
assert.equal(typeof config.nightYotoRadioEnabled, 'boolean', 'nightYotoRadioEnabled should be boolean')
|
|
73
|
+
if (config.nightYotoRadioEnabled) {
|
|
74
|
+
assert.equal(typeof config.nightYotoRadio, 'string', 'nightYotoRadio should be string when enabled')
|
|
75
|
+
} else {
|
|
76
|
+
assert.equal(config.nightYotoRadio, null, 'nightYotoRadio should be null when disabled')
|
|
77
|
+
}
|
|
78
|
+
assert.equal(typeof config.nightSoundsOff, 'boolean', 'nightSoundsOff should be boolean')
|
|
79
|
+
assert.equal(typeof config.pausePowerButton, 'boolean', 'pausePowerButton should be boolean')
|
|
80
|
+
assert.equal(typeof config.pauseVolumeDown, 'boolean', 'pauseVolumeDown should be boolean')
|
|
81
|
+
assert.equal(typeof config.repeatAll, 'boolean', 'repeatAll should be boolean')
|
|
82
|
+
assert.equal(typeof config.showDiagnostics, 'boolean', 'showDiagnostics should be boolean')
|
|
83
|
+
assert.equal(typeof config.shutdownTimeout, 'number', 'shutdownTimeout should be number')
|
|
84
|
+
assert.equal(typeof config.systemVolume, 'number', 'systemVolume should be number')
|
|
85
|
+
assert.equal(typeof config.timezone, 'string', 'timezone should be string')
|
|
86
|
+
assert.equal(typeof config.volumeLevel, 'string', 'volumeLevel should be string')
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* @param {YotoPlaybackState} playback
|
|
91
|
+
*/
|
|
92
|
+
export function assertPlaybackShape (playback) {
|
|
93
|
+
assert.ok(playback, 'playback should exist')
|
|
94
|
+
assert.equal(typeof playback.updatedAt, 'string', 'playback.updatedAt should be string')
|
|
95
|
+
|
|
96
|
+
assert.ok(playback.cardId === null || typeof playback.cardId === 'string', 'playback.cardId should be string or null')
|
|
97
|
+
assert.ok(playback.source === null || typeof playback.source === 'string', 'playback.source should be string or null')
|
|
98
|
+
assert.ok(playback.playbackStatus === null || typeof playback.playbackStatus === 'string', 'playback.playbackStatus should be string or null')
|
|
99
|
+
assert.ok(playback.trackTitle === null || typeof playback.trackTitle === 'string', 'playback.trackTitle should be string or null')
|
|
100
|
+
assert.ok(playback.trackKey === null || typeof playback.trackKey === 'string', 'playback.trackKey should be string or null')
|
|
101
|
+
assert.ok(playback.chapterTitle === null || typeof playback.chapterTitle === 'string', 'playback.chapterTitle should be string or null')
|
|
102
|
+
assert.ok(playback.chapterKey === null || typeof playback.chapterKey === 'string', 'playback.chapterKey should be string or null')
|
|
103
|
+
assert.ok(playback.position === null || typeof playback.position === 'number', 'playback.position should be number or null')
|
|
104
|
+
assert.ok(playback.trackLength === null || typeof playback.trackLength === 'number', 'playback.trackLength should be number or null')
|
|
105
|
+
assert.ok(playback.streaming === null || typeof playback.streaming === 'boolean', 'playback.streaming should be boolean or null')
|
|
106
|
+
assert.ok(playback.sleepTimerActive === null || typeof playback.sleepTimerActive === 'boolean', 'playback.sleepTimerActive should be boolean or null')
|
|
107
|
+
assert.ok(playback.sleepTimerSeconds === null || typeof playback.sleepTimerSeconds === 'number', 'playback.sleepTimerSeconds should be number or null')
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* @param {string | undefined} value
|
|
112
|
+
* @returns {string}
|
|
113
|
+
*/
|
|
114
|
+
export function toLower (value) {
|
|
115
|
+
return typeof value === 'string' ? value.toLowerCase() : ''
|
|
116
|
+
}
|