homebridge-yoto 0.0.11 → 0.0.13
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/platform.js +93 -2
- package/lib/playerAccessory.js +43 -65
- package/package.json +1 -1
package/lib/platform.js
CHANGED
|
@@ -49,6 +49,13 @@ export class YotoPlatform {
|
|
|
49
49
|
this.storagePath = api.user.storagePath()
|
|
50
50
|
this.tokenFilePath = join(this.storagePath, 'homebridge-yoto-tokens.json')
|
|
51
51
|
|
|
52
|
+
// Track accessory handlers for status updates
|
|
53
|
+
/** @type {Map<string, import('./playerAccessory.js').YotoPlayerAccessory>} */
|
|
54
|
+
this.accessoryHandlers = new Map()
|
|
55
|
+
|
|
56
|
+
// Status polling interval
|
|
57
|
+
this.statusPollInterval = null
|
|
58
|
+
|
|
52
59
|
// Initialize API clients
|
|
53
60
|
this.auth = new YotoAuth(log, this.config.clientId)
|
|
54
61
|
this.yotoApi = new YotoApi(log, this.auth)
|
|
@@ -100,11 +107,71 @@ export class YotoPlatform {
|
|
|
100
107
|
}
|
|
101
108
|
throw error
|
|
102
109
|
}
|
|
110
|
+
|
|
111
|
+
// Start platform-level status polling (every 60 seconds)
|
|
112
|
+
this.startStatusPolling()
|
|
103
113
|
} catch (error) {
|
|
104
114
|
this.log.error(LOG_PREFIX.PLATFORM, 'Initialization failed:', error)
|
|
105
115
|
}
|
|
106
116
|
}
|
|
107
117
|
|
|
118
|
+
/**
|
|
119
|
+
* Start periodic status polling for all devices
|
|
120
|
+
*/
|
|
121
|
+
startStatusPolling () {
|
|
122
|
+
// Poll every 60 seconds
|
|
123
|
+
this.statusPollInterval = setInterval(async () => {
|
|
124
|
+
try {
|
|
125
|
+
await this.checkAllDevicesStatus()
|
|
126
|
+
} catch (error) {
|
|
127
|
+
this.log.error(LOG_PREFIX.PLATFORM, 'Failed to check device status:', error)
|
|
128
|
+
}
|
|
129
|
+
}, 60000)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Stop status polling
|
|
134
|
+
*/
|
|
135
|
+
stopStatusPolling () {
|
|
136
|
+
if (this.statusPollInterval) {
|
|
137
|
+
clearInterval(this.statusPollInterval)
|
|
138
|
+
this.statusPollInterval = null
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Check all devices' online status and notify accessories
|
|
144
|
+
* @returns {Promise<void>}
|
|
145
|
+
*/
|
|
146
|
+
async checkAllDevicesStatus () {
|
|
147
|
+
try {
|
|
148
|
+
// Fetch fresh device list from API (single call for all devices)
|
|
149
|
+
const devices = await this.yotoApi.getDevices()
|
|
150
|
+
|
|
151
|
+
// Update each accessory with fresh device info
|
|
152
|
+
for (const device of devices) {
|
|
153
|
+
const uuid = this.api.hap.uuid.generate(device.deviceId)
|
|
154
|
+
const accessory = this.accessories.get(uuid)
|
|
155
|
+
|
|
156
|
+
if (accessory) {
|
|
157
|
+
const wasOnline = accessory.context.device.online
|
|
158
|
+
const isNowOnline = device.online
|
|
159
|
+
|
|
160
|
+
// Update device info in context
|
|
161
|
+
accessory.context.device = device
|
|
162
|
+
|
|
163
|
+
// Notify accessory handler if status changed
|
|
164
|
+
const handler = this.accessoryHandlers.get(uuid)
|
|
165
|
+
if (handler && wasOnline !== isNowOnline) {
|
|
166
|
+
handler.handleOnlineStatusChange(isNowOnline, wasOnline)
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
} catch (error) {
|
|
171
|
+
this.log.error(LOG_PREFIX.PLATFORM, 'Error checking device status:', error)
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
108
175
|
/**
|
|
109
176
|
* Perform device authorization flow
|
|
110
177
|
* @returns {Promise<void>}
|
|
@@ -286,6 +353,9 @@ export class YotoPlatform {
|
|
|
286
353
|
// Create handler for this accessory
|
|
287
354
|
const handler = new YotoPlayerAccessory(this, typedAccessory)
|
|
288
355
|
|
|
356
|
+
// Track handler for status updates
|
|
357
|
+
this.accessoryHandlers.set(uuid, handler)
|
|
358
|
+
|
|
289
359
|
// Initialize accessory (connect MQTT, etc.)
|
|
290
360
|
handler.initialize().catch(error => {
|
|
291
361
|
this.log.error(LOG_PREFIX.PLATFORM, `Failed to initialize ${device.name}:`, error)
|
|
@@ -312,6 +382,9 @@ export class YotoPlatform {
|
|
|
312
382
|
// Create handler for this accessory
|
|
313
383
|
const handler = new YotoPlayerAccessory(this, typedAccessory)
|
|
314
384
|
|
|
385
|
+
// Track handler for status updates
|
|
386
|
+
this.accessoryHandlers.set(uuid, handler)
|
|
387
|
+
|
|
315
388
|
// Initialize accessory (connect MQTT, etc.)
|
|
316
389
|
handler.initialize().catch(error => {
|
|
317
390
|
this.log.error(LOG_PREFIX.PLATFORM, `Failed to initialize ${device.name}:`, error)
|
|
@@ -341,8 +414,15 @@ export class YotoPlatform {
|
|
|
341
414
|
if (staleAccessories.length > 0) {
|
|
342
415
|
this.api.unregisterPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, staleAccessories)
|
|
343
416
|
|
|
344
|
-
// Remove from tracking map
|
|
417
|
+
// Remove from tracking map and handlers
|
|
345
418
|
for (const accessory of staleAccessories) {
|
|
419
|
+
const handler = this.accessoryHandlers.get(accessory.UUID)
|
|
420
|
+
if (handler) {
|
|
421
|
+
handler.destroy().catch(error => {
|
|
422
|
+
this.log.error(LOG_PREFIX.PLATFORM, 'Error destroying accessory handler:', error)
|
|
423
|
+
})
|
|
424
|
+
this.accessoryHandlers.delete(accessory.UUID)
|
|
425
|
+
}
|
|
346
426
|
this.accessories.delete(accessory.UUID)
|
|
347
427
|
}
|
|
348
428
|
}
|
|
@@ -353,6 +433,17 @@ export class YotoPlatform {
|
|
|
353
433
|
*/
|
|
354
434
|
async shutdown () {
|
|
355
435
|
this.log.info(LOG_PREFIX.PLATFORM, 'Shutting down...')
|
|
356
|
-
|
|
436
|
+
|
|
437
|
+
// Stop status polling
|
|
438
|
+
this.stopStatusPolling()
|
|
439
|
+
|
|
440
|
+
// Cleanup all accessory handlers
|
|
441
|
+
for (const [, handler] of this.accessoryHandlers) {
|
|
442
|
+
try {
|
|
443
|
+
await handler.destroy()
|
|
444
|
+
} catch (error) {
|
|
445
|
+
this.log.error(LOG_PREFIX.PLATFORM, 'Error shutting down accessory:', error)
|
|
446
|
+
}
|
|
447
|
+
}
|
|
357
448
|
}
|
|
358
449
|
}
|
package/lib/playerAccessory.js
CHANGED
|
@@ -111,67 +111,26 @@ export class YotoPlayerAccessory {
|
|
|
111
111
|
*/
|
|
112
112
|
async initialize () {
|
|
113
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)
|
|
114
|
+
// Status polling is handled at platform level
|
|
131
115
|
}
|
|
132
116
|
|
|
133
117
|
/**
|
|
134
|
-
*
|
|
135
|
-
|
|
136
|
-
|
|
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
|
|
118
|
+
* Handle online status change from platform polling
|
|
119
|
+
* @param {boolean} isNowOnline - Current online status
|
|
120
|
+
* @param {boolean} wasOnline - Previous online status
|
|
145
121
|
* @returns {Promise<void>}
|
|
146
122
|
*/
|
|
147
|
-
async
|
|
148
|
-
|
|
149
|
-
|
|
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
|
|
123
|
+
async handleOnlineStatusChange (isNowOnline, wasOnline) {
|
|
124
|
+
// Update device reference from accessory context (platform already updated it)
|
|
125
|
+
this.device = this.accessory.context.device
|
|
160
126
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
this.
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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)
|
|
127
|
+
// Handle state changes
|
|
128
|
+
if (!wasOnline && isNowOnline) {
|
|
129
|
+
this.log.info(LOG_PREFIX.ACCESSORY, `[${this.device.name}] Device came online, connecting MQTT...`)
|
|
130
|
+
await this.connectMqtt()
|
|
131
|
+
} else if (wasOnline && !isNowOnline) {
|
|
132
|
+
this.log.warn(LOG_PREFIX.ACCESSORY, `[${this.device.name}] Device went offline`)
|
|
133
|
+
await this.disconnectMqtt()
|
|
175
134
|
}
|
|
176
135
|
}
|
|
177
136
|
|
|
@@ -485,9 +444,6 @@ export class YotoPlayerAccessory {
|
|
|
485
444
|
async destroy () {
|
|
486
445
|
this.log.debug(LOG_PREFIX.ACCESSORY, `[${this.device.name}] Destroying accessory`)
|
|
487
446
|
|
|
488
|
-
// Stop status polling
|
|
489
|
-
this.stopStatusPolling()
|
|
490
|
-
|
|
491
447
|
// Disconnect MQTT
|
|
492
448
|
await this.disconnectMqtt()
|
|
493
449
|
}
|
|
@@ -829,10 +785,10 @@ export class YotoPlayerAccessory {
|
|
|
829
785
|
* @returns {Promise<CharacteristicValue>}
|
|
830
786
|
*/
|
|
831
787
|
async getVolume () {
|
|
832
|
-
if (!this.currentStatus) {
|
|
788
|
+
if (!this.currentStatus || this.currentStatus.userVolume === undefined) {
|
|
833
789
|
return 50
|
|
834
790
|
}
|
|
835
|
-
return this.currentStatus.userVolume
|
|
791
|
+
return Number(this.currentStatus.userVolume) || 50
|
|
836
792
|
}
|
|
837
793
|
|
|
838
794
|
/**
|
|
@@ -857,10 +813,10 @@ export class YotoPlayerAccessory {
|
|
|
857
813
|
* @returns {Promise<CharacteristicValue>}
|
|
858
814
|
*/
|
|
859
815
|
async getMute () {
|
|
860
|
-
if (!this.currentStatus) {
|
|
816
|
+
if (!this.currentStatus || this.currentStatus.userVolume === undefined) {
|
|
861
817
|
return false
|
|
862
818
|
}
|
|
863
|
-
return this.currentStatus.userVolume === 0
|
|
819
|
+
return Number(this.currentStatus.userVolume) === 0
|
|
864
820
|
}
|
|
865
821
|
|
|
866
822
|
/**
|
|
@@ -893,10 +849,10 @@ export class YotoPlayerAccessory {
|
|
|
893
849
|
* @returns {Promise<CharacteristicValue>}
|
|
894
850
|
*/
|
|
895
851
|
async getBatteryLevel () {
|
|
896
|
-
if (!this.currentStatus) {
|
|
852
|
+
if (!this.currentStatus || this.currentStatus.batteryLevel === undefined) {
|
|
897
853
|
return 100
|
|
898
854
|
}
|
|
899
|
-
return this.currentStatus.batteryLevel
|
|
855
|
+
return Number(this.currentStatus.batteryLevel) || 100
|
|
900
856
|
}
|
|
901
857
|
|
|
902
858
|
/**
|
|
@@ -1571,6 +1527,15 @@ export class YotoPlayerAccessory {
|
|
|
1571
1527
|
}
|
|
1572
1528
|
|
|
1573
1529
|
this.activeContentCardId = cardId
|
|
1530
|
+
|
|
1531
|
+
// Handle no card inserted
|
|
1532
|
+
if (!cardId || cardId === 'none' || cardId === 'null') {
|
|
1533
|
+
this.log.info(LOG_PREFIX.ACCESSORY, `[${this.device.name}] No card inserted`)
|
|
1534
|
+
this.activeContentInfo = null
|
|
1535
|
+
this.accessory.context.activeContentInfo = null
|
|
1536
|
+
return
|
|
1537
|
+
}
|
|
1538
|
+
|
|
1574
1539
|
this.log.info(LOG_PREFIX.ACCESSORY, `[${this.device.name}] Active card changed: ${cardId}`)
|
|
1575
1540
|
|
|
1576
1541
|
try {
|
|
@@ -1599,7 +1564,20 @@ export class YotoPlayerAccessory {
|
|
|
1599
1564
|
// Store in context for persistence
|
|
1600
1565
|
this.accessory.context.activeContentInfo = this.activeContentInfo
|
|
1601
1566
|
} catch (error) {
|
|
1602
|
-
|
|
1567
|
+
// Handle 403 Forbidden (store-bought cards user doesn't own)
|
|
1568
|
+
if (error instanceof Error && error.message.includes('403')) {
|
|
1569
|
+
this.log.info(LOG_PREFIX.ACCESSORY, `[${this.device.name}] Playing card ${cardId} (store content - details not available)`)
|
|
1570
|
+
this.activeContentInfo = null
|
|
1571
|
+
this.accessory.context.activeContentInfo = null
|
|
1572
|
+
} else if (error instanceof Error && error.message.includes('404')) {
|
|
1573
|
+
// Card not found - might be deleted or invalid
|
|
1574
|
+
this.log.warn(LOG_PREFIX.ACCESSORY, `[${this.device.name}] Card ${cardId} not found`)
|
|
1575
|
+
this.activeContentInfo = null
|
|
1576
|
+
this.accessory.context.activeContentInfo = null
|
|
1577
|
+
} else {
|
|
1578
|
+
// Other errors
|
|
1579
|
+
this.log.error(LOG_PREFIX.ACCESSORY, `[${this.device.name}] Failed to fetch content details:`, error)
|
|
1580
|
+
}
|
|
1603
1581
|
}
|
|
1604
1582
|
}
|
|
1605
1583
|
}
|
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.13",
|
|
5
5
|
"author": "Bret Comnes <bcomnes@gmail.com> (https://bret.io)",
|
|
6
6
|
"bugs": {
|
|
7
7
|
"url": "https://github.com/bcomnes/homebridge-yoto/issues"
|