homebridge-yoto 0.0.28 → 0.0.31
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/AGENTS.md +42 -157
- package/CHANGELOG.md +13 -5
- package/NOTES.md +87 -0
- package/PLAN.md +320 -504
- package/README.md +18 -314
- package/config.schema.cjs +3 -0
- package/config.schema.json +19 -155
- package/homebridge-ui/server.js +264 -0
- package/index.js +1 -1
- package/index.test.js +1 -1
- package/lib/accessory.js +1870 -0
- package/lib/constants.js +8 -149
- package/lib/platform.js +303 -363
- package/lib/sanitize-name.js +49 -0
- package/lib/settings.js +16 -0
- package/lib/sync-service-names.js +34 -0
- package/logo.png +0 -0
- package/package.json +17 -22
- package/pnpm-workspace.yaml +4 -0
- package/declaration.tsconfig.json +0 -15
- package/lib/auth.js +0 -237
- package/lib/playerAccessory.js +0 -1724
- package/lib/types.js +0 -253
- package/lib/yotoApi.js +0 -270
- package/lib/yotoMqtt.js +0 -570
package/lib/yotoMqtt.js
DELETED
|
@@ -1,570 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @fileoverview MQTT client for real-time Yoto device communication
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
/** @import { Logger } from 'homebridge' */
|
|
6
|
-
/** @import { YotoDeviceStatus, YotoPlaybackEvents, MqttVolumeCommand, MqttAmbientCommand, MqttSleepTimerCommand, MqttCardStartCommand, MqttCommandResponse } from './types.js' */
|
|
7
|
-
|
|
8
|
-
import mqtt from 'mqtt'
|
|
9
|
-
import { EventEmitter } from 'events'
|
|
10
|
-
import {
|
|
11
|
-
YOTO_MQTT_BROKER_URL,
|
|
12
|
-
YOTO_MQTT_AUTH_NAME,
|
|
13
|
-
MQTT_RECONNECT_PERIOD,
|
|
14
|
-
MQTT_CONNECT_TIMEOUT,
|
|
15
|
-
MQTT_TOPIC_DATA_STATUS,
|
|
16
|
-
MQTT_TOPIC_DATA_EVENTS,
|
|
17
|
-
MQTT_TOPIC_RESPONSE,
|
|
18
|
-
MQTT_TOPIC_COMMAND_STATUS_REQUEST,
|
|
19
|
-
MQTT_TOPIC_COMMAND_EVENTS_REQUEST,
|
|
20
|
-
MQTT_TOPIC_COMMAND_VOLUME_SET,
|
|
21
|
-
MQTT_TOPIC_COMMAND_CARD_START,
|
|
22
|
-
MQTT_TOPIC_COMMAND_CARD_STOP,
|
|
23
|
-
MQTT_TOPIC_COMMAND_CARD_PAUSE,
|
|
24
|
-
MQTT_TOPIC_COMMAND_CARD_RESUME,
|
|
25
|
-
MQTT_TOPIC_COMMAND_SLEEP_TIMER,
|
|
26
|
-
MQTT_TOPIC_COMMAND_AMBIENTS_SET,
|
|
27
|
-
ERROR_MESSAGES,
|
|
28
|
-
LOG_PREFIX,
|
|
29
|
-
INITIAL_STATUS_REQUEST_DELAY
|
|
30
|
-
} from './constants.js'
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* MQTT client for Yoto device communication
|
|
34
|
-
* @extends EventEmitter
|
|
35
|
-
*/
|
|
36
|
-
export class YotoMqtt extends EventEmitter {
|
|
37
|
-
/**
|
|
38
|
-
* @param {Logger} log - Homebridge logger
|
|
39
|
-
* @param {Object} [options] - MQTT options
|
|
40
|
-
* @param {string} [options.brokerUrl] - MQTT broker URL
|
|
41
|
-
*/
|
|
42
|
-
constructor (log, options = {}) {
|
|
43
|
-
super()
|
|
44
|
-
this.log = log
|
|
45
|
-
this.brokerUrl = options.brokerUrl || YOTO_MQTT_BROKER_URL
|
|
46
|
-
this.client = null
|
|
47
|
-
this.connected = false
|
|
48
|
-
this.subscribedDevices = new Set()
|
|
49
|
-
this.deviceCallbacks = new Map()
|
|
50
|
-
this.reconnectAttempts = 0
|
|
51
|
-
this.maxReconnectAttempts = 10
|
|
52
|
-
this.reconnectDelay = MQTT_RECONNECT_PERIOD
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* Connect to MQTT broker
|
|
57
|
-
* @param {string} accessToken - Yoto access token for authentication
|
|
58
|
-
* @param {string} deviceId - Device ID for MQTT client identification
|
|
59
|
-
* @returns {Promise<void>}
|
|
60
|
-
*/
|
|
61
|
-
async connect (accessToken, deviceId) {
|
|
62
|
-
if (this.client) {
|
|
63
|
-
this.log.debug(LOG_PREFIX.MQTT, 'Already connected, disconnecting first...')
|
|
64
|
-
await this.disconnect()
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
return new Promise((resolve, reject) => {
|
|
68
|
-
this.log.debug(LOG_PREFIX.MQTT, `Connecting to ${this.brokerUrl}...`)
|
|
69
|
-
|
|
70
|
-
const clientId = `DASH${deviceId}`
|
|
71
|
-
const username = `${deviceId}?x-amz-customauthorizer-name=${YOTO_MQTT_AUTH_NAME}`
|
|
72
|
-
|
|
73
|
-
this.log.debug(LOG_PREFIX.MQTT, `Connecting with client ID: ${clientId}`)
|
|
74
|
-
|
|
75
|
-
this.client = mqtt.connect(this.brokerUrl, {
|
|
76
|
-
keepalive: 300,
|
|
77
|
-
port: 443,
|
|
78
|
-
protocol: 'wss',
|
|
79
|
-
username,
|
|
80
|
-
password: accessToken,
|
|
81
|
-
reconnectPeriod: 0, // Disable auto-reconnect - we'll handle reconnection manually
|
|
82
|
-
connectTimeout: MQTT_CONNECT_TIMEOUT,
|
|
83
|
-
clientId,
|
|
84
|
-
ALPNProtocols: ['x-amzn-mqtt-ca']
|
|
85
|
-
})
|
|
86
|
-
|
|
87
|
-
this.client.on('connect', () => {
|
|
88
|
-
this.connected = true
|
|
89
|
-
this.reconnectAttempts = 0
|
|
90
|
-
this.reconnectDelay = MQTT_RECONNECT_PERIOD
|
|
91
|
-
this.log.info(LOG_PREFIX.MQTT, '✓ Connected to MQTT broker')
|
|
92
|
-
|
|
93
|
-
// Emit connected event
|
|
94
|
-
this.emit('connected')
|
|
95
|
-
|
|
96
|
-
// Resubscribe to all devices after reconnection
|
|
97
|
-
this.resubscribeDevices()
|
|
98
|
-
|
|
99
|
-
resolve()
|
|
100
|
-
})
|
|
101
|
-
|
|
102
|
-
this.client.on('error', (error) => {
|
|
103
|
-
this.log.error(LOG_PREFIX.MQTT, 'Connection error:', error)
|
|
104
|
-
this.log.error(LOG_PREFIX.MQTT, 'Error message:', error.message)
|
|
105
|
-
const errorWithCode = /** @type {any} */ (error)
|
|
106
|
-
this.log.error(LOG_PREFIX.MQTT, 'Error code:', errorWithCode.code)
|
|
107
|
-
this.log.error(LOG_PREFIX.MQTT, 'Error stack:', error.stack)
|
|
108
|
-
if (errorWithCode.code) {
|
|
109
|
-
this.log.error(LOG_PREFIX.MQTT, `AWS IoT error code: ${errorWithCode.code}`)
|
|
110
|
-
}
|
|
111
|
-
if (!this.connected) {
|
|
112
|
-
reject(error)
|
|
113
|
-
}
|
|
114
|
-
})
|
|
115
|
-
|
|
116
|
-
this.client.on('close', () => {
|
|
117
|
-
const wasConnected = this.connected
|
|
118
|
-
this.connected = false
|
|
119
|
-
|
|
120
|
-
// Emit disconnected event
|
|
121
|
-
this.emit('disconnected')
|
|
122
|
-
|
|
123
|
-
if (wasConnected) {
|
|
124
|
-
this.log.info(LOG_PREFIX.MQTT, ERROR_MESSAGES.MQTT_DISCONNECTED)
|
|
125
|
-
this.handleReconnect()
|
|
126
|
-
} else {
|
|
127
|
-
this.log.error(LOG_PREFIX.MQTT, 'Connection closed before establishing connection')
|
|
128
|
-
}
|
|
129
|
-
})
|
|
130
|
-
|
|
131
|
-
this.client.on('reconnect', () => {
|
|
132
|
-
this.reconnectAttempts++
|
|
133
|
-
this.log.debug(LOG_PREFIX.MQTT, `Reconnection attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts}`)
|
|
134
|
-
|
|
135
|
-
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
|
|
136
|
-
this.log.error(LOG_PREFIX.MQTT, 'Max reconnection attempts reached, stopping reconnection')
|
|
137
|
-
if (this.client) {
|
|
138
|
-
this.client.end(true)
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
})
|
|
142
|
-
|
|
143
|
-
this.client.on('offline', () => {
|
|
144
|
-
this.connected = false
|
|
145
|
-
this.log.debug(LOG_PREFIX.MQTT, 'MQTT client offline - connection failed or lost')
|
|
146
|
-
|
|
147
|
-
// Emit offline event
|
|
148
|
-
this.emit('offline')
|
|
149
|
-
})
|
|
150
|
-
|
|
151
|
-
this.client.on('end', () => {
|
|
152
|
-
this.log.debug(LOG_PREFIX.MQTT, 'MQTT client ended')
|
|
153
|
-
})
|
|
154
|
-
|
|
155
|
-
this.client.on('disconnect', (packet) => {
|
|
156
|
-
this.log.debug(LOG_PREFIX.MQTT, 'MQTT client disconnected:', packet)
|
|
157
|
-
})
|
|
158
|
-
|
|
159
|
-
this.client.on('packetreceive', (packet) => {
|
|
160
|
-
this.log.debug(LOG_PREFIX.MQTT, 'Packet received:', packet.cmd)
|
|
161
|
-
})
|
|
162
|
-
|
|
163
|
-
this.client.on('packetsend', (packet) => {
|
|
164
|
-
this.log.debug(LOG_PREFIX.MQTT, 'Packet sent:', packet.cmd)
|
|
165
|
-
})
|
|
166
|
-
|
|
167
|
-
this.client.on('message', (topic, message) => {
|
|
168
|
-
this.handleMessage(topic, message)
|
|
169
|
-
})
|
|
170
|
-
|
|
171
|
-
// Timeout if connection takes too long
|
|
172
|
-
setTimeout(() => {
|
|
173
|
-
if (!this.connected) {
|
|
174
|
-
reject(new Error(ERROR_MESSAGES.MQTT_CONNECTION_FAILED))
|
|
175
|
-
}
|
|
176
|
-
}, MQTT_CONNECT_TIMEOUT)
|
|
177
|
-
})
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
/**
|
|
181
|
-
* Disconnect from MQTT broker
|
|
182
|
-
* @returns {Promise<void>}
|
|
183
|
-
*/
|
|
184
|
-
async disconnect () {
|
|
185
|
-
if (!this.client) {
|
|
186
|
-
return
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
return new Promise((resolve) => {
|
|
190
|
-
this.log.debug(LOG_PREFIX.MQTT, 'Disconnecting from MQTT broker...')
|
|
191
|
-
|
|
192
|
-
if (this.client) {
|
|
193
|
-
this.client.end(false, {}, () => {
|
|
194
|
-
this.connected = false
|
|
195
|
-
this.client = null
|
|
196
|
-
this.subscribedDevices.clear()
|
|
197
|
-
this.log.debug(LOG_PREFIX.MQTT, 'Disconnected')
|
|
198
|
-
resolve()
|
|
199
|
-
})
|
|
200
|
-
} else {
|
|
201
|
-
resolve()
|
|
202
|
-
}
|
|
203
|
-
})
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
/**
|
|
207
|
-
* Handle reconnection with exponential backoff
|
|
208
|
-
*/
|
|
209
|
-
handleReconnect () {
|
|
210
|
-
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
|
|
211
|
-
this.log.error(LOG_PREFIX.MQTT, 'Max reconnection attempts reached')
|
|
212
|
-
return
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
// Exponential backoff with jitter
|
|
216
|
-
this.reconnectDelay = Math.min(
|
|
217
|
-
MQTT_RECONNECT_PERIOD * Math.pow(2, this.reconnectAttempts),
|
|
218
|
-
60000 // Max 60 seconds
|
|
219
|
-
)
|
|
220
|
-
|
|
221
|
-
const jitter = Math.random() * 1000
|
|
222
|
-
const delay = this.reconnectDelay + jitter
|
|
223
|
-
|
|
224
|
-
this.log.debug(LOG_PREFIX.MQTT, `Will attempt reconnection in ${Math.round(delay / 1000)}s`)
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
/**
|
|
228
|
-
* Resubscribe to all device topics after reconnection
|
|
229
|
-
*/
|
|
230
|
-
async resubscribeDevices () {
|
|
231
|
-
if (this.subscribedDevices.size === 0) {
|
|
232
|
-
return
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
this.log.debug(LOG_PREFIX.MQTT, `Resubscribing to ${this.subscribedDevices.size} device(s)...`)
|
|
236
|
-
|
|
237
|
-
for (const deviceId of this.subscribedDevices) {
|
|
238
|
-
const callbacks = this.deviceCallbacks.get(deviceId)
|
|
239
|
-
if (callbacks) {
|
|
240
|
-
try {
|
|
241
|
-
// Clear from set temporarily to allow resubscription
|
|
242
|
-
this.subscribedDevices.delete(deviceId)
|
|
243
|
-
await this.subscribeToDevice(deviceId, callbacks)
|
|
244
|
-
} catch (error) {
|
|
245
|
-
this.log.error(LOG_PREFIX.MQTT, `Failed to resubscribe to device ${deviceId}:`, error)
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
/**
|
|
252
|
-
* Subscribe to device topics
|
|
253
|
-
* @param {string} deviceId - Device ID
|
|
254
|
-
* @param {Object} callbacks - Callback functions
|
|
255
|
-
* @param {(status: YotoDeviceStatus) => void} [callbacks.onStatus] - Status update callback
|
|
256
|
-
* @param {(events: YotoPlaybackEvents) => void} [callbacks.onEvents] - Events update callback
|
|
257
|
-
* @param {(response: MqttCommandResponse) => void} [callbacks.onResponse] - Command response callback
|
|
258
|
-
* @returns {Promise<void>}
|
|
259
|
-
*/
|
|
260
|
-
async subscribeToDevice (deviceId, callbacks) {
|
|
261
|
-
if (!this.client || !this.connected) {
|
|
262
|
-
throw new Error('MQTT client not connected')
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
if (this.subscribedDevices.has(deviceId)) {
|
|
266
|
-
this.log.debug(LOG_PREFIX.MQTT, `Already subscribed to device ${deviceId}`)
|
|
267
|
-
return
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
this.log.debug(LOG_PREFIX.MQTT, `Subscribing to device ${deviceId}...`)
|
|
271
|
-
|
|
272
|
-
const topics = [
|
|
273
|
-
this.buildTopic(MQTT_TOPIC_DATA_STATUS, deviceId),
|
|
274
|
-
this.buildTopic(MQTT_TOPIC_DATA_EVENTS, deviceId),
|
|
275
|
-
this.buildTopic(MQTT_TOPIC_RESPONSE, deviceId)
|
|
276
|
-
]
|
|
277
|
-
|
|
278
|
-
return new Promise((resolve, reject) => {
|
|
279
|
-
if (!this.client) {
|
|
280
|
-
reject(new Error('MQTT client not available'))
|
|
281
|
-
return
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
this.client.subscribe(topics, (error) => {
|
|
285
|
-
if (error) {
|
|
286
|
-
this.log.error(LOG_PREFIX.MQTT, `Failed to subscribe to device ${deviceId}:`, error)
|
|
287
|
-
reject(error)
|
|
288
|
-
return
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
this.subscribedDevices.add(deviceId)
|
|
292
|
-
this.deviceCallbacks.set(deviceId, callbacks)
|
|
293
|
-
this.log.debug(LOG_PREFIX.MQTT, `✓ Subscribed to device ${deviceId}`)
|
|
294
|
-
|
|
295
|
-
// Request initial status after a short delay
|
|
296
|
-
setTimeout(() => {
|
|
297
|
-
this.requestStatus(deviceId).catch(err => {
|
|
298
|
-
this.log.debug(LOG_PREFIX.MQTT, `Failed to request initial status for ${deviceId}:`, err)
|
|
299
|
-
})
|
|
300
|
-
this.requestEvents(deviceId).catch(err => {
|
|
301
|
-
this.log.debug(LOG_PREFIX.MQTT, `Failed to request initial events for ${deviceId}:`, err)
|
|
302
|
-
})
|
|
303
|
-
}, INITIAL_STATUS_REQUEST_DELAY)
|
|
304
|
-
|
|
305
|
-
resolve()
|
|
306
|
-
})
|
|
307
|
-
})
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
/**
|
|
311
|
-
* Unsubscribe from device topics
|
|
312
|
-
* @param {string} deviceId - Device ID
|
|
313
|
-
* @returns {Promise<void>}
|
|
314
|
-
*/
|
|
315
|
-
async unsubscribeFromDevice (deviceId) {
|
|
316
|
-
if (!this.client || !this.subscribedDevices.has(deviceId)) {
|
|
317
|
-
return
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
this.log.debug(LOG_PREFIX.MQTT, `Unsubscribing from device ${deviceId}...`)
|
|
321
|
-
|
|
322
|
-
const topics = [
|
|
323
|
-
this.buildTopic(MQTT_TOPIC_DATA_STATUS, deviceId),
|
|
324
|
-
this.buildTopic(MQTT_TOPIC_DATA_EVENTS, deviceId),
|
|
325
|
-
this.buildTopic(MQTT_TOPIC_RESPONSE, deviceId)
|
|
326
|
-
]
|
|
327
|
-
|
|
328
|
-
return new Promise((resolve, reject) => {
|
|
329
|
-
if (!this.client) {
|
|
330
|
-
reject(new Error('MQTT client not available'))
|
|
331
|
-
return
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
this.client.unsubscribe(topics, (error) => {
|
|
335
|
-
if (error) {
|
|
336
|
-
this.log.error(LOG_PREFIX.MQTT, `Failed to unsubscribe from device ${deviceId}:`, error)
|
|
337
|
-
reject(error)
|
|
338
|
-
return
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
this.subscribedDevices.delete(deviceId)
|
|
342
|
-
this.deviceCallbacks.delete(deviceId)
|
|
343
|
-
this.log.debug(LOG_PREFIX.MQTT, `✓ Unsubscribed from device ${deviceId}`)
|
|
344
|
-
resolve()
|
|
345
|
-
})
|
|
346
|
-
})
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
/**
|
|
350
|
-
* Handle incoming MQTT message
|
|
351
|
-
* @param {string} topic - Message topic
|
|
352
|
-
* @param {Buffer} message - Message payload
|
|
353
|
-
*/
|
|
354
|
-
handleMessage (topic, message) {
|
|
355
|
-
try {
|
|
356
|
-
const payload = JSON.parse(message.toString())
|
|
357
|
-
const deviceId = this.extractDeviceId(topic)
|
|
358
|
-
|
|
359
|
-
if (!deviceId) {
|
|
360
|
-
this.log.debug(LOG_PREFIX.MQTT, `Could not extract device ID from topic: ${topic}`)
|
|
361
|
-
return
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
const callbacks = this.deviceCallbacks.get(deviceId)
|
|
365
|
-
if (!callbacks) {
|
|
366
|
-
this.log.debug(LOG_PREFIX.MQTT, `No callbacks registered for device ${deviceId}`)
|
|
367
|
-
return
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
if (topic.includes('/status')) {
|
|
371
|
-
this.log.debug(LOG_PREFIX.MQTT, `Status update for ${deviceId}`)
|
|
372
|
-
callbacks.onStatus?.(payload)
|
|
373
|
-
} else if (topic.includes('/events')) {
|
|
374
|
-
this.log.debug(LOG_PREFIX.MQTT, `Events update for ${deviceId}`)
|
|
375
|
-
callbacks.onEvents?.(payload)
|
|
376
|
-
} else if (topic.includes('/response')) {
|
|
377
|
-
this.log.debug(LOG_PREFIX.MQTT, `Command response for ${deviceId}`)
|
|
378
|
-
callbacks.onResponse?.(payload)
|
|
379
|
-
}
|
|
380
|
-
} catch (error) {
|
|
381
|
-
this.log.error(LOG_PREFIX.MQTT, 'Error handling message:', error)
|
|
382
|
-
this.log.debug(LOG_PREFIX.MQTT, 'Failed message topic:', topic)
|
|
383
|
-
this.log.debug(LOG_PREFIX.MQTT, 'Failed message payload:', message.toString())
|
|
384
|
-
}
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
/**
|
|
388
|
-
* Publish a command to device
|
|
389
|
-
* @param {string} topic - Command topic
|
|
390
|
-
* @param {any} [payload] - Command payload
|
|
391
|
-
* @returns {Promise<void>}
|
|
392
|
-
*/
|
|
393
|
-
async publish (topic, payload = {}) {
|
|
394
|
-
if (!this.client || !this.connected) {
|
|
395
|
-
throw new Error('MQTT client not connected')
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
return new Promise((resolve, reject) => {
|
|
399
|
-
if (!this.client) {
|
|
400
|
-
reject(new Error('MQTT client not available'))
|
|
401
|
-
return
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
const message = JSON.stringify(payload)
|
|
405
|
-
this.log.debug(LOG_PREFIX.MQTT, `Publishing to ${topic}:`, message)
|
|
406
|
-
|
|
407
|
-
// Add timeout for publish operation
|
|
408
|
-
const timeout = setTimeout(() => {
|
|
409
|
-
reject(new Error('MQTT publish timeout'))
|
|
410
|
-
}, 5000)
|
|
411
|
-
|
|
412
|
-
this.client.publish(topic, message, { qos: 0 }, (error) => {
|
|
413
|
-
clearTimeout(timeout)
|
|
414
|
-
|
|
415
|
-
if (error) {
|
|
416
|
-
this.log.error(LOG_PREFIX.MQTT, `Failed to publish to ${topic}:`, error)
|
|
417
|
-
reject(error)
|
|
418
|
-
} else {
|
|
419
|
-
resolve()
|
|
420
|
-
}
|
|
421
|
-
})
|
|
422
|
-
})
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
/**
|
|
426
|
-
* Request current device status
|
|
427
|
-
* @param {string} deviceId - Device ID
|
|
428
|
-
* @returns {Promise<void>}
|
|
429
|
-
*/
|
|
430
|
-
async requestStatus (deviceId) {
|
|
431
|
-
const topic = this.buildTopic(MQTT_TOPIC_COMMAND_STATUS_REQUEST, deviceId)
|
|
432
|
-
await this.publish(topic, {})
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
/**
|
|
436
|
-
* Request current playback events
|
|
437
|
-
* @param {string} deviceId - Device ID
|
|
438
|
-
* @returns {Promise<void>}
|
|
439
|
-
*/
|
|
440
|
-
async requestEvents (deviceId) {
|
|
441
|
-
const topic = this.buildTopic(MQTT_TOPIC_COMMAND_EVENTS_REQUEST, deviceId)
|
|
442
|
-
await this.publish(topic, {})
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
/**
|
|
446
|
-
* Set device volume
|
|
447
|
-
* @param {string} deviceId - Device ID
|
|
448
|
-
* @param {number} volume - Volume level (0-100)
|
|
449
|
-
* @returns {Promise<void>}
|
|
450
|
-
*/
|
|
451
|
-
async setVolume (deviceId, volume) {
|
|
452
|
-
const topic = this.buildTopic(MQTT_TOPIC_COMMAND_VOLUME_SET, deviceId)
|
|
453
|
-
/** @type {MqttVolumeCommand} */
|
|
454
|
-
const payload = { volume: Math.round(volume) }
|
|
455
|
-
await this.publish(topic, payload)
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
/**
|
|
459
|
-
* Start playing a card
|
|
460
|
-
* @param {string} deviceId - Device ID
|
|
461
|
-
* @param {string} cardUri - Card URI
|
|
462
|
-
* @param {Object} [options] - Playback options
|
|
463
|
-
* @param {string} [options.chapterKey] - Chapter to start from
|
|
464
|
-
* @param {string} [options.trackKey] - Track to start from
|
|
465
|
-
* @param {number} [options.secondsIn] - Start offset in seconds
|
|
466
|
-
* @param {number} [options.cutOff] - Stop offset in seconds
|
|
467
|
-
* @param {boolean} [options.anyButtonStop] - Any button stops playback
|
|
468
|
-
* @returns {Promise<void>}
|
|
469
|
-
*/
|
|
470
|
-
async startCard (deviceId, cardUri, options = {}) {
|
|
471
|
-
const topic = this.buildTopic(MQTT_TOPIC_COMMAND_CARD_START, deviceId)
|
|
472
|
-
/** @type {MqttCardStartCommand} */
|
|
473
|
-
const payload = {
|
|
474
|
-
uri: cardUri
|
|
475
|
-
}
|
|
476
|
-
if (options.chapterKey !== undefined) payload.chapterKey = options.chapterKey
|
|
477
|
-
if (options.trackKey !== undefined) payload.trackKey = options.trackKey
|
|
478
|
-
if (options.secondsIn !== undefined) payload.secondsIn = options.secondsIn
|
|
479
|
-
if (options.cutOff !== undefined) payload.cutOff = options.cutOff
|
|
480
|
-
if (options.anyButtonStop !== undefined) payload.anyButtonStop = options.anyButtonStop
|
|
481
|
-
await this.publish(topic, payload)
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
/**
|
|
485
|
-
* Pause card playback
|
|
486
|
-
* @param {string} deviceId - Device ID
|
|
487
|
-
* @returns {Promise<void>}
|
|
488
|
-
*/
|
|
489
|
-
async pauseCard (deviceId) {
|
|
490
|
-
const topic = this.buildTopic(MQTT_TOPIC_COMMAND_CARD_PAUSE, deviceId)
|
|
491
|
-
await this.publish(topic, {})
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
/**
|
|
495
|
-
* Resume card playback
|
|
496
|
-
* @param {string} deviceId - Device ID
|
|
497
|
-
* @returns {Promise<void>}
|
|
498
|
-
*/
|
|
499
|
-
async resumeCard (deviceId) {
|
|
500
|
-
const topic = this.buildTopic(MQTT_TOPIC_COMMAND_CARD_RESUME, deviceId)
|
|
501
|
-
await this.publish(topic, {})
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
/**
|
|
505
|
-
* Stop card playback
|
|
506
|
-
* @param {string} deviceId - Device ID
|
|
507
|
-
* @returns {Promise<void>}
|
|
508
|
-
*/
|
|
509
|
-
async stopCard (deviceId) {
|
|
510
|
-
const topic = this.buildTopic(MQTT_TOPIC_COMMAND_CARD_STOP, deviceId)
|
|
511
|
-
await this.publish(topic, {})
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
/**
|
|
515
|
-
* Set sleep timer
|
|
516
|
-
* @param {string} deviceId - Device ID
|
|
517
|
-
* @param {number} seconds - Duration in seconds (0 to disable)
|
|
518
|
-
* @returns {Promise<void>}
|
|
519
|
-
*/
|
|
520
|
-
async setSleepTimer (deviceId, seconds) {
|
|
521
|
-
const topic = this.buildTopic(MQTT_TOPIC_COMMAND_SLEEP_TIMER, deviceId)
|
|
522
|
-
/** @type {MqttSleepTimerCommand} */
|
|
523
|
-
const payload = { seconds }
|
|
524
|
-
await this.publish(topic, payload)
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
/**
|
|
528
|
-
* Set ambient light color
|
|
529
|
-
* @param {string} deviceId - Device ID
|
|
530
|
-
* @param {number} r - Red intensity (0-255)
|
|
531
|
-
* @param {number} g - Green intensity (0-255)
|
|
532
|
-
* @param {number} b - Blue intensity (0-255)
|
|
533
|
-
* @returns {Promise<void>}
|
|
534
|
-
*/
|
|
535
|
-
async setAmbientLight (deviceId, r, g, b) {
|
|
536
|
-
const topic = this.buildTopic(MQTT_TOPIC_COMMAND_AMBIENTS_SET, deviceId)
|
|
537
|
-
/** @type {MqttAmbientCommand} */
|
|
538
|
-
const payload = { r, g, b }
|
|
539
|
-
await this.publish(topic, payload)
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
/**
|
|
543
|
-
* Build topic string with device ID
|
|
544
|
-
* @param {string} template - Topic template
|
|
545
|
-
* @param {string} deviceId - Device ID
|
|
546
|
-
* @returns {string}
|
|
547
|
-
*/
|
|
548
|
-
buildTopic (template, deviceId) {
|
|
549
|
-
return template.replace('{deviceId}', deviceId)
|
|
550
|
-
}
|
|
551
|
-
|
|
552
|
-
/**
|
|
553
|
-
* Extract device ID from topic
|
|
554
|
-
* @param {string} topic - Topic string
|
|
555
|
-
* @returns {string | null}
|
|
556
|
-
*/
|
|
557
|
-
extractDeviceId (topic) {
|
|
558
|
-
// Match both /device/{id}/ and device/{id}/ patterns
|
|
559
|
-
const match = topic.match(/\/?device\/([^/]+)\//)
|
|
560
|
-
return match?.[1] ?? null
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
/**
|
|
564
|
-
* Check if connected to MQTT broker
|
|
565
|
-
* @returns {boolean}
|
|
566
|
-
*/
|
|
567
|
-
isConnected () {
|
|
568
|
-
return this.connected && this.client !== null
|
|
569
|
-
}
|
|
570
|
-
}
|