homebridge-yoto 0.0.9 → 0.0.10
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/lib/playerAccessory.js +120 -0
- package/lib/yotoMqtt.js +40 -5
- package/package.json +1 -1
package/lib/playerAccessory.js
CHANGED
|
@@ -41,6 +41,10 @@ export class YotoPlayerAccessory {
|
|
|
41
41
|
// Create dedicated MQTT client for this device
|
|
42
42
|
this.mqtt = new YotoMqtt(this.log)
|
|
43
43
|
|
|
44
|
+
// Track online status polling
|
|
45
|
+
this.statusPollInterval = null
|
|
46
|
+
this.mqttConnected = false
|
|
47
|
+
|
|
44
48
|
this.log.debug(LOG_PREFIX.ACCESSORY, `[${this.device.name}] Initializing accessory`)
|
|
45
49
|
|
|
46
50
|
// Set up services
|
|
@@ -107,6 +111,84 @@ export class YotoPlayerAccessory {
|
|
|
107
111
|
*/
|
|
108
112
|
async initialize () {
|
|
109
113
|
await this.connectMqtt()
|
|
114
|
+
|
|
115
|
+
// Start polling for device status updates (every 60 seconds)
|
|
116
|
+
this.startStatusPolling()
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Start periodic status polling to detect online/offline changes
|
|
121
|
+
*/
|
|
122
|
+
startStatusPolling () {
|
|
123
|
+
// Poll every 60 seconds
|
|
124
|
+
this.statusPollInterval = setInterval(async () => {
|
|
125
|
+
try {
|
|
126
|
+
await this.checkDeviceStatus()
|
|
127
|
+
} catch (error) {
|
|
128
|
+
this.log.error(LOG_PREFIX.ACCESSORY, `[${this.device.name}] Failed to check device status:`, error)
|
|
129
|
+
}
|
|
130
|
+
}, 60000)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Stop status polling
|
|
135
|
+
*/
|
|
136
|
+
stopStatusPolling () {
|
|
137
|
+
if (this.statusPollInterval) {
|
|
138
|
+
clearInterval(this.statusPollInterval)
|
|
139
|
+
this.statusPollInterval = null
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Check device online status and reconnect if needed
|
|
145
|
+
* @returns {Promise<void>}
|
|
146
|
+
*/
|
|
147
|
+
async checkDeviceStatus () {
|
|
148
|
+
try {
|
|
149
|
+
// Fetch fresh device list from API
|
|
150
|
+
const devices = await this.platform.yotoApi.getDevices()
|
|
151
|
+
const currentDevice = devices.find(d => d.deviceId === this.device.deviceId)
|
|
152
|
+
|
|
153
|
+
if (!currentDevice) {
|
|
154
|
+
this.log.warn(LOG_PREFIX.ACCESSORY, `[${this.device.name}] Device no longer found in API`)
|
|
155
|
+
return
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const wasOnline = this.device.online
|
|
159
|
+
const isNowOnline = currentDevice.online
|
|
160
|
+
|
|
161
|
+
// Update device info
|
|
162
|
+
this.device = currentDevice
|
|
163
|
+
this.accessory.context.device = currentDevice
|
|
164
|
+
|
|
165
|
+
// Handle state changes
|
|
166
|
+
if (!wasOnline && isNowOnline) {
|
|
167
|
+
this.log.info(LOG_PREFIX.ACCESSORY, `[${this.device.name}] Device came online, connecting MQTT...`)
|
|
168
|
+
await this.connectMqtt()
|
|
169
|
+
} else if (wasOnline && !isNowOnline) {
|
|
170
|
+
this.log.warn(LOG_PREFIX.ACCESSORY, `[${this.device.name}] Device went offline`)
|
|
171
|
+
await this.disconnectMqtt()
|
|
172
|
+
}
|
|
173
|
+
} catch (error) {
|
|
174
|
+
this.log.error(LOG_PREFIX.ACCESSORY, `[${this.device.name}] Error checking device status:`, error)
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Disconnect MQTT for this device
|
|
180
|
+
* @returns {Promise<void>}
|
|
181
|
+
*/
|
|
182
|
+
async disconnectMqtt () {
|
|
183
|
+
if (this.mqtt && this.mqttConnected) {
|
|
184
|
+
try {
|
|
185
|
+
await this.mqtt.disconnect()
|
|
186
|
+
this.mqttConnected = false
|
|
187
|
+
this.log.debug(LOG_PREFIX.ACCESSORY, `[${this.device.name}] MQTT disconnected`)
|
|
188
|
+
} catch (error) {
|
|
189
|
+
this.log.error(LOG_PREFIX.ACCESSORY, `[${this.device.name}] Failed to disconnect MQTT:`, error)
|
|
190
|
+
}
|
|
191
|
+
}
|
|
110
192
|
}
|
|
111
193
|
|
|
112
194
|
/**
|
|
@@ -397,11 +479,30 @@ export class YotoPlayerAccessory {
|
|
|
397
479
|
.onSet(this.setAmbientLightBrightness.bind(this))
|
|
398
480
|
}
|
|
399
481
|
|
|
482
|
+
/**
|
|
483
|
+
* Cleanup and destroy the accessory
|
|
484
|
+
*/
|
|
485
|
+
async destroy () {
|
|
486
|
+
this.log.debug(LOG_PREFIX.ACCESSORY, `[${this.device.name}] Destroying accessory`)
|
|
487
|
+
|
|
488
|
+
// Stop status polling
|
|
489
|
+
this.stopStatusPolling()
|
|
490
|
+
|
|
491
|
+
// Disconnect MQTT
|
|
492
|
+
await this.disconnectMqtt()
|
|
493
|
+
}
|
|
494
|
+
|
|
400
495
|
/**
|
|
401
496
|
* Connect MQTT for this device
|
|
402
497
|
*/
|
|
403
498
|
async connectMqtt () {
|
|
404
499
|
try {
|
|
500
|
+
// Check if device is online first
|
|
501
|
+
if (!this.device.online) {
|
|
502
|
+
this.log.warn(LOG_PREFIX.ACCESSORY, `[${this.device.name}] Device is offline, skipping MQTT connection`)
|
|
503
|
+
return
|
|
504
|
+
}
|
|
505
|
+
|
|
405
506
|
// Ensure we have an access token
|
|
406
507
|
if (!this.platform.config.accessToken) {
|
|
407
508
|
this.log.warn(LOG_PREFIX.ACCESSORY, `[${this.device.name}] No access token available for MQTT connection`)
|
|
@@ -410,6 +511,7 @@ export class YotoPlayerAccessory {
|
|
|
410
511
|
|
|
411
512
|
// TEMPORARY: Debug logging for MQTT troubleshooting
|
|
412
513
|
this.log.warn(LOG_PREFIX.ACCESSORY, `[${this.device.name}] MQTT Connection Details:`)
|
|
514
|
+
this.log.warn(LOG_PREFIX.ACCESSORY, ` Device Online: ${this.device.online}`)
|
|
413
515
|
this.log.warn(LOG_PREFIX.ACCESSORY, ` Device ID: ${this.device.deviceId}`)
|
|
414
516
|
this.log.warn(LOG_PREFIX.ACCESSORY, ` Access Token: ${this.platform.config.accessToken}`)
|
|
415
517
|
this.log.warn(LOG_PREFIX.ACCESSORY, ` Token Length: ${this.platform.config.accessToken.length}`)
|
|
@@ -427,8 +529,26 @@ export class YotoPlayerAccessory {
|
|
|
427
529
|
onResponse: this.handleCommandResponse.bind(this)
|
|
428
530
|
})
|
|
429
531
|
|
|
532
|
+
this.mqttConnected = true
|
|
430
533
|
this.log.debug(LOG_PREFIX.ACCESSORY, `[${this.device.name}] MQTT connected and subscribed`)
|
|
534
|
+
|
|
535
|
+
// Set up MQTT event listeners
|
|
536
|
+
this.mqtt.on('disconnected', () => {
|
|
537
|
+
this.mqttConnected = false
|
|
538
|
+
this.log.warn(LOG_PREFIX.ACCESSORY, `[${this.device.name}] MQTT disconnected`)
|
|
539
|
+
})
|
|
540
|
+
|
|
541
|
+
this.mqtt.on('offline', () => {
|
|
542
|
+
this.mqttConnected = false
|
|
543
|
+
this.log.warn(LOG_PREFIX.ACCESSORY, `[${this.device.name}] MQTT offline`)
|
|
544
|
+
})
|
|
545
|
+
|
|
546
|
+
this.mqtt.on('connected', () => {
|
|
547
|
+
this.mqttConnected = true
|
|
548
|
+
this.log.info(LOG_PREFIX.ACCESSORY, `[${this.device.name}] MQTT reconnected`)
|
|
549
|
+
})
|
|
431
550
|
} catch (error) {
|
|
551
|
+
this.mqttConnected = false
|
|
432
552
|
this.log.error(LOG_PREFIX.ACCESSORY, `[${this.device.name}] Failed to connect MQTT:`, error)
|
|
433
553
|
}
|
|
434
554
|
}
|
package/lib/yotoMqtt.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
/** @import { YotoDeviceStatus, YotoPlaybackEvents, MqttVolumeCommand, MqttAmbientCommand, MqttSleepTimerCommand, MqttCardStartCommand, MqttCommandResponse } from './types.js' */
|
|
7
7
|
|
|
8
8
|
import mqtt from 'mqtt'
|
|
9
|
+
import { EventEmitter } from 'events'
|
|
9
10
|
import {
|
|
10
11
|
YOTO_MQTT_BROKER_URL,
|
|
11
12
|
YOTO_MQTT_AUTH_NAME,
|
|
@@ -30,14 +31,16 @@ import {
|
|
|
30
31
|
|
|
31
32
|
/**
|
|
32
33
|
* MQTT client for Yoto device communication
|
|
34
|
+
* @extends EventEmitter
|
|
33
35
|
*/
|
|
34
|
-
export class YotoMqtt {
|
|
36
|
+
export class YotoMqtt extends EventEmitter {
|
|
35
37
|
/**
|
|
36
38
|
* @param {Logger} log - Homebridge logger
|
|
37
39
|
* @param {Object} [options] - MQTT options
|
|
38
40
|
* @param {string} [options.brokerUrl] - MQTT broker URL
|
|
39
41
|
*/
|
|
40
42
|
constructor (log, options = {}) {
|
|
43
|
+
super()
|
|
41
44
|
this.log = log
|
|
42
45
|
this.brokerUrl = options.brokerUrl || YOTO_MQTT_BROKER_URL
|
|
43
46
|
this.client = null
|
|
@@ -64,7 +67,7 @@ export class YotoMqtt {
|
|
|
64
67
|
return new Promise((resolve, reject) => {
|
|
65
68
|
this.log.info(LOG_PREFIX.MQTT, `Connecting to ${this.brokerUrl}...`)
|
|
66
69
|
|
|
67
|
-
const clientId = `
|
|
70
|
+
const clientId = `DASH${deviceId}`
|
|
68
71
|
const username = `${deviceId}?x-amz-customauthorizer-name=${YOTO_MQTT_AUTH_NAME}`
|
|
69
72
|
|
|
70
73
|
// TEMPORARY: Detailed debug logging for MQTT troubleshooting
|
|
@@ -92,7 +95,6 @@ export class YotoMqtt {
|
|
|
92
95
|
reconnectPeriod: 0, // Disable auto-reconnect - we'll handle reconnection manually
|
|
93
96
|
connectTimeout: MQTT_CONNECT_TIMEOUT,
|
|
94
97
|
clientId,
|
|
95
|
-
clean: true,
|
|
96
98
|
ALPNProtocols: ['x-amzn-mqtt-ca']
|
|
97
99
|
})
|
|
98
100
|
|
|
@@ -102,6 +104,9 @@ export class YotoMqtt {
|
|
|
102
104
|
this.reconnectDelay = MQTT_RECONNECT_PERIOD
|
|
103
105
|
this.log.info(LOG_PREFIX.MQTT, '✓ Connected to MQTT broker')
|
|
104
106
|
|
|
107
|
+
// Emit connected event
|
|
108
|
+
this.emit('connected')
|
|
109
|
+
|
|
105
110
|
// Resubscribe to all devices after reconnection
|
|
106
111
|
this.resubscribeDevices()
|
|
107
112
|
|
|
@@ -110,7 +115,13 @@ export class YotoMqtt {
|
|
|
110
115
|
|
|
111
116
|
this.client.on('error', (error) => {
|
|
112
117
|
this.log.error(LOG_PREFIX.MQTT, 'Connection error:', error)
|
|
113
|
-
this.log.error(LOG_PREFIX.MQTT, 'Error
|
|
118
|
+
this.log.error(LOG_PREFIX.MQTT, 'Error message:', error.message)
|
|
119
|
+
const errorWithCode = /** @type {any} */ (error)
|
|
120
|
+
this.log.error(LOG_PREFIX.MQTT, 'Error code:', errorWithCode.code)
|
|
121
|
+
this.log.error(LOG_PREFIX.MQTT, 'Error stack:', error.stack)
|
|
122
|
+
if (errorWithCode.code) {
|
|
123
|
+
this.log.error(LOG_PREFIX.MQTT, `AWS IoT error code: ${errorWithCode.code}`)
|
|
124
|
+
}
|
|
114
125
|
if (!this.connected) {
|
|
115
126
|
reject(error)
|
|
116
127
|
}
|
|
@@ -120,9 +131,14 @@ export class YotoMqtt {
|
|
|
120
131
|
const wasConnected = this.connected
|
|
121
132
|
this.connected = false
|
|
122
133
|
|
|
134
|
+
// Emit disconnected event
|
|
135
|
+
this.emit('disconnected')
|
|
136
|
+
|
|
123
137
|
if (wasConnected) {
|
|
124
138
|
this.log.warn(LOG_PREFIX.MQTT, ERROR_MESSAGES.MQTT_DISCONNECTED)
|
|
125
139
|
this.handleReconnect()
|
|
140
|
+
} else {
|
|
141
|
+
this.log.error(LOG_PREFIX.MQTT, 'Connection closed before establishing connection')
|
|
126
142
|
}
|
|
127
143
|
})
|
|
128
144
|
|
|
@@ -140,7 +156,26 @@ export class YotoMqtt {
|
|
|
140
156
|
|
|
141
157
|
this.client.on('offline', () => {
|
|
142
158
|
this.connected = false
|
|
143
|
-
this.log.warn(LOG_PREFIX.MQTT, 'MQTT client offline')
|
|
159
|
+
this.log.warn(LOG_PREFIX.MQTT, 'MQTT client offline - connection failed or lost')
|
|
160
|
+
|
|
161
|
+
// Emit offline event
|
|
162
|
+
this.emit('offline')
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
this.client.on('end', () => {
|
|
166
|
+
this.log.warn(LOG_PREFIX.MQTT, 'MQTT client ended')
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
this.client.on('disconnect', (packet) => {
|
|
170
|
+
this.log.warn(LOG_PREFIX.MQTT, 'MQTT client disconnected:', packet)
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
this.client.on('packetreceive', (packet) => {
|
|
174
|
+
this.log.debug(LOG_PREFIX.MQTT, 'Packet received:', packet.cmd)
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
this.client.on('packetsend', (packet) => {
|
|
178
|
+
this.log.debug(LOG_PREFIX.MQTT, 'Packet sent:', packet.cmd)
|
|
144
179
|
})
|
|
145
180
|
|
|
146
181
|
this.client.on('message', (topic, message) => {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "homebridge-yoto",
|
|
3
3
|
"description": "Control your Yoto players through Apple HomeKit with real-time MQTT updates",
|
|
4
|
-
"version": "0.0.
|
|
4
|
+
"version": "0.0.10",
|
|
5
5
|
"author": "Bret Comnes <bcomnes@gmail.com> (https://bret.io)",
|
|
6
6
|
"bugs": {
|
|
7
7
|
"url": "https://github.com/bcomnes/homebridge-yoto/issues"
|