homebridge-yoto 0.0.8 → 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 +126 -0
- package/lib/yotoMqtt.js +59 -6
- 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,17 +479,43 @@ 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`)
|
|
408
509
|
return
|
|
409
510
|
}
|
|
410
511
|
|
|
512
|
+
// TEMPORARY: Debug logging for MQTT troubleshooting
|
|
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}`)
|
|
515
|
+
this.log.warn(LOG_PREFIX.ACCESSORY, ` Device ID: ${this.device.deviceId}`)
|
|
516
|
+
this.log.warn(LOG_PREFIX.ACCESSORY, ` Access Token: ${this.platform.config.accessToken}`)
|
|
517
|
+
this.log.warn(LOG_PREFIX.ACCESSORY, ` Token Length: ${this.platform.config.accessToken.length}`)
|
|
518
|
+
|
|
411
519
|
// Connect MQTT with device ID and access token
|
|
412
520
|
await this.mqtt.connect(
|
|
413
521
|
this.platform.config.accessToken,
|
|
@@ -421,8 +529,26 @@ export class YotoPlayerAccessory {
|
|
|
421
529
|
onResponse: this.handleCommandResponse.bind(this)
|
|
422
530
|
})
|
|
423
531
|
|
|
532
|
+
this.mqttConnected = true
|
|
424
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
|
+
})
|
|
425
550
|
} catch (error) {
|
|
551
|
+
this.mqttConnected = false
|
|
426
552
|
this.log.error(LOG_PREFIX.ACCESSORY, `[${this.device.name}] Failed to connect MQTT:`, error)
|
|
427
553
|
}
|
|
428
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,18 +67,34 @@ 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}`
|
|
71
|
+
const username = `${deviceId}?x-amz-customauthorizer-name=${YOTO_MQTT_AUTH_NAME}`
|
|
72
|
+
|
|
73
|
+
// TEMPORARY: Detailed debug logging for MQTT troubleshooting
|
|
74
|
+
this.log.warn(LOG_PREFIX.MQTT, '='.repeat(60))
|
|
75
|
+
this.log.warn(LOG_PREFIX.MQTT, 'MQTT CONNECTION DETAILS')
|
|
76
|
+
this.log.warn(LOG_PREFIX.MQTT, '='.repeat(60))
|
|
77
|
+
this.log.warn(LOG_PREFIX.MQTT, `Broker URL: ${this.brokerUrl}`)
|
|
78
|
+
this.log.warn(LOG_PREFIX.MQTT, `Client ID: ${clientId}`)
|
|
79
|
+
this.log.warn(LOG_PREFIX.MQTT, `Username: ${username}`)
|
|
80
|
+
this.log.warn(LOG_PREFIX.MQTT, `Device ID: ${deviceId}`)
|
|
81
|
+
this.log.warn(LOG_PREFIX.MQTT, `Token: ${accessToken}`)
|
|
82
|
+
this.log.warn(LOG_PREFIX.MQTT, `Token length: ${accessToken.length}`)
|
|
83
|
+
this.log.warn(LOG_PREFIX.MQTT, 'Port: 443')
|
|
84
|
+
this.log.warn(LOG_PREFIX.MQTT, 'Protocol: wss')
|
|
85
|
+
this.log.warn(LOG_PREFIX.MQTT, 'Keepalive: 300')
|
|
86
|
+
this.log.warn(LOG_PREFIX.MQTT, `Connect Timeout: ${MQTT_CONNECT_TIMEOUT}ms`)
|
|
87
|
+
this.log.warn(LOG_PREFIX.MQTT, '='.repeat(60))
|
|
68
88
|
|
|
69
89
|
this.client = mqtt.connect(this.brokerUrl, {
|
|
70
90
|
keepalive: 300,
|
|
71
91
|
port: 443,
|
|
72
92
|
protocol: 'wss',
|
|
73
|
-
username
|
|
93
|
+
username,
|
|
74
94
|
password: accessToken,
|
|
75
|
-
reconnectPeriod:
|
|
95
|
+
reconnectPeriod: 0, // Disable auto-reconnect - we'll handle reconnection manually
|
|
76
96
|
connectTimeout: MQTT_CONNECT_TIMEOUT,
|
|
77
97
|
clientId,
|
|
78
|
-
clean: true,
|
|
79
98
|
ALPNProtocols: ['x-amzn-mqtt-ca']
|
|
80
99
|
})
|
|
81
100
|
|
|
@@ -85,6 +104,9 @@ export class YotoMqtt {
|
|
|
85
104
|
this.reconnectDelay = MQTT_RECONNECT_PERIOD
|
|
86
105
|
this.log.info(LOG_PREFIX.MQTT, '✓ Connected to MQTT broker')
|
|
87
106
|
|
|
107
|
+
// Emit connected event
|
|
108
|
+
this.emit('connected')
|
|
109
|
+
|
|
88
110
|
// Resubscribe to all devices after reconnection
|
|
89
111
|
this.resubscribeDevices()
|
|
90
112
|
|
|
@@ -93,6 +115,13 @@ export class YotoMqtt {
|
|
|
93
115
|
|
|
94
116
|
this.client.on('error', (error) => {
|
|
95
117
|
this.log.error(LOG_PREFIX.MQTT, 'Connection error:', 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
|
+
}
|
|
96
125
|
if (!this.connected) {
|
|
97
126
|
reject(error)
|
|
98
127
|
}
|
|
@@ -102,9 +131,14 @@ export class YotoMqtt {
|
|
|
102
131
|
const wasConnected = this.connected
|
|
103
132
|
this.connected = false
|
|
104
133
|
|
|
134
|
+
// Emit disconnected event
|
|
135
|
+
this.emit('disconnected')
|
|
136
|
+
|
|
105
137
|
if (wasConnected) {
|
|
106
138
|
this.log.warn(LOG_PREFIX.MQTT, ERROR_MESSAGES.MQTT_DISCONNECTED)
|
|
107
139
|
this.handleReconnect()
|
|
140
|
+
} else {
|
|
141
|
+
this.log.error(LOG_PREFIX.MQTT, 'Connection closed before establishing connection')
|
|
108
142
|
}
|
|
109
143
|
})
|
|
110
144
|
|
|
@@ -122,7 +156,26 @@ export class YotoMqtt {
|
|
|
122
156
|
|
|
123
157
|
this.client.on('offline', () => {
|
|
124
158
|
this.connected = false
|
|
125
|
-
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)
|
|
126
179
|
})
|
|
127
180
|
|
|
128
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"
|