homebridge-yoto 0.0.41 → 0.0.42

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/accessory.js CHANGED
@@ -243,6 +243,11 @@ export class YotoPlayerAccessory {
243
243
  const { Service, Characteristic } = this.#platform
244
244
  const service = this.#accessory.getService(Service.AccessoryInformation) ||
245
245
  this.#accessory.addService(Service.AccessoryInformation)
246
+ const displayName = sanitizeName(this.#accessory.displayName)
247
+ const nameCharacteristic = service.getCharacteristic(Characteristic.Name)
248
+ const configuredCharacteristic = service.getCharacteristic(Characteristic.ConfiguredName)
249
+ const previousName = nameCharacteristic.value
250
+ const configuredName = configuredCharacteristic.value
246
251
 
247
252
  // Build hardware revision from generation and form factor
248
253
  const hardwareRevision = [
@@ -255,11 +260,16 @@ export class YotoPlayerAccessory {
255
260
 
256
261
  // Set standard characteristics
257
262
  service
263
+ .setCharacteristic(Characteristic.Name, displayName)
258
264
  .setCharacteristic(Characteristic.Manufacturer, DEFAULT_MANUFACTURER)
259
265
  .setCharacteristic(Characteristic.Model, model)
260
266
  .setCharacteristic(Characteristic.SerialNumber, this.#device.deviceId)
261
267
  .setCharacteristic(Characteristic.HardwareRevision, hardwareRevision)
262
268
 
269
+ if (typeof configuredName !== 'string' || configuredName === previousName) {
270
+ service.setCharacteristic(Characteristic.ConfiguredName, displayName)
271
+ }
272
+
263
273
  // Set firmware version from live status if available
264
274
  if (this.#deviceModel.status.firmwareVersion) {
265
275
  service.setCharacteristic(
@@ -933,7 +943,9 @@ export class YotoPlayerAccessory {
933
943
  * @returns {Promise<CharacteristicValue>}
934
944
  */
935
945
  async getPlaybackOn () {
936
- return this.#deviceModel.playback.playbackStatus === 'playing'
946
+ const isOn = this.#deviceModel.playback.playbackStatus === 'playing'
947
+ this.#log.debug(LOG_PREFIX.ACCESSORY, `[${this.#device.name}] Get playback switch -> ${isOn}`)
948
+ return isOn
937
949
  }
938
950
 
939
951
  /**
@@ -1033,7 +1045,9 @@ export class YotoPlayerAccessory {
1033
1045
  * @returns {Promise<CharacteristicValue>}
1034
1046
  */
1035
1047
  async getVolumeOn () {
1036
- return this.#deviceModel.status.volume > 0
1048
+ const isOn = this.#deviceModel.status.volume > 0
1049
+ this.#log.debug(LOG_PREFIX.ACCESSORY, `[${this.#device.name}] Get volume on -> ${isOn}`)
1050
+ return isOn
1037
1051
  }
1038
1052
 
1039
1053
  /**
@@ -1064,7 +1078,9 @@ export class YotoPlayerAccessory {
1064
1078
  * @returns {Promise<CharacteristicValue>}
1065
1079
  */
1066
1080
  async getStatusActive () {
1067
- return this.#deviceModel.status.isOnline
1081
+ const isOnline = this.#deviceModel.status.isOnline
1082
+ this.#log.debug(LOG_PREFIX.ACCESSORY, `[${this.#device.name}] Get status active -> ${isOnline}`)
1083
+ return isOnline
1068
1084
  }
1069
1085
 
1070
1086
  /**
@@ -1073,7 +1089,9 @@ export class YotoPlayerAccessory {
1073
1089
  */
1074
1090
  async getOnlineStatus () {
1075
1091
  const { Characteristic } = this.#platform
1076
- return this.#deviceModel.status.isOnline
1092
+ const isOnline = this.#deviceModel.status.isOnline
1093
+ this.#log.debug(LOG_PREFIX.ACCESSORY, `[${this.#device.name}] Get online status -> ${isOnline}`)
1094
+ return isOnline
1077
1095
  ? Characteristic.ContactSensorState.CONTACT_NOT_DETECTED
1078
1096
  : Characteristic.ContactSensorState.CONTACT_DETECTED
1079
1097
  }
@@ -1086,7 +1104,9 @@ export class YotoPlayerAccessory {
1086
1104
  */
1087
1105
  async getBatteryLevel () {
1088
1106
  const battery = this.#deviceModel.status.batteryLevelPercentage
1089
- return Number.isFinite(battery) ? battery : 100
1107
+ const level = Number.isFinite(battery) ? battery : 100
1108
+ this.#log.debug(LOG_PREFIX.ACCESSORY, `[${this.#device.name}] Get battery level -> ${level}`)
1109
+ return level
1090
1110
  }
1091
1111
 
1092
1112
  /**
@@ -1095,6 +1115,7 @@ export class YotoPlayerAccessory {
1095
1115
  */
1096
1116
  async getChargingState () {
1097
1117
  const isCharging = this.#deviceModel.status.isCharging
1118
+ this.#log.debug(LOG_PREFIX.ACCESSORY, `[${this.#device.name}] Get charging state -> ${isCharging}`)
1098
1119
  return isCharging
1099
1120
  ? this.#platform.Characteristic.ChargingState.CHARGING
1100
1121
  : this.#platform.Characteristic.ChargingState.NOT_CHARGING
@@ -1107,6 +1128,7 @@ export class YotoPlayerAccessory {
1107
1128
  async getStatusLowBattery () {
1108
1129
  const battery = this.#deviceModel.status.batteryLevelPercentage
1109
1130
  const batteryLevel = Number.isFinite(battery) ? battery : 100
1131
+ this.#log.debug(LOG_PREFIX.ACCESSORY, `[${this.#device.name}] Get low battery -> ${batteryLevel}`)
1110
1132
  return batteryLevel <= LOW_BATTERY_THRESHOLD
1111
1133
  ? this.#platform.Characteristic.StatusLowBattery.BATTERY_LEVEL_LOW
1112
1134
  : this.#platform.Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL
@@ -1123,9 +1145,11 @@ export class YotoPlayerAccessory {
1123
1145
 
1124
1146
  // Return a default value if temperature is not available
1125
1147
  if (temp === null || temp === 'notSupported') {
1148
+ this.#log.debug(LOG_PREFIX.ACCESSORY, `[${this.#device.name}] Get temperature -> unavailable`)
1126
1149
  return 0
1127
1150
  }
1128
1151
 
1152
+ this.#log.debug(LOG_PREFIX.ACCESSORY, `[${this.#device.name}] Get temperature -> ${temp}`)
1129
1153
  return Number(temp)
1130
1154
  }
1131
1155
 
@@ -1135,13 +1159,18 @@ export class YotoPlayerAccessory {
1135
1159
  */
1136
1160
  async getTemperatureSensorFault () {
1137
1161
  // Report fault if device is offline or temperature is not available
1138
- if (!this.#deviceModel.status.isOnline ||
1139
- this.#deviceModel.status.temperatureCelsius === null ||
1140
- this.#deviceModel.status.temperatureCelsius === 'notSupported') {
1141
- return this.#platform.Characteristic.StatusFault.GENERAL_FAULT
1142
- }
1143
-
1144
- return this.#platform.Characteristic.StatusFault.NO_FAULT
1162
+ const isOffline = !this.#deviceModel.status.isOnline
1163
+ const temp = this.#deviceModel.status.temperatureCelsius
1164
+ const isUnavailable = temp === null || temp === 'notSupported'
1165
+ const isFault = isOffline || isUnavailable
1166
+ const fault = isFault
1167
+ ? this.#platform.Characteristic.StatusFault.GENERAL_FAULT
1168
+ : this.#platform.Characteristic.StatusFault.NO_FAULT
1169
+ this.#log.debug(
1170
+ LOG_PREFIX.ACCESSORY,
1171
+ `[${this.#device.name}] Get temperature sensor fault -> ${isFault} (online=${!isOffline} temp=${temp})`
1172
+ )
1173
+ return fault
1145
1174
  }
1146
1175
 
1147
1176
  // ==================== Nightlight Characteristic Handlers ====================
@@ -1183,7 +1212,9 @@ export class YotoPlayerAccessory {
1183
1212
  */
1184
1213
  async getDayNightlightOn () {
1185
1214
  const color = this.#deviceModel.config.ambientColour
1186
- return !this.isColorOff(color)
1215
+ const isOn = !this.isColorOff(color)
1216
+ this.#log.debug(LOG_PREFIX.ACCESSORY, `[${this.#device.name}] Get day nightlight on -> ${isOn} (${color})`)
1217
+ return isOn
1187
1218
  }
1188
1219
 
1189
1220
  /**
@@ -1220,10 +1251,16 @@ export class YotoPlayerAccessory {
1220
1251
  */
1221
1252
  async getDayNightlightBrightness () {
1222
1253
  const config = this.#deviceModel.config
1223
- if (config.dayDisplayBrightnessAuto || config.dayDisplayBrightness === null) {
1224
- return 100
1225
- }
1226
- return Math.max(0, Math.min(Math.round(config.dayDisplayBrightness), 100))
1254
+ const isAuto = config.dayDisplayBrightnessAuto
1255
+ const raw = config.dayDisplayBrightness
1256
+ const brightness = isAuto || raw === null
1257
+ ? 100
1258
+ : Math.max(0, Math.min(Math.round(raw), 100))
1259
+ this.#log.debug(
1260
+ LOG_PREFIX.ACCESSORY,
1261
+ `[${this.#device.name}] Get day nightlight brightness -> ${brightness} (raw=${raw} auto=${isAuto})`
1262
+ )
1263
+ return brightness
1227
1264
  }
1228
1265
 
1229
1266
  /**
@@ -1260,10 +1297,12 @@ export class YotoPlayerAccessory {
1260
1297
  async getDayNightlightHue () {
1261
1298
  const color = this.#deviceModel.config.ambientColour
1262
1299
  if (this.isColorOff(color)) {
1300
+ this.#log.debug(LOG_PREFIX.ACCESSORY, `[${this.#device.name}] Get day nightlight hue -> 0 (off)`)
1263
1301
  return 0
1264
1302
  }
1265
1303
  const hex = this.parseHexColor(color)
1266
1304
  const [h] = convert.hex.hsv(hex)
1305
+ this.#log.debug(LOG_PREFIX.ACCESSORY, `[${this.#device.name}] Get day nightlight hue -> ${h} (${color})`)
1267
1306
  return h
1268
1307
  }
1269
1308
 
@@ -1311,10 +1350,12 @@ export class YotoPlayerAccessory {
1311
1350
  async getDayNightlightSaturation () {
1312
1351
  const color = this.#deviceModel.config.ambientColour
1313
1352
  if (this.isColorOff(color)) {
1353
+ this.#log.debug(LOG_PREFIX.ACCESSORY, `[${this.#device.name}] Get day nightlight saturation -> 0 (off)`)
1314
1354
  return 0
1315
1355
  }
1316
1356
  const hex = this.parseHexColor(color)
1317
1357
  const [, s] = convert.hex.hsv(hex)
1358
+ this.#log.debug(LOG_PREFIX.ACCESSORY, `[${this.#device.name}] Get day nightlight saturation -> ${s} (${color})`)
1318
1359
  return s
1319
1360
  }
1320
1361
 
@@ -1363,7 +1404,9 @@ export class YotoPlayerAccessory {
1363
1404
  */
1364
1405
  async getNightNightlightOn () {
1365
1406
  const color = this.#deviceModel.config.nightAmbientColour
1366
- return !this.isColorOff(color)
1407
+ const isOn = !this.isColorOff(color)
1408
+ this.#log.debug(LOG_PREFIX.ACCESSORY, `[${this.#device.name}] Get night nightlight on -> ${isOn} (${color})`)
1409
+ return isOn
1367
1410
  }
1368
1411
 
1369
1412
  /**
@@ -1400,10 +1443,16 @@ export class YotoPlayerAccessory {
1400
1443
  */
1401
1444
  async getNightNightlightBrightness () {
1402
1445
  const config = this.#deviceModel.config
1403
- if (config.nightDisplayBrightnessAuto || config.nightDisplayBrightness === null) {
1404
- return 100
1405
- }
1406
- return Math.max(0, Math.min(Math.round(config.nightDisplayBrightness), 100))
1446
+ const isAuto = config.nightDisplayBrightnessAuto
1447
+ const raw = config.nightDisplayBrightness
1448
+ const brightness = isAuto || raw === null
1449
+ ? 100
1450
+ : Math.max(0, Math.min(Math.round(raw), 100))
1451
+ this.#log.debug(
1452
+ LOG_PREFIX.ACCESSORY,
1453
+ `[${this.#device.name}] Get night nightlight brightness -> ${brightness} (raw=${raw} auto=${isAuto})`
1454
+ )
1455
+ return brightness
1407
1456
  }
1408
1457
 
1409
1458
  /**
@@ -1440,10 +1489,12 @@ export class YotoPlayerAccessory {
1440
1489
  async getNightNightlightHue () {
1441
1490
  const color = this.#deviceModel.config.nightAmbientColour
1442
1491
  if (this.isColorOff(color)) {
1492
+ this.#log.debug(LOG_PREFIX.ACCESSORY, `[${this.#device.name}] Get night nightlight hue -> 0 (off)`)
1443
1493
  return 0
1444
1494
  }
1445
1495
  const hex = this.parseHexColor(color)
1446
1496
  const [h] = convert.hex.hsv(hex)
1497
+ this.#log.debug(LOG_PREFIX.ACCESSORY, `[${this.#device.name}] Get night nightlight hue -> ${h} (${color})`)
1447
1498
  return h
1448
1499
  }
1449
1500
 
@@ -1491,10 +1542,12 @@ export class YotoPlayerAccessory {
1491
1542
  async getNightNightlightSaturation () {
1492
1543
  const color = this.#deviceModel.config.nightAmbientColour
1493
1544
  if (this.isColorOff(color)) {
1545
+ this.#log.debug(LOG_PREFIX.ACCESSORY, `[${this.#device.name}] Get night nightlight saturation -> 0 (off)`)
1494
1546
  return 0
1495
1547
  }
1496
1548
  const hex = this.parseHexColor(color)
1497
1549
  const [, s] = convert.hex.hsv(hex)
1550
+ this.#log.debug(LOG_PREFIX.ACCESSORY, `[${this.#device.name}] Get night nightlight saturation -> ${s} (${color})`)
1498
1551
  return s
1499
1552
  }
1500
1553
 
@@ -1545,6 +1598,7 @@ export class YotoPlayerAccessory {
1545
1598
  const { Characteristic } = this.#platform
1546
1599
  const status = this.#deviceModel.status
1547
1600
  const isActive = status.nightlightMode !== 'off'
1601
+ this.#log.debug(LOG_PREFIX.ACCESSORY, `[${this.#device.name}] Get nightlight active -> ${isActive}`)
1548
1602
  return isActive ? Characteristic.ContactSensorState.CONTACT_NOT_DETECTED : Characteristic.ContactSensorState.CONTACT_DETECTED
1549
1603
  }
1550
1604
 
@@ -1558,6 +1612,10 @@ export class YotoPlayerAccessory {
1558
1612
  const isDay = status.dayMode === 'day'
1559
1613
  const isActive = status.nightlightMode !== 'off'
1560
1614
  const isShowing = isDay && isActive
1615
+ this.#log.debug(
1616
+ LOG_PREFIX.ACCESSORY,
1617
+ `[${this.#device.name}] Get day nightlight active -> ${isShowing} (day=${isDay} active=${isActive})`
1618
+ )
1561
1619
  return isShowing ? Characteristic.ContactSensorState.CONTACT_NOT_DETECTED : Characteristic.ContactSensorState.CONTACT_DETECTED
1562
1620
  }
1563
1621
 
@@ -1571,6 +1629,10 @@ export class YotoPlayerAccessory {
1571
1629
  const isNight = status.dayMode === 'night'
1572
1630
  const isActive = status.nightlightMode !== 'off'
1573
1631
  const isShowing = isNight && isActive
1632
+ this.#log.debug(
1633
+ LOG_PREFIX.ACCESSORY,
1634
+ `[${this.#device.name}] Get night nightlight active -> ${isShowing} (night=${isNight} active=${isActive})`
1635
+ )
1574
1636
  return isShowing ? Characteristic.ContactSensorState.CONTACT_NOT_DETECTED : Characteristic.ContactSensorState.CONTACT_DETECTED
1575
1637
  }
1576
1638
 
@@ -1584,6 +1646,7 @@ export class YotoPlayerAccessory {
1584
1646
  const { Characteristic } = this.#platform
1585
1647
  const status = this.#deviceModel.status
1586
1648
  const hasCard = status.cardInsertionState !== 'none'
1649
+ this.#log.debug(LOG_PREFIX.ACCESSORY, `[${this.#device.name}] Get card slot -> ${hasCard} (${status.cardInsertionState})`)
1587
1650
  return hasCard ? Characteristic.ContactSensorState.CONTACT_NOT_DETECTED : Characteristic.ContactSensorState.CONTACT_DETECTED
1588
1651
  }
1589
1652
 
@@ -1597,6 +1660,7 @@ export class YotoPlayerAccessory {
1597
1660
  const { Characteristic } = this.#platform
1598
1661
  const status = this.#deviceModel.status
1599
1662
  const isDayMode = status.dayMode === 'day'
1663
+ this.#log.debug(LOG_PREFIX.ACCESSORY, `[${this.#device.name}] Get day mode -> ${isDayMode}`)
1600
1664
  return isDayMode
1601
1665
  ? Characteristic.ContactSensorState.CONTACT_NOT_DETECTED
1602
1666
  : Characteristic.ContactSensorState.CONTACT_DETECTED
@@ -1610,6 +1674,10 @@ export class YotoPlayerAccessory {
1610
1674
  */
1611
1675
  async getSleepTimerState () {
1612
1676
  const playback = this.#deviceModel.playback
1677
+ this.#log.debug(
1678
+ LOG_PREFIX.ACCESSORY,
1679
+ `[${this.#device.name}] Get sleep timer -> ${playback.sleepTimerActive ?? false}`
1680
+ )
1613
1681
  return playback.sleepTimerActive ?? false
1614
1682
  }
1615
1683
 
@@ -1645,7 +1713,9 @@ export class YotoPlayerAccessory {
1645
1713
  * @returns {Promise<CharacteristicValue>}
1646
1714
  */
1647
1715
  async getBluetoothState () {
1648
- return this.#deviceModel.config.bluetoothEnabled ?? false
1716
+ const enabled = this.#deviceModel.config.bluetoothEnabled ?? false
1717
+ this.#log.debug(LOG_PREFIX.ACCESSORY, `[${this.#device.name}] Get Bluetooth -> ${enabled}`)
1718
+ return enabled
1649
1719
  }
1650
1720
 
1651
1721
  /**
@@ -1677,6 +1747,10 @@ export class YotoPlayerAccessory {
1677
1747
  async setCardControl (service, control, value) {
1678
1748
  const { Characteristic } = this.#platform
1679
1749
  const isOn = Boolean(value)
1750
+ this.#log.debug(
1751
+ LOG_PREFIX.ACCESSORY,
1752
+ `[${this.#device.name}] Set card control: ${control.label} (${control.cardId}) -> ${isOn}`
1753
+ )
1680
1754
 
1681
1755
  if (!isOn) {
1682
1756
  service.getCharacteristic(Characteristic.On).updateValue(false)
@@ -72,13 +72,20 @@ export class YotoCardControlAccessory {
72
72
  this.#accessory.addService(Service.AccessoryInformation)
73
73
 
74
74
  const displayName = sanitizeName(this.#accessory.displayName)
75
+ const nameCharacteristic = service.getCharacteristic(Characteristic.Name)
76
+ const configuredCharacteristic = service.getCharacteristic(Characteristic.ConfiguredName)
77
+ const previousName = nameCharacteristic.value
78
+ const configuredName = configuredCharacteristic.value
75
79
 
76
80
  service
77
81
  .setCharacteristic(Characteristic.Manufacturer, DEFAULT_MANUFACTURER)
78
82
  .setCharacteristic(Characteristic.Model, DEFAULT_MODEL)
79
83
  .setCharacteristic(Characteristic.SerialNumber, this.#cardControl.id)
80
84
  .setCharacteristic(Characteristic.Name, displayName)
81
- .setCharacteristic(Characteristic.ConfiguredName, displayName)
85
+
86
+ if (typeof configuredName !== 'string' || configuredName === previousName) {
87
+ service.setCharacteristic(Characteristic.ConfiguredName, displayName)
88
+ }
82
89
 
83
90
  this.#currentServices.add(service)
84
91
  }
@@ -114,6 +121,10 @@ export class YotoCardControlAccessory {
114
121
  async setCardControl (value) {
115
122
  const { Characteristic } = this.#platform
116
123
  const isOn = Boolean(value)
124
+ this.#log.debug(
125
+ LOG_PREFIX.ACCESSORY,
126
+ `Card control toggle requested: ${this.#cardControl.label} (${this.#cardControl.cardId}) -> ${isOn}`
127
+ )
117
128
 
118
129
  if (!isOn) {
119
130
  this.switchService?.getCharacteristic(Characteristic.On).updateValue(false)
package/lib/platform.js CHANGED
@@ -170,7 +170,7 @@ export class YotoPlatform {
170
170
  */
171
171
  configureAccessory (accessory) {
172
172
  const { log, accessories, cardAccessories } = this
173
- log.debug('Loading accessory from cache:', accessory.displayName)
173
+ log.debug('Loading accessory from cache:', accessory.displayName, accessory.UUID)
174
174
 
175
175
  const context = accessory.context
176
176
  const record = context && typeof context === 'object'
@@ -179,6 +179,7 @@ export class YotoPlatform {
179
179
  const accessoryType = record && typeof record['type'] === 'string'
180
180
  ? record['type']
181
181
  : undefined
182
+ log.debug('Cached accessory context type:', accessory.displayName, accessoryType ?? 'device')
182
183
 
183
184
  if (accessoryType === 'card-control' || record?.['cardControl']) {
184
185
  cardAccessories.set(accessory.UUID, /** @type {PlatformAccessory<YotoCardAccessoryContext>} */ (accessory))
@@ -200,6 +201,7 @@ export class YotoPlatform {
200
201
 
201
202
  try {
202
203
  this.log.debug('Starting Yoto account...')
204
+ this.log.debug('Playback accessory mode:', this.playbackAccessoryConfig.mode)
203
205
 
204
206
  // Listen for devices being added
205
207
  this.yotoAccount.on('deviceAdded', async ({ deviceId }) => {
@@ -212,6 +214,7 @@ export class YotoPlatform {
212
214
 
213
215
  const device = deviceModel.device
214
216
  this.log.info(`Device discovered: ${device.name} (${deviceId})`)
217
+ this.log.debug('Registering device from account discovery:', device.name, deviceId)
215
218
  await this.registerDevice(device, deviceModel)
216
219
  })
217
220
 
@@ -334,10 +337,15 @@ export class YotoPlatform {
334
337
  await this.yotoAccount.start()
335
338
 
336
339
  this.log.info(`✓ Yoto account started with ${this.yotoAccount.devices.size} device(s)`)
340
+ this.log.debug(
341
+ 'Account devices:',
342
+ Array.from(this.yotoAccount.devices.keys()).join(', ') || 'none'
343
+ )
337
344
 
338
345
  // Remove stale accessories after all devices are registered
339
346
  this.removeStaleAccessories()
340
347
 
348
+ this.log.debug('Registering card control accessories (playOnAll).')
341
349
  await this.registerCardControlAccessories()
342
350
  } catch (error) {
343
351
  this.log.error('Failed to start account:', error instanceof Error ? error.message : String(error))
@@ -401,28 +409,24 @@ export class YotoPlatform {
401
409
  const uuid = this.api.hap.uuid.generate(device.deviceId)
402
410
  const sanitizedDeviceName = sanitizeName(device.name)
403
411
  const accessoryCategory = this.api.hap.Categories.SPEAKER
412
+ this.log.debug(
413
+ 'Register device:',
414
+ `${device.name} (${device.deviceId})`,
415
+ `uuid=${uuid}`,
416
+ `category=${accessoryCategory}`
417
+ )
404
418
 
405
419
  // Check if accessory already exists
406
420
  const existingAccessory = this.accessories.get(uuid)
407
421
 
408
422
  if (existingAccessory) {
409
423
  // Accessory exists - update it
410
- this.log.debug('Restoring existing accessory from cache:', device.name)
424
+ this.log.debug('Restoring existing accessory from cache:', device.name, existingAccessory.UUID)
411
425
 
412
426
  // Update display name if it has changed
413
427
  if (existingAccessory.displayName !== sanitizedDeviceName) {
428
+ this.log.debug('Updating accessory display name:', existingAccessory.displayName, '->', sanitizedDeviceName)
414
429
  existingAccessory.updateDisplayName(sanitizedDeviceName)
415
- const infoService = existingAccessory.getService(this.api.hap.Service.AccessoryInformation)
416
- if (infoService) {
417
- // Only update Name, preserve user's ConfiguredName customization
418
- // ConfiguredName is intentionally NOT updated here because:
419
- // - It allows users to rename accessories in the Home app
420
- // - Their custom names should survive Homebridge restarts and Yoto device name changes
421
- // - Name stays in sync with Yoto's device name for plugin identification
422
- infoService
423
- .setCharacteristic(this.api.hap.Characteristic.Name, sanitizedDeviceName)
424
- .setCharacteristic(this.api.hap.Characteristic.ConfiguredName, sanitizedDeviceName)
425
- }
426
430
  }
427
431
 
428
432
  // Update context with fresh device data
@@ -439,6 +443,7 @@ export class YotoPlatform {
439
443
 
440
444
  // Update accessory information
441
445
  this.api.updatePlatformAccessories([existingAccessory])
446
+ this.log.debug('Updated accessory cache entry:', existingAccessory.displayName, existingAccessory.UUID)
442
447
 
443
448
  // Create handler for this accessory with device model
444
449
  const handler = new YotoPlayerAccessory({
@@ -449,9 +454,11 @@ export class YotoPlatform {
449
454
 
450
455
  // Track handler
451
456
  this.accessoryHandlers.set(uuid, handler)
457
+ this.log.debug('Created accessory handler:', existingAccessory.displayName, uuid)
452
458
 
453
459
  // Initialize accessory (setup services and event listeners)
454
460
  await handler.setup()
461
+ this.log.debug('Accessory setup complete:', existingAccessory.displayName)
455
462
 
456
463
  if (this.playbackAccessoryConfig.mode === 'external') {
457
464
  await this.registerSpeakerAccessory(device, deviceModel)
@@ -460,21 +467,13 @@ export class YotoPlatform {
460
467
  return { success: true }
461
468
  } else {
462
469
  // Create new accessory
463
- this.log.debug('Adding new accessory:', device.name)
470
+ this.log.debug('Adding new accessory:', device.name, uuid)
464
471
 
465
472
  // Create platform accessory
466
473
  /** @type {PlatformAccessory<YotoAccessoryContext>} */
467
474
  // eslint-disable-next-line new-cap
468
475
  const accessory = new this.api.platformAccessory(sanitizedDeviceName, uuid, accessoryCategory)
469
476
 
470
- // Set Name and ConfiguredName on AccessoryInformation service
471
- const infoService = accessory.getService(this.api.hap.Service.AccessoryInformation)
472
- if (infoService) {
473
- infoService
474
- .setCharacteristic(this.api.hap.Characteristic.Name, sanitizedDeviceName)
475
- .setCharacteristic(this.api.hap.Characteristic.ConfiguredName, sanitizedDeviceName)
476
- }
477
-
478
477
  // Set accessory context
479
478
  accessory.context = {
480
479
  type: 'device',
@@ -490,13 +489,16 @@ export class YotoPlatform {
490
489
 
491
490
  // Track handler
492
491
  this.accessoryHandlers.set(uuid, handler)
492
+ this.log.debug('Created accessory handler:', device.name, uuid)
493
493
 
494
494
  // Initialize accessory (setup services and event listeners)
495
495
  await handler.setup()
496
+ this.log.debug('Accessory setup complete:', device.name)
496
497
 
497
498
  // Register as a platform accessory (bridged).
498
499
  this.log.debug(`Registering new accessory: ${device.name}`)
499
500
  this.api.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [accessory])
501
+ this.log.debug('Registered platform accessory:', device.name, uuid)
500
502
 
501
503
  if (this.playbackAccessoryConfig.mode === 'external') {
502
504
  await this.registerSpeakerAccessory(device, deviceModel)
@@ -504,6 +506,7 @@ export class YotoPlatform {
504
506
 
505
507
  // Add to our tracking map (cast to typed version)
506
508
  this.accessories.set(uuid, accessory)
509
+ this.log.debug('Tracked new accessory:', device.name, uuid)
507
510
 
508
511
  return { success: true }
509
512
  }
@@ -519,11 +522,12 @@ export class YotoPlatform {
519
522
  const uuid = this.getSpeakerAccessoryUuid(device.deviceId)
520
523
  const speakerName = this.getSpeakerAccessoryName(device)
521
524
  if (this.speakerAccessories.has(uuid)) {
522
- this.log.debug('SmartSpeaker accessory already published:', speakerName)
525
+ this.log.debug('SmartSpeaker accessory already published:', speakerName, uuid)
523
526
  return { success: true }
524
527
  }
525
528
 
526
529
  this.log.info('Adding new SmartSpeaker accessory:', speakerName)
530
+ this.log.debug('Creating SmartSpeaker accessory:', speakerName, uuid)
527
531
 
528
532
  /** @type {PlatformAccessory<YotoAccessoryContext>} */
529
533
  // eslint-disable-next-line new-cap
@@ -533,13 +537,6 @@ export class YotoPlatform {
533
537
  this.api.hap.Categories.SPEAKER
534
538
  )
535
539
 
536
- const infoService = accessory.getService(this.api.hap.Service.AccessoryInformation)
537
- if (infoService) {
538
- infoService
539
- .setCharacteristic(this.api.hap.Characteristic.Name, speakerName)
540
- .setCharacteristic(this.api.hap.Characteristic.ConfiguredName, speakerName)
541
- }
542
-
543
540
  accessory.context = {
544
541
  device,
545
542
  }
@@ -551,11 +548,14 @@ export class YotoPlatform {
551
548
  })
552
549
 
553
550
  this.speakerAccessoryHandlers.set(uuid, handler)
551
+ this.log.debug('Created SmartSpeaker handler:', speakerName, uuid)
554
552
 
555
553
  await handler.setup()
554
+ this.log.debug('SmartSpeaker setup complete:', speakerName)
556
555
 
557
556
  this.log.info(`Publishing external SmartSpeaker accessory: ${speakerName}`)
558
557
  this.api.publishExternalAccessories(PLUGIN_NAME, [accessory])
558
+ this.log.debug('Published external SmartSpeaker accessory:', speakerName, uuid)
559
559
 
560
560
  this.speakerAccessories.set(uuid, accessory)
561
561
 
@@ -569,24 +569,21 @@ export class YotoPlatform {
569
569
  async registerCardControlAccessories () {
570
570
  const cardControls = getCardControlConfigs(this.config).filter(control => control.playOnAll)
571
571
  const desiredUuids = new Set()
572
+ this.log.debug('Card control configs (playOnAll):', cardControls.length)
572
573
 
573
574
  for (const control of cardControls) {
574
575
  const uuid = this.getCardControlAccessoryUuid(control)
575
576
  const accessoryName = this.getCardControlAccessoryName(control)
576
577
  desiredUuids.add(uuid)
578
+ this.log.debug('Ensuring card control accessory:', accessoryName, uuid)
577
579
 
578
580
  const existingAccessory = this.cardAccessories.get(uuid)
579
581
  if (existingAccessory) {
580
- this.log.debug('Restoring existing card control accessory from cache:', accessoryName)
582
+ this.log.debug('Restoring existing card control accessory from cache:', accessoryName, uuid)
581
583
 
582
584
  if (existingAccessory.displayName !== accessoryName) {
585
+ this.log.debug('Updating card control display name:', existingAccessory.displayName, '->', accessoryName)
583
586
  existingAccessory.updateDisplayName(accessoryName)
584
- const infoService = existingAccessory.getService(this.api.hap.Service.AccessoryInformation)
585
- if (infoService) {
586
- infoService
587
- .setCharacteristic(this.api.hap.Characteristic.Name, accessoryName)
588
- .setCharacteristic(this.api.hap.Characteristic.ConfiguredName, accessoryName)
589
- }
590
587
  }
591
588
 
592
589
  existingAccessory.context = {
@@ -595,6 +592,7 @@ export class YotoPlatform {
595
592
  }
596
593
 
597
594
  this.api.updatePlatformAccessories([existingAccessory])
595
+ this.log.debug('Updated card control cache entry:', existingAccessory.displayName, uuid)
598
596
 
599
597
  const existingHandler = this.cardAccessoryHandlers.get(uuid)
600
598
  if (existingHandler) {
@@ -612,10 +610,11 @@ export class YotoPlatform {
612
610
 
613
611
  this.cardAccessoryHandlers.set(uuid, handler)
614
612
  await handler.setup()
613
+ this.log.debug('Card control setup complete:', accessoryName)
615
614
  continue
616
615
  }
617
616
 
618
- this.log.debug('Adding new card control accessory:', accessoryName)
617
+ this.log.debug('Adding new card control accessory:', accessoryName, uuid)
619
618
 
620
619
  /** @type {PlatformAccessory<YotoCardAccessoryContext>} */
621
620
  // eslint-disable-next-line new-cap
@@ -625,13 +624,6 @@ export class YotoPlatform {
625
624
  this.api.hap.Categories.SWITCH
626
625
  )
627
626
 
628
- const infoService = accessory.getService(this.api.hap.Service.AccessoryInformation)
629
- if (infoService) {
630
- infoService
631
- .setCharacteristic(this.api.hap.Characteristic.Name, accessoryName)
632
- .setCharacteristic(this.api.hap.Characteristic.ConfiguredName, accessoryName)
633
- }
634
-
635
627
  accessory.context = {
636
628
  type: 'card-control',
637
629
  cardControl: control,
@@ -645,11 +637,14 @@ export class YotoPlatform {
645
637
 
646
638
  this.cardAccessoryHandlers.set(uuid, handler)
647
639
  await handler.setup()
640
+ this.log.debug('Card control setup complete:', accessoryName)
648
641
 
649
642
  this.log.debug(`Registering card control accessory: ${accessoryName}`)
650
643
  this.api.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [accessory])
644
+ this.log.debug('Registered card control accessory:', accessoryName, uuid)
651
645
 
652
646
  this.cardAccessories.set(uuid, accessory)
647
+ this.log.debug('Tracked card control accessory:', accessoryName, uuid)
653
648
  }
654
649
 
655
650
  for (const [uuid, accessory] of this.cardAccessories) {
@@ -657,7 +652,7 @@ export class YotoPlatform {
657
652
  continue
658
653
  }
659
654
 
660
- this.log.debug('Removing card control accessory from cache:', accessory.displayName)
655
+ this.log.debug('Removing card control accessory from cache:', accessory.displayName, uuid)
661
656
 
662
657
  const handler = this.cardAccessoryHandlers.get(uuid)
663
658
  if (handler) {
@@ -669,6 +664,7 @@ export class YotoPlatform {
669
664
 
670
665
  this.api.unregisterPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [accessory])
671
666
  this.cardAccessories.delete(uuid)
667
+ this.log.debug('Removed card control accessory:', accessory.displayName, uuid)
672
668
  }
673
669
  }
674
670
 
@@ -683,10 +679,16 @@ export class YotoPlatform {
683
679
  // Get current device IDs from account
684
680
  const currentDeviceIds = this.yotoAccount.getDeviceIds()
685
681
  const currentUUIDs = currentDeviceIds.map(id => this.api.hap.uuid.generate(id))
682
+ this.log.debug(
683
+ 'Evaluating stale accessories:',
684
+ `accountDevices=${currentDeviceIds.length}`,
685
+ `cachedAccessories=${this.accessories.size}`,
686
+ `externalSpeakers=${this.speakerAccessories.size}`
687
+ )
686
688
 
687
689
  for (const [uuid, accessory] of this.accessories) {
688
690
  if (!currentUUIDs.includes(uuid)) {
689
- this.log.debug('Removing existing accessory from cache:', accessory.displayName)
691
+ this.log.debug('Removing existing accessory from cache:', accessory.displayName, uuid)
690
692
 
691
693
  // Stop handler if it exists
692
694
  const handler = this.accessoryHandlers.get(uuid)
@@ -699,16 +701,18 @@ export class YotoPlatform {
699
701
 
700
702
  // Unregister from Homebridge
701
703
  this.api.unregisterPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [accessory])
704
+ this.log.debug('Unregistered accessory from Homebridge:', accessory.displayName, uuid)
702
705
 
703
706
  // Remove from our tracking map
704
707
  this.accessories.delete(uuid)
708
+ this.log.debug('Removed accessory from tracking map:', accessory.displayName, uuid)
705
709
  }
706
710
  }
707
711
 
708
712
  for (const [uuid, accessory] of this.speakerAccessories) {
709
713
  const deviceId = accessory.context.device?.deviceId
710
714
  if (!deviceId || !currentDeviceIds.includes(deviceId)) {
711
- this.log.debug('Removing external SmartSpeaker accessory from runtime:', accessory.displayName)
715
+ this.log.debug('Removing external SmartSpeaker accessory from runtime:', accessory.displayName, uuid)
712
716
 
713
717
  const handler = this.speakerAccessoryHandlers.get(uuid)
714
718
  if (handler) {
@@ -719,6 +723,7 @@ export class YotoPlatform {
719
723
  }
720
724
 
721
725
  this.speakerAccessories.delete(uuid)
726
+ this.log.debug('Removed external SmartSpeaker accessory:', accessory.displayName, uuid)
722
727
  }
723
728
  }
724
729
  }
@@ -728,6 +733,12 @@ export class YotoPlatform {
728
733
  */
729
734
  async shutdown () {
730
735
  this.log.debug('Shutting down Yoto platform...')
736
+ this.log.debug(
737
+ 'Handlers to stop:',
738
+ `devices=${this.accessoryHandlers.size}`,
739
+ `speakers=${this.speakerAccessoryHandlers.size}`,
740
+ `cardControls=${this.cardAccessoryHandlers.size}`
741
+ )
731
742
 
732
743
  // Stop all accessory handlers
733
744
  const stopPromises = []
@@ -78,6 +78,11 @@ export class YotoSpeakerAccessory {
78
78
  const { Service, Characteristic } = this.#platform
79
79
  const service = this.#accessory.getService(Service.AccessoryInformation) ||
80
80
  this.#accessory.addService(Service.AccessoryInformation)
81
+ const displayName = sanitizeName(this.#accessory.displayName)
82
+ const nameCharacteristic = service.getCharacteristic(Characteristic.Name)
83
+ const configuredCharacteristic = service.getCharacteristic(Characteristic.ConfiguredName)
84
+ const previousName = nameCharacteristic.value
85
+ const configuredName = configuredCharacteristic.value
81
86
 
82
87
  const hardwareRevision = [
83
88
  this.#device.generation,
@@ -87,11 +92,16 @@ export class YotoSpeakerAccessory {
87
92
  const model = this.#device.deviceFamily || this.#device.deviceType || DEFAULT_MODEL
88
93
 
89
94
  service
95
+ .setCharacteristic(Characteristic.Name, displayName)
90
96
  .setCharacteristic(Characteristic.Manufacturer, DEFAULT_MANUFACTURER)
91
97
  .setCharacteristic(Characteristic.Model, model)
92
98
  .setCharacteristic(Characteristic.SerialNumber, this.#device.deviceId)
93
99
  .setCharacteristic(Characteristic.HardwareRevision, hardwareRevision)
94
100
 
101
+ if (typeof configuredName !== 'string' || configuredName === previousName) {
102
+ service.setCharacteristic(Characteristic.ConfiguredName, displayName)
103
+ }
104
+
95
105
  if (this.#deviceModel.status.firmwareVersion) {
96
106
  service.setCharacteristic(
97
107
  Characteristic.FirmwareRevision,
@@ -285,7 +295,12 @@ export class YotoSpeakerAccessory {
285
295
  */
286
296
  async getCurrentMediaState () {
287
297
  const playbackStatus = this.#deviceModel.playback.playbackStatus ?? null
288
- return this.getMediaStateValues(playbackStatus).current
298
+ const current = this.getMediaStateValues(playbackStatus).current
299
+ this.#log.debug(
300
+ LOG_PREFIX.ACCESSORY,
301
+ `[${this.#device.name}] Get current media state -> ${current} (${playbackStatus ?? 'unknown'})`
302
+ )
303
+ return current
289
304
  }
290
305
 
291
306
  /**
@@ -294,7 +309,12 @@ export class YotoSpeakerAccessory {
294
309
  */
295
310
  async getTargetMediaState () {
296
311
  const playbackStatus = this.#deviceModel.playback.playbackStatus ?? null
297
- return this.getMediaStateValues(playbackStatus).target
312
+ const target = this.getMediaStateValues(playbackStatus).target
313
+ this.#log.debug(
314
+ LOG_PREFIX.ACCESSORY,
315
+ `[${this.#device.name}] Get target media state -> ${target} (${playbackStatus ?? 'unknown'})`
316
+ )
317
+ return target
298
318
  }
299
319
 
300
320
  /**
@@ -337,7 +357,12 @@ export class YotoSpeakerAccessory {
337
357
  const volumeSteps = this.#deviceModel.status.volume
338
358
  const normalizedSteps = Number.isFinite(volumeSteps) ? volumeSteps : 0
339
359
  const clampedSteps = Math.max(0, Math.min(normalizedSteps, 16))
340
- return Math.round((clampedSteps / 16) * 100)
360
+ const percent = Math.round((clampedSteps / 16) * 100)
361
+ this.#log.debug(
362
+ LOG_PREFIX.ACCESSORY,
363
+ `[${this.#device.name}] Get speaker volume -> ${percent} (rawSteps=${volumeSteps})`
364
+ )
365
+ return percent
341
366
  }
342
367
 
343
368
  /**
@@ -380,7 +405,9 @@ export class YotoSpeakerAccessory {
380
405
  * @returns {Promise<CharacteristicValue>}
381
406
  */
382
407
  async getMute () {
383
- return this.#deviceModel.status.volume === 0
408
+ const isMuted = this.#deviceModel.status.volume === 0
409
+ this.#log.debug(LOG_PREFIX.ACCESSORY, `[${this.#device.name}] Get speaker mute -> ${isMuted}`)
410
+ return isMuted
384
411
  }
385
412
 
386
413
  /**
@@ -405,7 +432,9 @@ export class YotoSpeakerAccessory {
405
432
  * @returns {Promise<CharacteristicValue>}
406
433
  */
407
434
  async getStatusActive () {
408
- return this.#deviceModel.status.isOnline
435
+ const isOnline = this.#deviceModel.status.isOnline
436
+ this.#log.debug(LOG_PREFIX.ACCESSORY, `[${this.#device.name}] Get speaker status active -> ${isOnline}`)
437
+ return isOnline
409
438
  }
410
439
 
411
440
  /**
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.41",
4
+ "version": "0.0.42",
5
5
  "author": "Bret Comnes <bcomnes@gmail.com> (https://bret.io)",
6
6
  "bugs": {
7
7
  "url": "https://github.com/bcomnes/homebridge-yoto/issues"