homebridge-yoto 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/lib/constants.js CHANGED
@@ -21,32 +21,33 @@ export const YOTO_OAUTH_DEVICE_CODE_URL = `${YOTO_OAUTH_BASE_URL}/oauth/device/c
21
21
  * MQTT configuration
22
22
  */
23
23
  export const YOTO_MQTT_BROKER_URL = 'wss://aqrphjqbp3u2z-ats.iot.eu-west-2.amazonaws.com'
24
+ export const YOTO_MQTT_AUTH_NAME = 'PublicJWTAuthorizer'
24
25
  export const MQTT_RECONNECT_PERIOD = 5000 // milliseconds
25
26
  export const MQTT_CONNECT_TIMEOUT = 30000 // milliseconds
26
27
 
27
28
  /**
28
29
  * MQTT topic templates
29
30
  */
30
- export const MQTT_TOPIC_DATA_STATUS = '/device/{deviceId}/data/status'
31
- export const MQTT_TOPIC_DATA_EVENTS = '/device/{deviceId}/data/events'
32
- export const MQTT_TOPIC_RESPONSE = '/device/{deviceId}/response'
33
- export const MQTT_TOPIC_COMMAND_STATUS_REQUEST = '/device/{deviceId}/command/status/request'
34
- export const MQTT_TOPIC_COMMAND_EVENTS_REQUEST = '/device/{deviceId}/command/events/request'
35
- export const MQTT_TOPIC_COMMAND_VOLUME_SET = '/device/{deviceId}/command/volume/set'
36
- export const MQTT_TOPIC_COMMAND_CARD_START = '/device/{deviceId}/command/card/start'
37
- export const MQTT_TOPIC_COMMAND_CARD_STOP = '/device/{deviceId}/command/card/stop'
38
- export const MQTT_TOPIC_COMMAND_CARD_PAUSE = '/device/{deviceId}/command/card/pause'
39
- export const MQTT_TOPIC_COMMAND_CARD_RESUME = '/device/{deviceId}/command/card/resume'
40
- export const MQTT_TOPIC_COMMAND_SLEEP_TIMER = '/device/{deviceId}/command/sleep-timer/set'
41
- export const MQTT_TOPIC_COMMAND_AMBIENTS_SET = '/device/{deviceId}/command/ambients/set'
42
- export const MQTT_TOPIC_COMMAND_REBOOT = '/device/{deviceId}/command/reboot'
31
+ export const MQTT_TOPIC_DATA_STATUS = 'device/{deviceId}/status'
32
+ export const MQTT_TOPIC_DATA_EVENTS = 'device/{deviceId}/events'
33
+ export const MQTT_TOPIC_RESPONSE = 'device/{deviceId}/response'
34
+ export const MQTT_TOPIC_COMMAND_STATUS_REQUEST = 'device/{deviceId}/command/status/request'
35
+ export const MQTT_TOPIC_COMMAND_EVENTS_REQUEST = 'device/{deviceId}/command/events/request'
36
+ export const MQTT_TOPIC_COMMAND_VOLUME_SET = 'device/{deviceId}/command/volume/set'
37
+ export const MQTT_TOPIC_COMMAND_CARD_START = 'device/{deviceId}/command/card/start'
38
+ export const MQTT_TOPIC_COMMAND_CARD_STOP = 'device/{deviceId}/command/card/stop'
39
+ export const MQTT_TOPIC_COMMAND_CARD_PAUSE = 'device/{deviceId}/command/card/pause'
40
+ export const MQTT_TOPIC_COMMAND_CARD_RESUME = 'device/{deviceId}/command/card/resume'
41
+ export const MQTT_TOPIC_COMMAND_SLEEP_TIMER = 'device/{deviceId}/command/sleep-timer/set'
42
+ export const MQTT_TOPIC_COMMAND_AMBIENTS_SET = 'device/{deviceId}/command/ambients/set'
43
+ export const MQTT_TOPIC_COMMAND_REBOOT = 'device/{deviceId}/command/reboot'
43
44
 
44
45
  /**
45
46
  * OAuth configuration
46
47
  */
47
48
  export const OAUTH_CLIENT_ID = 'Y4HJ8BFqRQ24GQoLzgOzZ2KSqWmFG8LI'
48
49
  export const OAUTH_AUDIENCE = 'https://api.yotoplay.com'
49
- export const OAUTH_SCOPE = 'profile offline_access'
50
+ export const OAUTH_SCOPE = 'openid profile offline_access'
50
51
  export const OAUTH_POLLING_INTERVAL = 5000 // milliseconds
51
52
  export const OAUTH_DEVICE_CODE_TIMEOUT = 300000 // 5 minutes
52
53
 
package/lib/platform.js CHANGED
@@ -5,9 +5,10 @@
5
5
  /** @import { API, DynamicPlatformPlugin, Logger, PlatformAccessory, PlatformConfig } from 'homebridge' */
6
6
  /** @import { YotoDevice, YotoPlatformConfig, YotoAccessoryContext } from './types.js' */
7
7
 
8
+ import { readFile, writeFile, mkdir } from 'fs/promises'
9
+ import { join } from 'path'
8
10
  import { YotoAuth } from './auth.js'
9
11
  import { YotoApi } from './yotoApi.js'
10
- import { YotoMqtt } from './yotoMqtt.js'
11
12
  import { YotoPlayerAccessory } from './playerAccessory.js'
12
13
  import {
13
14
  PLATFORM_NAME,
@@ -44,10 +45,13 @@ export class YotoPlatform {
44
45
  /** @type {string[]} */
45
46
  this.discoveredUUIDs = []
46
47
 
48
+ // Persistent storage path for tokens
49
+ this.storagePath = api.user.storagePath()
50
+ this.tokenFilePath = join(this.storagePath, 'homebridge-yoto-tokens.json')
51
+
47
52
  // Initialize API clients
48
53
  this.auth = new YotoAuth(log, this.config.clientId)
49
54
  this.yotoApi = new YotoApi(log, this.auth)
50
- this.yotoMqtt = new YotoMqtt(log)
51
55
 
52
56
  // Set up token refresh callback
53
57
  this.yotoApi.setTokenRefreshCallback(this.handleTokenRefresh.bind(this))
@@ -69,25 +73,12 @@ export class YotoPlatform {
69
73
  */
70
74
  async initialize () {
71
75
  try {
76
+ // Load tokens from persistent storage
77
+ await this.loadTokens()
78
+
72
79
  // Check if we have stored credentials
73
80
  if (!this.config.accessToken || !this.config.refreshToken) {
74
- this.log.warn(LOG_PREFIX.PLATFORM, ERROR_MESSAGES.NO_AUTH)
75
- this.log.info(LOG_PREFIX.PLATFORM, 'Starting OAuth flow...')
76
-
77
- const tokenResponse = await this.auth.authorize()
78
-
79
- // Store tokens in config
80
- this.config.accessToken = tokenResponse.access_token
81
- this.config.refreshToken = tokenResponse.refresh_token || ''
82
- this.config.tokenExpiresAt = this.auth.calculateExpiresAt(tokenResponse.expires_in)
83
-
84
- this.log.info(LOG_PREFIX.PLATFORM, '✓ Authentication successful!')
85
- this.log.warn(LOG_PREFIX.PLATFORM, 'IMPORTANT: Please update your Homebridge config with the following:')
86
- this.log.warn(LOG_PREFIX.PLATFORM, JSON.stringify({
87
- accessToken: this.config.accessToken,
88
- refreshToken: this.config.refreshToken,
89
- tokenExpiresAt: this.config.tokenExpiresAt
90
- }, null, 2))
81
+ await this.performDeviceFlow()
91
82
  }
92
83
 
93
84
  // Set tokens in API client
@@ -97,16 +88,61 @@ export class YotoPlatform {
97
88
  this.config.tokenExpiresAt || 0
98
89
  )
99
90
 
100
- // Connect to MQTT
101
- await this.yotoMqtt.connect(this.config.accessToken)
102
-
103
91
  // Discover and register devices
104
- await this.discoverDevices()
92
+ try {
93
+ await this.discoverDevices()
94
+ } catch (error) {
95
+ // Check if this is an auth error that requires re-authentication
96
+ if (error instanceof Error && error.message.includes('TOKEN_REFRESH_FAILED')) {
97
+ this.log.warn(LOG_PREFIX.PLATFORM, 'Token refresh failed, clearing tokens and restarting auth flow...')
98
+ await this.clearTokensAndReauth()
99
+ return
100
+ }
101
+ throw error
102
+ }
105
103
  } catch (error) {
106
104
  this.log.error(LOG_PREFIX.PLATFORM, 'Initialization failed:', error)
107
105
  }
108
106
  }
109
107
 
108
+ /**
109
+ * Perform device authorization flow
110
+ * @returns {Promise<void>}
111
+ */
112
+ async performDeviceFlow () {
113
+ this.log.warn(LOG_PREFIX.PLATFORM, ERROR_MESSAGES.NO_AUTH)
114
+ this.log.info(LOG_PREFIX.PLATFORM, 'Starting OAuth flow...')
115
+
116
+ const tokenResponse = await this.auth.authorize()
117
+
118
+ // Store tokens in config
119
+ this.config.accessToken = tokenResponse.access_token
120
+ this.config.refreshToken = tokenResponse.refresh_token || ''
121
+ this.config.tokenExpiresAt = this.auth.calculateExpiresAt(tokenResponse.expires_in)
122
+
123
+ // Save tokens to persistent storage
124
+ await this.saveTokens()
125
+
126
+ this.log.info(LOG_PREFIX.PLATFORM, '✓ Authentication successful and saved!')
127
+ }
128
+
129
+ /**
130
+ * Clear invalid tokens and restart authentication
131
+ * @returns {Promise<void>}
132
+ */
133
+ async clearTokensAndReauth () {
134
+ // Clear tokens from config
135
+ this.config.accessToken = ''
136
+ this.config.refreshToken = ''
137
+ this.config.tokenExpiresAt = 0
138
+
139
+ // Save cleared tokens
140
+ await this.saveTokens()
141
+
142
+ // Restart initialization
143
+ await this.initialize()
144
+ }
145
+
110
146
  /**
111
147
  * Handle token refresh - update config
112
148
  * @param {string} accessToken - New access token
@@ -114,17 +150,69 @@ export class YotoPlatform {
114
150
  * @param {number} expiresAt - New expiration timestamp
115
151
  */
116
152
  handleTokenRefresh (accessToken, refreshToken, expiresAt) {
117
- this.log.info(LOG_PREFIX.PLATFORM, 'Token refreshed, please update your config')
153
+ this.log.info(LOG_PREFIX.PLATFORM, 'Token refreshed')
118
154
  this.config.accessToken = accessToken
119
155
  this.config.refreshToken = refreshToken
120
156
  this.config.tokenExpiresAt = expiresAt
121
157
 
122
- // Update MQTT connection with new token
123
- this.yotoMqtt.disconnect().then(() => {
124
- return this.yotoMqtt.connect(accessToken)
125
- }).catch(error => {
126
- this.log.error(LOG_PREFIX.PLATFORM, 'Failed to reconnect MQTT after token refresh:', error)
158
+ // Save updated tokens to persistent storage
159
+ this.saveTokens().catch(error => {
160
+ this.log.error(LOG_PREFIX.PLATFORM, 'Failed to save refreshed tokens:', error)
127
161
  })
162
+
163
+ // Note: MQTT reconnection is handled by each accessory's own MQTT client
164
+ }
165
+
166
+ /**
167
+ * Load tokens from config or persistent storage
168
+ * Priority: config.json > persistent storage file
169
+ * @returns {Promise<void>}
170
+ */
171
+ async loadTokens () {
172
+ // First check if tokens are in config.json
173
+ if (this.config.accessToken && this.config.refreshToken) {
174
+ this.log.debug(LOG_PREFIX.PLATFORM, 'Using tokens from config.json')
175
+ return
176
+ }
177
+
178
+ // Fall back to persistent storage file
179
+ try {
180
+ const data = await readFile(this.tokenFilePath, 'utf-8')
181
+ const tokens = JSON.parse(data)
182
+
183
+ if (tokens.accessToken && tokens.refreshToken) {
184
+ this.config.accessToken = tokens.accessToken
185
+ this.config.refreshToken = tokens.refreshToken
186
+ this.config.tokenExpiresAt = tokens.tokenExpiresAt
187
+ this.log.debug(LOG_PREFIX.PLATFORM, 'Loaded tokens from persistent storage')
188
+ }
189
+ } catch (error) {
190
+ // File doesn't exist or is invalid - not an error on first run
191
+ this.log.debug(LOG_PREFIX.PLATFORM, 'No saved tokens found in storage')
192
+ }
193
+ }
194
+
195
+ /**
196
+ * Save tokens to persistent storage
197
+ * @returns {Promise<void>}
198
+ */
199
+ async saveTokens () {
200
+ try {
201
+ // Ensure storage directory exists
202
+ await mkdir(this.storagePath, { recursive: true })
203
+
204
+ const tokens = {
205
+ accessToken: this.config.accessToken || '',
206
+ refreshToken: this.config.refreshToken || '',
207
+ tokenExpiresAt: this.config.tokenExpiresAt || 0
208
+ }
209
+
210
+ await writeFile(this.tokenFilePath, JSON.stringify(tokens, null, 2), 'utf-8')
211
+ this.log.debug(LOG_PREFIX.PLATFORM, 'Saved tokens to persistent storage')
212
+ } catch (error) {
213
+ this.log.error(LOG_PREFIX.PLATFORM, 'Failed to save tokens:', error)
214
+ throw error
215
+ }
128
216
  }
129
217
 
130
218
  /**
@@ -196,8 +284,12 @@ export class YotoPlatform {
196
284
  this.api.updatePlatformAccessories([existingAccessory])
197
285
 
198
286
  // Create handler for this accessory
199
- // eslint-disable-next-line no-new
200
- new YotoPlayerAccessory(this, typedAccessory)
287
+ const handler = new YotoPlayerAccessory(this, typedAccessory)
288
+
289
+ // Initialize accessory (connect MQTT, etc.)
290
+ handler.initialize().catch(error => {
291
+ this.log.error(LOG_PREFIX.PLATFORM, `Failed to initialize ${device.name}:`, error)
292
+ })
201
293
  } else {
202
294
  // Create new accessory
203
295
  this.log.info(LOG_PREFIX.PLATFORM, 'Adding new accessory:', device.name)
@@ -218,8 +310,12 @@ export class YotoPlatform {
218
310
  }
219
311
 
220
312
  // Create handler for this accessory
221
- // eslint-disable-next-line no-new
222
- new YotoPlayerAccessory(this, typedAccessory)
313
+ const handler = new YotoPlayerAccessory(this, typedAccessory)
314
+
315
+ // Initialize accessory (connect MQTT, etc.)
316
+ handler.initialize().catch(error => {
317
+ this.log.error(LOG_PREFIX.PLATFORM, `Failed to initialize ${device.name}:`, error)
318
+ })
223
319
 
224
320
  // Register with Homebridge
225
321
  this.api.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [accessory])
@@ -257,11 +353,6 @@ export class YotoPlatform {
257
353
  */
258
354
  async shutdown () {
259
355
  this.log.info(LOG_PREFIX.PLATFORM, 'Shutting down...')
260
-
261
- try {
262
- await this.yotoMqtt.disconnect()
263
- } catch (error) {
264
- this.log.error(LOG_PREFIX.PLATFORM, 'Error during shutdown:', error)
265
- }
356
+ // MQTT cleanup is handled by individual accessories
266
357
  }
267
358
  }
@@ -6,6 +6,7 @@
6
6
  /** @import { YotoPlatform } from './platform.js' */
7
7
  /** @import { YotoAccessoryContext, YotoDeviceStatus, YotoPlaybackEvents } from './types.js' */
8
8
 
9
+ import { YotoMqtt } from './yotoMqtt.js'
9
10
  import {
10
11
  DEFAULT_MANUFACTURER,
11
12
  DEFAULT_MODEL,
@@ -37,6 +38,9 @@ export class YotoPlayerAccessory {
37
38
  this.currentEvents = accessory.context.lastEvents || null
38
39
  this.lastUpdateTime = Date.now()
39
40
 
41
+ // Create dedicated MQTT client for this device
42
+ this.mqtt = new YotoMqtt(this.log)
43
+
40
44
  this.log.debug(LOG_PREFIX.ACCESSORY, `[${this.device.name}] Initializing accessory`)
41
45
 
42
46
  // Set up services
@@ -93,8 +97,16 @@ export class YotoPlayerAccessory {
93
97
  this.activeContentInfo = null
94
98
  }
95
99
 
96
- // Subscribe to MQTT updates
97
- this.subscribeMqtt()
100
+ // MQTT connection will be initiated by platform after construction
101
+ }
102
+
103
+ /**
104
+ * Initialize the accessory - connect MQTT and subscribe to updates
105
+ * Called by platform after construction
106
+ * @returns {Promise<void>}
107
+ */
108
+ async initialize () {
109
+ await this.connectMqtt()
98
110
  }
99
111
 
100
112
  /**
@@ -386,17 +398,32 @@ export class YotoPlayerAccessory {
386
398
  }
387
399
 
388
400
  /**
389
- * Subscribe to MQTT updates for this device
401
+ * Connect MQTT for this device
390
402
  */
391
- async subscribeMqtt () {
403
+ async connectMqtt () {
392
404
  try {
393
- await this.platform.yotoMqtt.subscribeToDevice(this.device.deviceId, {
405
+ // Ensure we have an access token
406
+ if (!this.platform.config.accessToken) {
407
+ this.log.warn(LOG_PREFIX.ACCESSORY, `[${this.device.name}] No access token available for MQTT connection`)
408
+ return
409
+ }
410
+
411
+ // Connect MQTT with device ID and access token
412
+ await this.mqtt.connect(
413
+ this.platform.config.accessToken,
414
+ this.device.deviceId
415
+ )
416
+
417
+ // Subscribe to device topics
418
+ await this.mqtt.subscribeToDevice(this.device.deviceId, {
394
419
  onStatus: this.handleStatusUpdate.bind(this),
395
420
  onEvents: this.handleEventsUpdate.bind(this),
396
421
  onResponse: this.handleCommandResponse.bind(this)
397
422
  })
423
+
424
+ this.log.debug(LOG_PREFIX.ACCESSORY, `[${this.device.name}] MQTT connected and subscribed`)
398
425
  } catch (error) {
399
- this.log.error(LOG_PREFIX.ACCESSORY, `[${this.device.name}] Failed to subscribe to MQTT:`, error)
426
+ this.log.error(LOG_PREFIX.ACCESSORY, `[${this.device.name}] Failed to connect MQTT:`, error)
400
427
  }
401
428
  }
402
429
 
@@ -657,11 +684,11 @@ export class YotoPlayerAccessory {
657
684
 
658
685
  try {
659
686
  if (value === this.platform.Characteristic.TargetMediaState.PLAY) {
660
- await this.platform.yotoMqtt.resumeCard(this.device.deviceId)
687
+ await this.mqtt.resumeCard(this.device.deviceId)
661
688
  } else if (value === this.platform.Characteristic.TargetMediaState.PAUSE) {
662
- await this.platform.yotoMqtt.pauseCard(this.device.deviceId)
689
+ await this.mqtt.pauseCard(this.device.deviceId)
663
690
  } else if (value === this.platform.Characteristic.TargetMediaState.STOP) {
664
- await this.platform.yotoMqtt.stopCard(this.device.deviceId)
691
+ await this.mqtt.stopCard(this.device.deviceId)
665
692
  }
666
693
  } catch (error) {
667
694
  this.log.error(LOG_PREFIX.ACCESSORY, `[${this.device.name}] Failed to set media state:`, error)
@@ -690,7 +717,7 @@ export class YotoPlayerAccessory {
690
717
  this.log.debug(LOG_PREFIX.ACCESSORY, `[${this.device.name}] Set volume:`, value)
691
718
 
692
719
  try {
693
- await this.platform.yotoMqtt.setVolume(this.device.deviceId, Number(value))
720
+ await this.mqtt.setVolume(this.device.deviceId, Number(value))
694
721
  } catch (error) {
695
722
  this.log.error(LOG_PREFIX.ACCESSORY, `[${this.device.name}] Failed to set volume:`, error)
696
723
  throw new this.platform.api.hap.HapStatusError(
@@ -720,12 +747,12 @@ export class YotoPlayerAccessory {
720
747
  try {
721
748
  if (value) {
722
749
  // Mute - set volume to 0
723
- await this.platform.yotoMqtt.setVolume(this.device.deviceId, 0)
750
+ await this.mqtt.setVolume(this.device.deviceId, 0)
724
751
  } else {
725
752
  // Unmute - restore to a reasonable volume if currently 0
726
753
  const currentVolume = this.currentStatus?.userVolume || 0
727
754
  const targetVolume = currentVolume === 0 ? 50 : currentVolume
728
- await this.platform.yotoMqtt.setVolume(this.device.deviceId, targetVolume)
755
+ await this.mqtt.setVolume(this.device.deviceId, targetVolume)
729
756
  }
730
757
  } catch (error) {
731
758
  this.log.error(LOG_PREFIX.ACCESSORY, `[${this.device.name}] Failed to set mute:`, error)
@@ -1014,10 +1041,10 @@ export class YotoPlayerAccessory {
1014
1041
  try {
1015
1042
  if (value === this.platform.Characteristic.Active.INACTIVE) {
1016
1043
  // Turn off timer
1017
- await this.platform.yotoMqtt.setSleepTimer(this.device.deviceId, 0)
1044
+ await this.mqtt.setSleepTimer(this.device.deviceId, 0)
1018
1045
  } else {
1019
1046
  // Turn on with default duration (30 minutes)
1020
- await this.platform.yotoMqtt.setSleepTimer(this.device.deviceId, 30 * 60)
1047
+ await this.mqtt.setSleepTimer(this.device.deviceId, 30 * 60)
1021
1048
  }
1022
1049
  } catch (error) {
1023
1050
  this.log.error(LOG_PREFIX.ACCESSORY, `[${this.device.name}] Failed to set sleep timer active:`, error)
@@ -1056,10 +1083,10 @@ export class YotoPlayerAccessory {
1056
1083
 
1057
1084
  if (seconds === 0) {
1058
1085
  // Turn off timer
1059
- await this.platform.yotoMqtt.setSleepTimer(this.device.deviceId, 0)
1086
+ await this.mqtt.setSleepTimer(this.device.deviceId, 0)
1060
1087
  } else {
1061
1088
  // Set timer with specified duration
1062
- await this.platform.yotoMqtt.setSleepTimer(this.device.deviceId, seconds)
1089
+ await this.mqtt.setSleepTimer(this.device.deviceId, seconds)
1063
1090
  }
1064
1091
  } catch (error) {
1065
1092
  this.log.error(LOG_PREFIX.ACCESSORY, `[${this.device.name}] Failed to set sleep timer:`, error)
@@ -1205,10 +1232,10 @@ export class YotoPlayerAccessory {
1205
1232
  try {
1206
1233
  if (value) {
1207
1234
  // Turn on - set to white or previous color
1208
- await this.platform.yotoMqtt.setAmbientLight(this.device.deviceId, 255, 255, 255)
1235
+ await this.mqtt.setAmbientLight(this.device.deviceId, 255, 255, 255)
1209
1236
  } else {
1210
1237
  // Turn off - set to black
1211
- await this.platform.yotoMqtt.setAmbientLight(this.device.deviceId, 0, 0, 0)
1238
+ await this.mqtt.setAmbientLight(this.device.deviceId, 0, 0, 0)
1212
1239
  }
1213
1240
  } catch (error) {
1214
1241
  this.log.error(LOG_PREFIX.ACCESSORY, `[${this.device.name}] Failed to set ambient light on:`, error)
@@ -1318,7 +1345,7 @@ export class YotoPlayerAccessory {
1318
1345
  const { r, g, b } = this.hsvToRgb(h, s, v)
1319
1346
 
1320
1347
  // Send MQTT command
1321
- await this.platform.yotoMqtt.setAmbientLight(this.device.deviceId, r, g, b)
1348
+ await this.mqtt.setAmbientLight(this.device.deviceId, r, g, b)
1322
1349
 
1323
1350
  // Clear pending values
1324
1351
  this.pendingAmbientHue = undefined
package/lib/yotoApi.js CHANGED
@@ -81,7 +81,8 @@ export class YotoApi {
81
81
  }
82
82
  } catch (error) {
83
83
  this.log.error(LOG_PREFIX.API, 'Token refresh failed:', error)
84
- throw error
84
+ // Throw specific error so platform can detect and restart auth flow
85
+ throw new Error('TOKEN_REFRESH_FAILED')
85
86
  }
86
87
  }
87
88
  }
package/lib/yotoMqtt.js CHANGED
@@ -8,6 +8,7 @@
8
8
  import mqtt from 'mqtt'
9
9
  import {
10
10
  YOTO_MQTT_BROKER_URL,
11
+ YOTO_MQTT_AUTH_NAME,
11
12
  MQTT_RECONNECT_PERIOD,
12
13
  MQTT_CONNECT_TIMEOUT,
13
14
  MQTT_TOPIC_DATA_STATUS,
@@ -51,9 +52,10 @@ export class YotoMqtt {
51
52
  /**
52
53
  * Connect to MQTT broker
53
54
  * @param {string} accessToken - Yoto access token for authentication
55
+ * @param {string} deviceId - Device ID for MQTT client identification
54
56
  * @returns {Promise<void>}
55
57
  */
56
- async connect (accessToken) {
58
+ async connect (accessToken, deviceId) {
57
59
  if (this.client) {
58
60
  this.log.warn(LOG_PREFIX.MQTT, 'Already connected, disconnecting first...')
59
61
  await this.disconnect()
@@ -62,12 +64,19 @@ export class YotoMqtt {
62
64
  return new Promise((resolve, reject) => {
63
65
  this.log.info(LOG_PREFIX.MQTT, `Connecting to ${this.brokerUrl}...`)
64
66
 
67
+ const clientId = `homebridge-yoto-${deviceId}-${Date.now()}`
68
+
65
69
  this.client = mqtt.connect(this.brokerUrl, {
66
- username: accessToken,
67
- password: '',
70
+ keepalive: 300,
71
+ port: 443,
72
+ protocol: 'wss',
73
+ username: `${deviceId}?x-amz-customauthorizer-name=${YOTO_MQTT_AUTH_NAME}`,
74
+ password: accessToken,
68
75
  reconnectPeriod: MQTT_RECONNECT_PERIOD,
69
76
  connectTimeout: MQTT_CONNECT_TIMEOUT,
70
- clean: true
77
+ clientId,
78
+ clean: true,
79
+ ALPNProtocols: ['x-amzn-mqtt-ca']
71
80
  })
72
81
 
73
82
  this.client.on('connect', () => {
@@ -319,10 +328,10 @@ export class YotoMqtt {
319
328
  return
320
329
  }
321
330
 
322
- if (topic.includes('/data/status')) {
331
+ if (topic.includes('/status')) {
323
332
  this.log.debug(LOG_PREFIX.MQTT, `Status update for ${deviceId}`)
324
333
  callbacks.onStatus?.(payload)
325
- } else if (topic.includes('/data/events')) {
334
+ } else if (topic.includes('/events')) {
326
335
  this.log.debug(LOG_PREFIX.MQTT, `Events update for ${deviceId}`)
327
336
  callbacks.onEvents?.(payload)
328
337
  } else if (topic.includes('/response')) {
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.6",
4
+ "version": "0.0.8",
5
5
  "author": "Bret Comnes <bcomnes@gmail.com> (https://bret.io)",
6
6
  "bugs": {
7
7
  "url": "https://github.com/bcomnes/homebridge-yoto/issues"