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 CHANGED
@@ -211,24 +211,21 @@ account.on('started', (metadata) => {
211
211
 
212
212
 
213
213
 
214
- // Listen for device-specific events from individual devices
215
- account.on('deviceAdded', (deviceId, deviceModel) => {
216
- // Attach event listeners to each device as it's added
217
- deviceModel.on('statusUpdate', (status, source) => {
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
- deviceModel.on('online', (metadata) => {
222
- console.log(`${deviceId} came online`)
223
- })
219
+ account.on('online', ({ deviceId }) => {
220
+ console.log(`${deviceId} came online`)
221
+ })
224
222
 
225
- deviceModel.on('offline', (metadata) => {
226
- console.log(`${deviceId} went offline`)
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, deviceModel)` - Device was added
1055
- - `deviceRemoved(deviceId)` - Device was removed
1056
- - `error(error, context)` - Error occurred (context: { source, deviceId, operation })
1057
-
1058
- **Note:** To listen to individual device events (statusUpdate, configUpdate, playbackUpdate, online, offline, mqttConnect, mqttDisconnect, mqttClose, mqttReconnect, etc.), access the device models directly via `account.devices` or `account.getDevice(deviceId)` and attach listeners to them.
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
- // Listen to device added events
1083
- account.on('deviceAdded', (deviceId, deviceModel) => {
1084
- console.log(`Device ${deviceId} added`)
1085
-
1086
- // Attach listeners to individual devices
1087
- deviceModel.on('statusUpdate', (status, source) => {
1088
- console.log(`${deviceId} battery: ${status.batteryLevelPercentage}%`)
1089
- })
1090
-
1091
- deviceModel.on('online', (metadata) => {
1092
- console.log(`${deviceId} came online (${metadata.reason})`)
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
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
+ }