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.
@@ -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 = `homebridge-yoto-${deviceId}-${Date.now()}`
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: `${deviceId}?x-amz-customauthorizer-name=${YOTO_MQTT_AUTH_NAME}`,
93
+ username,
74
94
  password: accessToken,
75
- reconnectPeriod: MQTT_RECONNECT_PERIOD,
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.8",
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"