homebridge-yoto 0.0.40 → 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.
@@ -61,6 +61,12 @@
61
61
  ],
62
62
  "description": "Choose how playback and volume controls are exposed. External Smart Speaker requires additional pairing steps and appears as a separate accessory."
63
63
  },
64
+ "battery": {
65
+ "title": "Battery",
66
+ "type": "boolean",
67
+ "default": true,
68
+ "description": "Expose battery status service."
69
+ },
64
70
  "temperature": {
65
71
  "title": "Temperature Sensor",
66
72
  "type": "boolean",
@@ -167,6 +173,7 @@
167
173
  "helpvalue": "<p><strong>Playback Controls:</strong> External Smart Speaker requires additional pairing steps in the Home app and will appear as a separate accessory.</p>"
168
174
  },
169
175
  "services.playbackAccessory",
176
+ "services.battery",
170
177
  "services.temperature",
171
178
  "services.nightlight",
172
179
  "services.cardSlot",
package/lib/accessory.js CHANGED
@@ -23,6 +23,7 @@
23
23
  * @typedef {Object} YotoServiceToggles
24
24
  * @property {boolean} playback
25
25
  * @property {boolean} volume
26
+ * @property {boolean} battery
26
27
  * @property {boolean} temperature
27
28
  * @property {boolean} nightlight
28
29
  * @property {boolean} cardSlot
@@ -135,6 +136,7 @@ export class YotoPlayerAccessory {
135
136
  return {
136
137
  playback: playbackConfig.playbackEnabled,
137
138
  volume: playbackConfig.volumeEnabled,
139
+ battery: getBooleanSetting(serviceConfig['battery'], getServiceDefault('battery')),
138
140
  temperature: getBooleanSetting(serviceConfig['temperature'], getServiceDefault('temperature')),
139
141
  nightlight: getBooleanSetting(serviceConfig['nightlight'], getServiceDefault('nightlight')),
140
142
  cardSlot: getBooleanSetting(serviceConfig['cardSlot'], getServiceDefault('cardSlot')),
@@ -177,7 +179,9 @@ export class YotoPlayerAccessory {
177
179
  this.setupVolumeService()
178
180
  }
179
181
 
180
- this.setupBatteryService()
182
+ if (serviceToggles.battery) {
183
+ this.setupBatteryService()
184
+ }
181
185
 
182
186
  // Setup optional services based on device capabilities
183
187
  if (serviceToggles.temperature && this.#deviceModel.capabilities.hasTemperatureSensor) {
@@ -239,6 +243,11 @@ export class YotoPlayerAccessory {
239
243
  const { Service, Characteristic } = this.#platform
240
244
  const service = this.#accessory.getService(Service.AccessoryInformation) ||
241
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
242
251
 
243
252
  // Build hardware revision from generation and form factor
244
253
  const hardwareRevision = [
@@ -251,11 +260,16 @@ export class YotoPlayerAccessory {
251
260
 
252
261
  // Set standard characteristics
253
262
  service
263
+ .setCharacteristic(Characteristic.Name, displayName)
254
264
  .setCharacteristic(Characteristic.Manufacturer, DEFAULT_MANUFACTURER)
255
265
  .setCharacteristic(Characteristic.Model, model)
256
266
  .setCharacteristic(Characteristic.SerialNumber, this.#device.deviceId)
257
267
  .setCharacteristic(Characteristic.HardwareRevision, hardwareRevision)
258
268
 
269
+ if (typeof configuredName !== 'string' || configuredName === previousName) {
270
+ service.setCharacteristic(Characteristic.ConfiguredName, displayName)
271
+ }
272
+
259
273
  // Set firmware version from live status if available
260
274
  if (this.#deviceModel.status.firmwareVersion) {
261
275
  service.setCharacteristic(
@@ -917,7 +931,8 @@ export class YotoPlayerAccessory {
917
931
  })
918
932
 
919
933
  this.#deviceModel.on('error', (error) => {
920
- this.#log.error(`[${this.#device.name}] Device error:`, error.message)
934
+ const details = error instanceof Error ? (error.stack || error.message) : String(error)
935
+ this.#log.error(`[${this.#device.name}] Device error:`, details)
921
936
  })
922
937
  }
923
938
 
@@ -928,7 +943,9 @@ export class YotoPlayerAccessory {
928
943
  * @returns {Promise<CharacteristicValue>}
929
944
  */
930
945
  async getPlaybackOn () {
931
- 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
932
949
  }
933
950
 
934
951
  /**
@@ -1028,7 +1045,9 @@ export class YotoPlayerAccessory {
1028
1045
  * @returns {Promise<CharacteristicValue>}
1029
1046
  */
1030
1047
  async getVolumeOn () {
1031
- 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
1032
1051
  }
1033
1052
 
1034
1053
  /**
@@ -1059,7 +1078,9 @@ export class YotoPlayerAccessory {
1059
1078
  * @returns {Promise<CharacteristicValue>}
1060
1079
  */
1061
1080
  async getStatusActive () {
1062
- 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
1063
1084
  }
1064
1085
 
1065
1086
  /**
@@ -1068,7 +1089,9 @@ export class YotoPlayerAccessory {
1068
1089
  */
1069
1090
  async getOnlineStatus () {
1070
1091
  const { Characteristic } = this.#platform
1071
- 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
1072
1095
  ? Characteristic.ContactSensorState.CONTACT_NOT_DETECTED
1073
1096
  : Characteristic.ContactSensorState.CONTACT_DETECTED
1074
1097
  }
@@ -1081,7 +1104,9 @@ export class YotoPlayerAccessory {
1081
1104
  */
1082
1105
  async getBatteryLevel () {
1083
1106
  const battery = this.#deviceModel.status.batteryLevelPercentage
1084
- 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
1085
1110
  }
1086
1111
 
1087
1112
  /**
@@ -1090,6 +1115,7 @@ export class YotoPlayerAccessory {
1090
1115
  */
1091
1116
  async getChargingState () {
1092
1117
  const isCharging = this.#deviceModel.status.isCharging
1118
+ this.#log.debug(LOG_PREFIX.ACCESSORY, `[${this.#device.name}] Get charging state -> ${isCharging}`)
1093
1119
  return isCharging
1094
1120
  ? this.#platform.Characteristic.ChargingState.CHARGING
1095
1121
  : this.#platform.Characteristic.ChargingState.NOT_CHARGING
@@ -1102,6 +1128,7 @@ export class YotoPlayerAccessory {
1102
1128
  async getStatusLowBattery () {
1103
1129
  const battery = this.#deviceModel.status.batteryLevelPercentage
1104
1130
  const batteryLevel = Number.isFinite(battery) ? battery : 100
1131
+ this.#log.debug(LOG_PREFIX.ACCESSORY, `[${this.#device.name}] Get low battery -> ${batteryLevel}`)
1105
1132
  return batteryLevel <= LOW_BATTERY_THRESHOLD
1106
1133
  ? this.#platform.Characteristic.StatusLowBattery.BATTERY_LEVEL_LOW
1107
1134
  : this.#platform.Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL
@@ -1118,9 +1145,11 @@ export class YotoPlayerAccessory {
1118
1145
 
1119
1146
  // Return a default value if temperature is not available
1120
1147
  if (temp === null || temp === 'notSupported') {
1148
+ this.#log.debug(LOG_PREFIX.ACCESSORY, `[${this.#device.name}] Get temperature -> unavailable`)
1121
1149
  return 0
1122
1150
  }
1123
1151
 
1152
+ this.#log.debug(LOG_PREFIX.ACCESSORY, `[${this.#device.name}] Get temperature -> ${temp}`)
1124
1153
  return Number(temp)
1125
1154
  }
1126
1155
 
@@ -1130,13 +1159,18 @@ export class YotoPlayerAccessory {
1130
1159
  */
1131
1160
  async getTemperatureSensorFault () {
1132
1161
  // Report fault if device is offline or temperature is not available
1133
- if (!this.#deviceModel.status.isOnline ||
1134
- this.#deviceModel.status.temperatureCelsius === null ||
1135
- this.#deviceModel.status.temperatureCelsius === 'notSupported') {
1136
- return this.#platform.Characteristic.StatusFault.GENERAL_FAULT
1137
- }
1138
-
1139
- 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
1140
1174
  }
1141
1175
 
1142
1176
  // ==================== Nightlight Characteristic Handlers ====================
@@ -1178,7 +1212,9 @@ export class YotoPlayerAccessory {
1178
1212
  */
1179
1213
  async getDayNightlightOn () {
1180
1214
  const color = this.#deviceModel.config.ambientColour
1181
- 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
1182
1218
  }
1183
1219
 
1184
1220
  /**
@@ -1186,19 +1222,26 @@ export class YotoPlayerAccessory {
1186
1222
  * @param {CharacteristicValue} value
1187
1223
  */
1188
1224
  async setDayNightlightOn (value) {
1189
- if (value) {
1190
- // Turn ON - restore previous color or default to white
1191
- const colorToSet = this.#lastDayColor || '0xffffff'
1192
- this.#log.debug(LOG_PREFIX.ACCESSORY, `Turning day nightlight ON with color: ${colorToSet}`)
1193
- await this.#deviceModel.updateConfig({ ambientColour: colorToSet })
1194
- } else {
1195
- // Turn OFF - save current color and set to black
1196
- const currentColor = this.#deviceModel.config.ambientColour
1197
- if (!this.isColorOff(currentColor)) {
1198
- this.#lastDayColor = currentColor
1225
+ try {
1226
+ if (value) {
1227
+ // Turn ON - restore previous color or default to white
1228
+ const colorToSet = this.#lastDayColor || '0xffffff'
1229
+ this.#log.debug(LOG_PREFIX.ACCESSORY, `Turning day nightlight ON with color: ${colorToSet}`)
1230
+ await this.#deviceModel.updateConfig({ ambientColour: colorToSet })
1231
+ } else {
1232
+ // Turn OFF - save current color and set to black
1233
+ const currentColor = this.#deviceModel.config.ambientColour
1234
+ if (!this.isColorOff(currentColor)) {
1235
+ this.#lastDayColor = currentColor
1236
+ }
1237
+ this.#log.debug(LOG_PREFIX.ACCESSORY, 'Turning day nightlight OFF')
1238
+ await this.#deviceModel.updateConfig({ ambientColour: '0x000000' })
1199
1239
  }
1200
- this.#log.debug(LOG_PREFIX.ACCESSORY, 'Turning day nightlight OFF')
1201
- await this.#deviceModel.updateConfig({ ambientColour: '0x000000' })
1240
+ } catch (error) {
1241
+ this.#log.error(LOG_PREFIX.ACCESSORY, `[${this.#device.name}] Failed to set day nightlight:`, error)
1242
+ throw new this.#platform.api.hap.HapStatusError(
1243
+ this.#platform.api.hap.HAPStatus.SERVICE_COMMUNICATION_FAILURE
1244
+ )
1202
1245
  }
1203
1246
  }
1204
1247
 
@@ -1208,10 +1251,16 @@ export class YotoPlayerAccessory {
1208
1251
  */
1209
1252
  async getDayNightlightBrightness () {
1210
1253
  const config = this.#deviceModel.config
1211
- if (config.dayDisplayBrightnessAuto || config.dayDisplayBrightness === null) {
1212
- return 100
1213
- }
1214
- 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
1215
1264
  }
1216
1265
 
1217
1266
  /**
@@ -1228,10 +1277,17 @@ export class YotoPlayerAccessory {
1228
1277
 
1229
1278
  const brightnessValue = Math.max(0, Math.min(Math.round(rawBrightness), 100))
1230
1279
  this.#log.debug(LOG_PREFIX.ACCESSORY, `Setting day display brightness: ${brightnessValue}`)
1231
- await this.#deviceModel.updateConfig({
1232
- dayDisplayBrightness: brightnessValue,
1233
- dayDisplayBrightnessAuto: false
1234
- })
1280
+ try {
1281
+ await this.#deviceModel.updateConfig({
1282
+ dayDisplayBrightness: brightnessValue,
1283
+ dayDisplayBrightnessAuto: false
1284
+ })
1285
+ } catch (error) {
1286
+ this.#log.error(LOG_PREFIX.ACCESSORY, `[${this.#device.name}] Failed to set day brightness:`, error)
1287
+ throw new this.#platform.api.hap.HapStatusError(
1288
+ this.#platform.api.hap.HAPStatus.SERVICE_COMMUNICATION_FAILURE
1289
+ )
1290
+ }
1235
1291
  }
1236
1292
 
1237
1293
  /**
@@ -1241,10 +1297,12 @@ export class YotoPlayerAccessory {
1241
1297
  async getDayNightlightHue () {
1242
1298
  const color = this.#deviceModel.config.ambientColour
1243
1299
  if (this.isColorOff(color)) {
1300
+ this.#log.debug(LOG_PREFIX.ACCESSORY, `[${this.#device.name}] Get day nightlight hue -> 0 (off)`)
1244
1301
  return 0
1245
1302
  }
1246
1303
  const hex = this.parseHexColor(color)
1247
1304
  const [h] = convert.hex.hsv(hex)
1305
+ this.#log.debug(LOG_PREFIX.ACCESSORY, `[${this.#device.name}] Get day nightlight hue -> ${h} (${color})`)
1248
1306
  return h
1249
1307
  }
1250
1308
 
@@ -1275,7 +1333,14 @@ export class YotoPlayerAccessory {
1275
1333
  const formattedColor = this.formatHexColor(newHex)
1276
1334
 
1277
1335
  this.#log.debug(LOG_PREFIX.ACCESSORY, `Setting day nightlight hue: ${hue}° → ${formattedColor}`)
1278
- await this.#deviceModel.updateConfig({ ambientColour: formattedColor })
1336
+ try {
1337
+ await this.#deviceModel.updateConfig({ ambientColour: formattedColor })
1338
+ } catch (error) {
1339
+ this.#log.error(LOG_PREFIX.ACCESSORY, `[${this.#device.name}] Failed to set day nightlight hue:`, error)
1340
+ throw new this.#platform.api.hap.HapStatusError(
1341
+ this.#platform.api.hap.HAPStatus.SERVICE_COMMUNICATION_FAILURE
1342
+ )
1343
+ }
1279
1344
  }
1280
1345
 
1281
1346
  /**
@@ -1285,10 +1350,12 @@ export class YotoPlayerAccessory {
1285
1350
  async getDayNightlightSaturation () {
1286
1351
  const color = this.#deviceModel.config.ambientColour
1287
1352
  if (this.isColorOff(color)) {
1353
+ this.#log.debug(LOG_PREFIX.ACCESSORY, `[${this.#device.name}] Get day nightlight saturation -> 0 (off)`)
1288
1354
  return 0
1289
1355
  }
1290
1356
  const hex = this.parseHexColor(color)
1291
1357
  const [, s] = convert.hex.hsv(hex)
1358
+ this.#log.debug(LOG_PREFIX.ACCESSORY, `[${this.#device.name}] Get day nightlight saturation -> ${s} (${color})`)
1292
1359
  return s
1293
1360
  }
1294
1361
 
@@ -1319,7 +1386,14 @@ export class YotoPlayerAccessory {
1319
1386
  const formattedColor = this.formatHexColor(newHex)
1320
1387
 
1321
1388
  this.#log.debug(LOG_PREFIX.ACCESSORY, `Setting day nightlight saturation: ${saturation}% → ${formattedColor}`)
1322
- await this.#deviceModel.updateConfig({ ambientColour: formattedColor })
1389
+ try {
1390
+ await this.#deviceModel.updateConfig({ ambientColour: formattedColor })
1391
+ } catch (error) {
1392
+ this.#log.error(LOG_PREFIX.ACCESSORY, `[${this.#device.name}] Failed to set day nightlight saturation:`, error)
1393
+ throw new this.#platform.api.hap.HapStatusError(
1394
+ this.#platform.api.hap.HAPStatus.SERVICE_COMMUNICATION_FAILURE
1395
+ )
1396
+ }
1323
1397
  }
1324
1398
 
1325
1399
  // ---------- Night Nightlight Handlers ----------
@@ -1330,7 +1404,9 @@ export class YotoPlayerAccessory {
1330
1404
  */
1331
1405
  async getNightNightlightOn () {
1332
1406
  const color = this.#deviceModel.config.nightAmbientColour
1333
- 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
1334
1410
  }
1335
1411
 
1336
1412
  /**
@@ -1338,19 +1414,26 @@ export class YotoPlayerAccessory {
1338
1414
  * @param {CharacteristicValue} value
1339
1415
  */
1340
1416
  async setNightNightlightOn (value) {
1341
- if (value) {
1342
- // Turn ON - restore previous color or default to white
1343
- const colorToSet = this.#lastNightColor || '0xffffff'
1344
- this.#log.debug(LOG_PREFIX.ACCESSORY, `Turning night nightlight ON with color: ${colorToSet}`)
1345
- await this.#deviceModel.updateConfig({ nightAmbientColour: colorToSet })
1346
- } else {
1347
- // Turn OFF - save current color and set to black
1348
- const currentColor = this.#deviceModel.config.nightAmbientColour
1349
- if (!this.isColorOff(currentColor)) {
1350
- this.#lastNightColor = currentColor
1417
+ try {
1418
+ if (value) {
1419
+ // Turn ON - restore previous color or default to white
1420
+ const colorToSet = this.#lastNightColor || '0xffffff'
1421
+ this.#log.debug(LOG_PREFIX.ACCESSORY, `Turning night nightlight ON with color: ${colorToSet}`)
1422
+ await this.#deviceModel.updateConfig({ nightAmbientColour: colorToSet })
1423
+ } else {
1424
+ // Turn OFF - save current color and set to black
1425
+ const currentColor = this.#deviceModel.config.nightAmbientColour
1426
+ if (!this.isColorOff(currentColor)) {
1427
+ this.#lastNightColor = currentColor
1428
+ }
1429
+ this.#log.debug(LOG_PREFIX.ACCESSORY, 'Turning night nightlight OFF')
1430
+ await this.#deviceModel.updateConfig({ nightAmbientColour: '0x000000' })
1351
1431
  }
1352
- this.#log.debug(LOG_PREFIX.ACCESSORY, 'Turning night nightlight OFF')
1353
- await this.#deviceModel.updateConfig({ nightAmbientColour: '0x000000' })
1432
+ } catch (error) {
1433
+ this.#log.error(LOG_PREFIX.ACCESSORY, `[${this.#device.name}] Failed to set night nightlight:`, error)
1434
+ throw new this.#platform.api.hap.HapStatusError(
1435
+ this.#platform.api.hap.HAPStatus.SERVICE_COMMUNICATION_FAILURE
1436
+ )
1354
1437
  }
1355
1438
  }
1356
1439
 
@@ -1360,10 +1443,16 @@ export class YotoPlayerAccessory {
1360
1443
  */
1361
1444
  async getNightNightlightBrightness () {
1362
1445
  const config = this.#deviceModel.config
1363
- if (config.nightDisplayBrightnessAuto || config.nightDisplayBrightness === null) {
1364
- return 100
1365
- }
1366
- 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
1367
1456
  }
1368
1457
 
1369
1458
  /**
@@ -1380,10 +1469,17 @@ export class YotoPlayerAccessory {
1380
1469
 
1381
1470
  const brightnessValue = Math.max(0, Math.min(Math.round(rawBrightness), 100))
1382
1471
  this.#log.debug(LOG_PREFIX.ACCESSORY, `Setting night display brightness: ${brightnessValue}`)
1383
- await this.#deviceModel.updateConfig({
1384
- nightDisplayBrightness: brightnessValue,
1385
- nightDisplayBrightnessAuto: false
1386
- })
1472
+ try {
1473
+ await this.#deviceModel.updateConfig({
1474
+ nightDisplayBrightness: brightnessValue,
1475
+ nightDisplayBrightnessAuto: false
1476
+ })
1477
+ } catch (error) {
1478
+ this.#log.error(LOG_PREFIX.ACCESSORY, `[${this.#device.name}] Failed to set night brightness:`, error)
1479
+ throw new this.#platform.api.hap.HapStatusError(
1480
+ this.#platform.api.hap.HAPStatus.SERVICE_COMMUNICATION_FAILURE
1481
+ )
1482
+ }
1387
1483
  }
1388
1484
 
1389
1485
  /**
@@ -1393,10 +1489,12 @@ export class YotoPlayerAccessory {
1393
1489
  async getNightNightlightHue () {
1394
1490
  const color = this.#deviceModel.config.nightAmbientColour
1395
1491
  if (this.isColorOff(color)) {
1492
+ this.#log.debug(LOG_PREFIX.ACCESSORY, `[${this.#device.name}] Get night nightlight hue -> 0 (off)`)
1396
1493
  return 0
1397
1494
  }
1398
1495
  const hex = this.parseHexColor(color)
1399
1496
  const [h] = convert.hex.hsv(hex)
1497
+ this.#log.debug(LOG_PREFIX.ACCESSORY, `[${this.#device.name}] Get night nightlight hue -> ${h} (${color})`)
1400
1498
  return h
1401
1499
  }
1402
1500
 
@@ -1427,7 +1525,14 @@ export class YotoPlayerAccessory {
1427
1525
  const formattedColor = this.formatHexColor(newHex)
1428
1526
 
1429
1527
  this.#log.debug(LOG_PREFIX.ACCESSORY, `Setting night nightlight hue: ${hue}° → ${formattedColor}`)
1430
- await this.#deviceModel.updateConfig({ nightAmbientColour: formattedColor })
1528
+ try {
1529
+ await this.#deviceModel.updateConfig({ nightAmbientColour: formattedColor })
1530
+ } catch (error) {
1531
+ this.#log.error(LOG_PREFIX.ACCESSORY, `[${this.#device.name}] Failed to set night nightlight hue:`, error)
1532
+ throw new this.#platform.api.hap.HapStatusError(
1533
+ this.#platform.api.hap.HAPStatus.SERVICE_COMMUNICATION_FAILURE
1534
+ )
1535
+ }
1431
1536
  }
1432
1537
 
1433
1538
  /**
@@ -1437,10 +1542,12 @@ export class YotoPlayerAccessory {
1437
1542
  async getNightNightlightSaturation () {
1438
1543
  const color = this.#deviceModel.config.nightAmbientColour
1439
1544
  if (this.isColorOff(color)) {
1545
+ this.#log.debug(LOG_PREFIX.ACCESSORY, `[${this.#device.name}] Get night nightlight saturation -> 0 (off)`)
1440
1546
  return 0
1441
1547
  }
1442
1548
  const hex = this.parseHexColor(color)
1443
1549
  const [, s] = convert.hex.hsv(hex)
1550
+ this.#log.debug(LOG_PREFIX.ACCESSORY, `[${this.#device.name}] Get night nightlight saturation -> ${s} (${color})`)
1444
1551
  return s
1445
1552
  }
1446
1553
 
@@ -1471,7 +1578,14 @@ export class YotoPlayerAccessory {
1471
1578
  const formattedColor = this.formatHexColor(newHex)
1472
1579
 
1473
1580
  this.#log.debug(LOG_PREFIX.ACCESSORY, `Setting night nightlight saturation: ${saturation}% → ${formattedColor}`)
1474
- await this.#deviceModel.updateConfig({ nightAmbientColour: formattedColor })
1581
+ try {
1582
+ await this.#deviceModel.updateConfig({ nightAmbientColour: formattedColor })
1583
+ } catch (error) {
1584
+ this.#log.error(LOG_PREFIX.ACCESSORY, `[${this.#device.name}] Failed to set night nightlight saturation:`, error)
1585
+ throw new this.#platform.api.hap.HapStatusError(
1586
+ this.#platform.api.hap.HAPStatus.SERVICE_COMMUNICATION_FAILURE
1587
+ )
1588
+ }
1475
1589
  }
1476
1590
 
1477
1591
  // ==================== Nightlight Status ContactSensor Getters ====================
@@ -1484,6 +1598,7 @@ export class YotoPlayerAccessory {
1484
1598
  const { Characteristic } = this.#platform
1485
1599
  const status = this.#deviceModel.status
1486
1600
  const isActive = status.nightlightMode !== 'off'
1601
+ this.#log.debug(LOG_PREFIX.ACCESSORY, `[${this.#device.name}] Get nightlight active -> ${isActive}`)
1487
1602
  return isActive ? Characteristic.ContactSensorState.CONTACT_NOT_DETECTED : Characteristic.ContactSensorState.CONTACT_DETECTED
1488
1603
  }
1489
1604
 
@@ -1497,6 +1612,10 @@ export class YotoPlayerAccessory {
1497
1612
  const isDay = status.dayMode === 'day'
1498
1613
  const isActive = status.nightlightMode !== 'off'
1499
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
+ )
1500
1619
  return isShowing ? Characteristic.ContactSensorState.CONTACT_NOT_DETECTED : Characteristic.ContactSensorState.CONTACT_DETECTED
1501
1620
  }
1502
1621
 
@@ -1510,6 +1629,10 @@ export class YotoPlayerAccessory {
1510
1629
  const isNight = status.dayMode === 'night'
1511
1630
  const isActive = status.nightlightMode !== 'off'
1512
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
+ )
1513
1636
  return isShowing ? Characteristic.ContactSensorState.CONTACT_NOT_DETECTED : Characteristic.ContactSensorState.CONTACT_DETECTED
1514
1637
  }
1515
1638
 
@@ -1523,6 +1646,7 @@ export class YotoPlayerAccessory {
1523
1646
  const { Characteristic } = this.#platform
1524
1647
  const status = this.#deviceModel.status
1525
1648
  const hasCard = status.cardInsertionState !== 'none'
1649
+ this.#log.debug(LOG_PREFIX.ACCESSORY, `[${this.#device.name}] Get card slot -> ${hasCard} (${status.cardInsertionState})`)
1526
1650
  return hasCard ? Characteristic.ContactSensorState.CONTACT_NOT_DETECTED : Characteristic.ContactSensorState.CONTACT_DETECTED
1527
1651
  }
1528
1652
 
@@ -1536,6 +1660,7 @@ export class YotoPlayerAccessory {
1536
1660
  const { Characteristic } = this.#platform
1537
1661
  const status = this.#deviceModel.status
1538
1662
  const isDayMode = status.dayMode === 'day'
1663
+ this.#log.debug(LOG_PREFIX.ACCESSORY, `[${this.#device.name}] Get day mode -> ${isDayMode}`)
1539
1664
  return isDayMode
1540
1665
  ? Characteristic.ContactSensorState.CONTACT_NOT_DETECTED
1541
1666
  : Characteristic.ContactSensorState.CONTACT_DETECTED
@@ -1549,6 +1674,10 @@ export class YotoPlayerAccessory {
1549
1674
  */
1550
1675
  async getSleepTimerState () {
1551
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
+ )
1552
1681
  return playback.sleepTimerActive ?? false
1553
1682
  }
1554
1683
 
@@ -1584,7 +1713,9 @@ export class YotoPlayerAccessory {
1584
1713
  * @returns {Promise<CharacteristicValue>}
1585
1714
  */
1586
1715
  async getBluetoothState () {
1587
- 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
1588
1719
  }
1589
1720
 
1590
1721
  /**
@@ -1594,7 +1725,14 @@ export class YotoPlayerAccessory {
1594
1725
  async setBluetoothState (value) {
1595
1726
  const enabled = Boolean(value)
1596
1727
  this.#log.debug(LOG_PREFIX.ACCESSORY, `Setting Bluetooth: ${enabled ? 'ON' : 'OFF'}`)
1597
- await this.#deviceModel.updateConfig({ bluetoothEnabled: enabled })
1728
+ try {
1729
+ await this.#deviceModel.updateConfig({ bluetoothEnabled: enabled })
1730
+ } catch (error) {
1731
+ this.#log.error(LOG_PREFIX.ACCESSORY, `[${this.#device.name}] Failed to set Bluetooth:`, error)
1732
+ throw new this.#platform.api.hap.HapStatusError(
1733
+ this.#platform.api.hap.HAPStatus.SERVICE_COMMUNICATION_FAILURE
1734
+ )
1735
+ }
1598
1736
  }
1599
1737
 
1600
1738
  // ==================== Card Control Switch Setter ====================
@@ -1609,6 +1747,10 @@ export class YotoPlayerAccessory {
1609
1747
  async setCardControl (service, control, value) {
1610
1748
  const { Characteristic } = this.#platform
1611
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
+ )
1612
1754
 
1613
1755
  if (!isOn) {
1614
1756
  service.getCharacteristic(Characteristic.On).updateValue(false)
@@ -1683,7 +1825,14 @@ export class YotoPlayerAccessory {
1683
1825
  LOG_PREFIX.ACCESSORY,
1684
1826
  `[${this.#device.name}] Set day max volume limit raw=${value} normalizedPercent=${normalizedPercent} requestedSteps=${requestedSteps} -> steps=${limit} percent=${limitPercent}`
1685
1827
  )
1686
- await this.#deviceModel.updateConfig({ maxVolumeLimit: limit })
1828
+ try {
1829
+ await this.#deviceModel.updateConfig({ maxVolumeLimit: limit })
1830
+ } catch (error) {
1831
+ this.#log.error(LOG_PREFIX.ACCESSORY, `[${this.#device.name}] Failed to set day max volume limit:`, error)
1832
+ throw new this.#platform.api.hap.HapStatusError(
1833
+ this.#platform.api.hap.HAPStatus.SERVICE_COMMUNICATION_FAILURE
1834
+ )
1835
+ }
1687
1836
  }
1688
1837
 
1689
1838
  /**
@@ -1722,7 +1871,14 @@ export class YotoPlayerAccessory {
1722
1871
  LOG_PREFIX.ACCESSORY,
1723
1872
  `[${this.#device.name}] Set night max volume limit raw=${value} normalizedPercent=${normalizedPercent} requestedSteps=${requestedSteps} -> steps=${limit} percent=${limitPercent}`
1724
1873
  )
1725
- await this.#deviceModel.updateConfig({ nightMaxVolumeLimit: limit })
1874
+ try {
1875
+ await this.#deviceModel.updateConfig({ nightMaxVolumeLimit: limit })
1876
+ } catch (error) {
1877
+ this.#log.error(LOG_PREFIX.ACCESSORY, `[${this.#device.name}] Failed to set night max volume limit:`, error)
1878
+ throw new this.#platform.api.hap.HapStatusError(
1879
+ this.#platform.api.hap.HAPStatus.SERVICE_COMMUNICATION_FAILURE
1880
+ )
1881
+ }
1726
1882
  }
1727
1883
 
1728
1884
  // ==================== Characteristic Update Methods ====================
@@ -1746,6 +1902,8 @@ export class YotoPlayerAccessory {
1746
1902
  * @param {number} volumeSteps - Volume level (0-16)
1747
1903
  */
1748
1904
  updateVolumeCharacteristic (volumeSteps) {
1905
+ if (!this.volumeService) return
1906
+
1749
1907
  if (volumeSteps > 0) {
1750
1908
  this.#lastNonZeroVolume = Math.round((volumeSteps / 16) * 100)
1751
1909
  }
@@ -1757,7 +1915,6 @@ export class YotoPlayerAccessory {
1757
1915
  LOG_PREFIX.ACCESSORY,
1758
1916
  `[${this.#device.name}] Update volume characteristic rawSteps=${volumeSteps} percent=${percent}`
1759
1917
  )
1760
- if (!this.volumeService) return
1761
1918
 
1762
1919
  const { Characteristic } = this.#platform
1763
1920
  this.volumeService
@@ -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
@@ -127,13 +127,20 @@ export class YotoPlatform {
127
127
  }
128
128
  })
129
129
 
130
+ const formatError = (/** @type {unknown} */ error) => (
131
+ error instanceof Error ? (error.stack || error.message) : String(error)
132
+ )
133
+
130
134
  // Listen to account-level events
131
135
  this.yotoAccount.on('error', ({ error, context }) => {
136
+ const details = formatError(error)
132
137
  if (context.deviceId) {
133
138
  const label = this.formatDeviceLabel(context.deviceId)
134
- log.error(`Device error [${label} ${context.operation} ${context.source}]:`, error.message)
139
+ log.error(`Device error [${label} ${context.operation} ${context.source}]:`, details)
140
+ log.debug('Device error context:', context)
135
141
  } else {
136
- log.error('Account error:', error.message)
142
+ log.error('Account error:', details)
143
+ log.debug('Account error context:', context)
137
144
  }
138
145
  })
139
146
 
@@ -163,7 +170,7 @@ export class YotoPlatform {
163
170
  */
164
171
  configureAccessory (accessory) {
165
172
  const { log, accessories, cardAccessories } = this
166
- log.debug('Loading accessory from cache:', accessory.displayName)
173
+ log.debug('Loading accessory from cache:', accessory.displayName, accessory.UUID)
167
174
 
168
175
  const context = accessory.context
169
176
  const record = context && typeof context === 'object'
@@ -172,6 +179,7 @@ export class YotoPlatform {
172
179
  const accessoryType = record && typeof record['type'] === 'string'
173
180
  ? record['type']
174
181
  : undefined
182
+ log.debug('Cached accessory context type:', accessory.displayName, accessoryType ?? 'device')
175
183
 
176
184
  if (accessoryType === 'card-control' || record?.['cardControl']) {
177
185
  cardAccessories.set(accessory.UUID, /** @type {PlatformAccessory<YotoCardAccessoryContext>} */ (accessory))
@@ -193,6 +201,7 @@ export class YotoPlatform {
193
201
 
194
202
  try {
195
203
  this.log.debug('Starting Yoto account...')
204
+ this.log.debug('Playback accessory mode:', this.playbackAccessoryConfig.mode)
196
205
 
197
206
  // Listen for devices being added
198
207
  this.yotoAccount.on('deviceAdded', async ({ deviceId }) => {
@@ -205,6 +214,7 @@ export class YotoPlatform {
205
214
 
206
215
  const device = deviceModel.device
207
216
  this.log.info(`Device discovered: ${device.name} (${deviceId})`)
217
+ this.log.debug('Registering device from account discovery:', device.name, deviceId)
208
218
  await this.registerDevice(device, deviceModel)
209
219
  })
210
220
 
@@ -327,10 +337,15 @@ export class YotoPlatform {
327
337
  await this.yotoAccount.start()
328
338
 
329
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
+ )
330
344
 
331
345
  // Remove stale accessories after all devices are registered
332
346
  this.removeStaleAccessories()
333
347
 
348
+ this.log.debug('Registering card control accessories (playOnAll).')
334
349
  await this.registerCardControlAccessories()
335
350
  } catch (error) {
336
351
  this.log.error('Failed to start account:', error instanceof Error ? error.message : String(error))
@@ -394,28 +409,24 @@ export class YotoPlatform {
394
409
  const uuid = this.api.hap.uuid.generate(device.deviceId)
395
410
  const sanitizedDeviceName = sanitizeName(device.name)
396
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
+ )
397
418
 
398
419
  // Check if accessory already exists
399
420
  const existingAccessory = this.accessories.get(uuid)
400
421
 
401
422
  if (existingAccessory) {
402
423
  // Accessory exists - update it
403
- this.log.debug('Restoring existing accessory from cache:', device.name)
424
+ this.log.debug('Restoring existing accessory from cache:', device.name, existingAccessory.UUID)
404
425
 
405
426
  // Update display name if it has changed
406
427
  if (existingAccessory.displayName !== sanitizedDeviceName) {
428
+ this.log.debug('Updating accessory display name:', existingAccessory.displayName, '->', sanitizedDeviceName)
407
429
  existingAccessory.updateDisplayName(sanitizedDeviceName)
408
- const infoService = existingAccessory.getService(this.api.hap.Service.AccessoryInformation)
409
- if (infoService) {
410
- // Only update Name, preserve user's ConfiguredName customization
411
- // ConfiguredName is intentionally NOT updated here because:
412
- // - It allows users to rename accessories in the Home app
413
- // - Their custom names should survive Homebridge restarts and Yoto device name changes
414
- // - Name stays in sync with Yoto's device name for plugin identification
415
- infoService
416
- .setCharacteristic(this.api.hap.Characteristic.Name, sanitizedDeviceName)
417
- .setCharacteristic(this.api.hap.Characteristic.ConfiguredName, sanitizedDeviceName)
418
- }
419
430
  }
420
431
 
421
432
  // Update context with fresh device data
@@ -432,6 +443,7 @@ export class YotoPlatform {
432
443
 
433
444
  // Update accessory information
434
445
  this.api.updatePlatformAccessories([existingAccessory])
446
+ this.log.debug('Updated accessory cache entry:', existingAccessory.displayName, existingAccessory.UUID)
435
447
 
436
448
  // Create handler for this accessory with device model
437
449
  const handler = new YotoPlayerAccessory({
@@ -442,9 +454,11 @@ export class YotoPlatform {
442
454
 
443
455
  // Track handler
444
456
  this.accessoryHandlers.set(uuid, handler)
457
+ this.log.debug('Created accessory handler:', existingAccessory.displayName, uuid)
445
458
 
446
459
  // Initialize accessory (setup services and event listeners)
447
460
  await handler.setup()
461
+ this.log.debug('Accessory setup complete:', existingAccessory.displayName)
448
462
 
449
463
  if (this.playbackAccessoryConfig.mode === 'external') {
450
464
  await this.registerSpeakerAccessory(device, deviceModel)
@@ -453,21 +467,13 @@ export class YotoPlatform {
453
467
  return { success: true }
454
468
  } else {
455
469
  // Create new accessory
456
- this.log.debug('Adding new accessory:', device.name)
470
+ this.log.debug('Adding new accessory:', device.name, uuid)
457
471
 
458
472
  // Create platform accessory
459
473
  /** @type {PlatformAccessory<YotoAccessoryContext>} */
460
474
  // eslint-disable-next-line new-cap
461
475
  const accessory = new this.api.platformAccessory(sanitizedDeviceName, uuid, accessoryCategory)
462
476
 
463
- // Set Name and ConfiguredName on AccessoryInformation service
464
- const infoService = accessory.getService(this.api.hap.Service.AccessoryInformation)
465
- if (infoService) {
466
- infoService
467
- .setCharacteristic(this.api.hap.Characteristic.Name, sanitizedDeviceName)
468
- .setCharacteristic(this.api.hap.Characteristic.ConfiguredName, sanitizedDeviceName)
469
- }
470
-
471
477
  // Set accessory context
472
478
  accessory.context = {
473
479
  type: 'device',
@@ -483,13 +489,16 @@ export class YotoPlatform {
483
489
 
484
490
  // Track handler
485
491
  this.accessoryHandlers.set(uuid, handler)
492
+ this.log.debug('Created accessory handler:', device.name, uuid)
486
493
 
487
494
  // Initialize accessory (setup services and event listeners)
488
495
  await handler.setup()
496
+ this.log.debug('Accessory setup complete:', device.name)
489
497
 
490
498
  // Register as a platform accessory (bridged).
491
499
  this.log.debug(`Registering new accessory: ${device.name}`)
492
500
  this.api.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [accessory])
501
+ this.log.debug('Registered platform accessory:', device.name, uuid)
493
502
 
494
503
  if (this.playbackAccessoryConfig.mode === 'external') {
495
504
  await this.registerSpeakerAccessory(device, deviceModel)
@@ -497,6 +506,7 @@ export class YotoPlatform {
497
506
 
498
507
  // Add to our tracking map (cast to typed version)
499
508
  this.accessories.set(uuid, accessory)
509
+ this.log.debug('Tracked new accessory:', device.name, uuid)
500
510
 
501
511
  return { success: true }
502
512
  }
@@ -512,11 +522,12 @@ export class YotoPlatform {
512
522
  const uuid = this.getSpeakerAccessoryUuid(device.deviceId)
513
523
  const speakerName = this.getSpeakerAccessoryName(device)
514
524
  if (this.speakerAccessories.has(uuid)) {
515
- this.log.debug('SmartSpeaker accessory already published:', speakerName)
525
+ this.log.debug('SmartSpeaker accessory already published:', speakerName, uuid)
516
526
  return { success: true }
517
527
  }
518
528
 
519
529
  this.log.info('Adding new SmartSpeaker accessory:', speakerName)
530
+ this.log.debug('Creating SmartSpeaker accessory:', speakerName, uuid)
520
531
 
521
532
  /** @type {PlatformAccessory<YotoAccessoryContext>} */
522
533
  // eslint-disable-next-line new-cap
@@ -526,13 +537,6 @@ export class YotoPlatform {
526
537
  this.api.hap.Categories.SPEAKER
527
538
  )
528
539
 
529
- const infoService = accessory.getService(this.api.hap.Service.AccessoryInformation)
530
- if (infoService) {
531
- infoService
532
- .setCharacteristic(this.api.hap.Characteristic.Name, speakerName)
533
- .setCharacteristic(this.api.hap.Characteristic.ConfiguredName, speakerName)
534
- }
535
-
536
540
  accessory.context = {
537
541
  device,
538
542
  }
@@ -544,11 +548,14 @@ export class YotoPlatform {
544
548
  })
545
549
 
546
550
  this.speakerAccessoryHandlers.set(uuid, handler)
551
+ this.log.debug('Created SmartSpeaker handler:', speakerName, uuid)
547
552
 
548
553
  await handler.setup()
554
+ this.log.debug('SmartSpeaker setup complete:', speakerName)
549
555
 
550
556
  this.log.info(`Publishing external SmartSpeaker accessory: ${speakerName}`)
551
557
  this.api.publishExternalAccessories(PLUGIN_NAME, [accessory])
558
+ this.log.debug('Published external SmartSpeaker accessory:', speakerName, uuid)
552
559
 
553
560
  this.speakerAccessories.set(uuid, accessory)
554
561
 
@@ -562,24 +569,21 @@ export class YotoPlatform {
562
569
  async registerCardControlAccessories () {
563
570
  const cardControls = getCardControlConfigs(this.config).filter(control => control.playOnAll)
564
571
  const desiredUuids = new Set()
572
+ this.log.debug('Card control configs (playOnAll):', cardControls.length)
565
573
 
566
574
  for (const control of cardControls) {
567
575
  const uuid = this.getCardControlAccessoryUuid(control)
568
576
  const accessoryName = this.getCardControlAccessoryName(control)
569
577
  desiredUuids.add(uuid)
578
+ this.log.debug('Ensuring card control accessory:', accessoryName, uuid)
570
579
 
571
580
  const existingAccessory = this.cardAccessories.get(uuid)
572
581
  if (existingAccessory) {
573
- this.log.debug('Restoring existing card control accessory from cache:', accessoryName)
582
+ this.log.debug('Restoring existing card control accessory from cache:', accessoryName, uuid)
574
583
 
575
584
  if (existingAccessory.displayName !== accessoryName) {
585
+ this.log.debug('Updating card control display name:', existingAccessory.displayName, '->', accessoryName)
576
586
  existingAccessory.updateDisplayName(accessoryName)
577
- const infoService = existingAccessory.getService(this.api.hap.Service.AccessoryInformation)
578
- if (infoService) {
579
- infoService
580
- .setCharacteristic(this.api.hap.Characteristic.Name, accessoryName)
581
- .setCharacteristic(this.api.hap.Characteristic.ConfiguredName, accessoryName)
582
- }
583
587
  }
584
588
 
585
589
  existingAccessory.context = {
@@ -588,6 +592,7 @@ export class YotoPlatform {
588
592
  }
589
593
 
590
594
  this.api.updatePlatformAccessories([existingAccessory])
595
+ this.log.debug('Updated card control cache entry:', existingAccessory.displayName, uuid)
591
596
 
592
597
  const existingHandler = this.cardAccessoryHandlers.get(uuid)
593
598
  if (existingHandler) {
@@ -605,10 +610,11 @@ export class YotoPlatform {
605
610
 
606
611
  this.cardAccessoryHandlers.set(uuid, handler)
607
612
  await handler.setup()
613
+ this.log.debug('Card control setup complete:', accessoryName)
608
614
  continue
609
615
  }
610
616
 
611
- this.log.debug('Adding new card control accessory:', accessoryName)
617
+ this.log.debug('Adding new card control accessory:', accessoryName, uuid)
612
618
 
613
619
  /** @type {PlatformAccessory<YotoCardAccessoryContext>} */
614
620
  // eslint-disable-next-line new-cap
@@ -618,13 +624,6 @@ export class YotoPlatform {
618
624
  this.api.hap.Categories.SWITCH
619
625
  )
620
626
 
621
- const infoService = accessory.getService(this.api.hap.Service.AccessoryInformation)
622
- if (infoService) {
623
- infoService
624
- .setCharacteristic(this.api.hap.Characteristic.Name, accessoryName)
625
- .setCharacteristic(this.api.hap.Characteristic.ConfiguredName, accessoryName)
626
- }
627
-
628
627
  accessory.context = {
629
628
  type: 'card-control',
630
629
  cardControl: control,
@@ -638,11 +637,14 @@ export class YotoPlatform {
638
637
 
639
638
  this.cardAccessoryHandlers.set(uuid, handler)
640
639
  await handler.setup()
640
+ this.log.debug('Card control setup complete:', accessoryName)
641
641
 
642
642
  this.log.debug(`Registering card control accessory: ${accessoryName}`)
643
643
  this.api.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [accessory])
644
+ this.log.debug('Registered card control accessory:', accessoryName, uuid)
644
645
 
645
646
  this.cardAccessories.set(uuid, accessory)
647
+ this.log.debug('Tracked card control accessory:', accessoryName, uuid)
646
648
  }
647
649
 
648
650
  for (const [uuid, accessory] of this.cardAccessories) {
@@ -650,7 +652,7 @@ export class YotoPlatform {
650
652
  continue
651
653
  }
652
654
 
653
- this.log.debug('Removing card control accessory from cache:', accessory.displayName)
655
+ this.log.debug('Removing card control accessory from cache:', accessory.displayName, uuid)
654
656
 
655
657
  const handler = this.cardAccessoryHandlers.get(uuid)
656
658
  if (handler) {
@@ -662,6 +664,7 @@ export class YotoPlatform {
662
664
 
663
665
  this.api.unregisterPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [accessory])
664
666
  this.cardAccessories.delete(uuid)
667
+ this.log.debug('Removed card control accessory:', accessory.displayName, uuid)
665
668
  }
666
669
  }
667
670
 
@@ -676,10 +679,16 @@ export class YotoPlatform {
676
679
  // Get current device IDs from account
677
680
  const currentDeviceIds = this.yotoAccount.getDeviceIds()
678
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
+ )
679
688
 
680
689
  for (const [uuid, accessory] of this.accessories) {
681
690
  if (!currentUUIDs.includes(uuid)) {
682
- this.log.debug('Removing existing accessory from cache:', accessory.displayName)
691
+ this.log.debug('Removing existing accessory from cache:', accessory.displayName, uuid)
683
692
 
684
693
  // Stop handler if it exists
685
694
  const handler = this.accessoryHandlers.get(uuid)
@@ -692,16 +701,18 @@ export class YotoPlatform {
692
701
 
693
702
  // Unregister from Homebridge
694
703
  this.api.unregisterPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [accessory])
704
+ this.log.debug('Unregistered accessory from Homebridge:', accessory.displayName, uuid)
695
705
 
696
706
  // Remove from our tracking map
697
707
  this.accessories.delete(uuid)
708
+ this.log.debug('Removed accessory from tracking map:', accessory.displayName, uuid)
698
709
  }
699
710
  }
700
711
 
701
712
  for (const [uuid, accessory] of this.speakerAccessories) {
702
713
  const deviceId = accessory.context.device?.deviceId
703
714
  if (!deviceId || !currentDeviceIds.includes(deviceId)) {
704
- this.log.debug('Removing external SmartSpeaker accessory from runtime:', accessory.displayName)
715
+ this.log.debug('Removing external SmartSpeaker accessory from runtime:', accessory.displayName, uuid)
705
716
 
706
717
  const handler = this.speakerAccessoryHandlers.get(uuid)
707
718
  if (handler) {
@@ -712,6 +723,7 @@ export class YotoPlatform {
712
723
  }
713
724
 
714
725
  this.speakerAccessories.delete(uuid)
726
+ this.log.debug('Removed external SmartSpeaker accessory:', accessory.displayName, uuid)
715
727
  }
716
728
  }
717
729
  }
@@ -721,6 +733,12 @@ export class YotoPlatform {
721
733
  */
722
734
  async shutdown () {
723
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
+ )
724
742
 
725
743
  // Stop all accessory handlers
726
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,
@@ -247,7 +257,8 @@ export class YotoSpeakerAccessory {
247
257
  })
248
258
 
249
259
  this.#deviceModel.on('error', (error) => {
250
- this.#log.error(`[${this.#device.name}] Speaker device error:`, error.message)
260
+ const details = error instanceof Error ? (error.stack || error.message) : String(error)
261
+ this.#log.error(`[${this.#device.name}] Speaker device error:`, details)
251
262
  })
252
263
  }
253
264
 
@@ -284,7 +295,12 @@ export class YotoSpeakerAccessory {
284
295
  */
285
296
  async getCurrentMediaState () {
286
297
  const playbackStatus = this.#deviceModel.playback.playbackStatus ?? null
287
- 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
288
304
  }
289
305
 
290
306
  /**
@@ -293,7 +309,12 @@ export class YotoSpeakerAccessory {
293
309
  */
294
310
  async getTargetMediaState () {
295
311
  const playbackStatus = this.#deviceModel.playback.playbackStatus ?? null
296
- 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
297
318
  }
298
319
 
299
320
  /**
@@ -336,7 +357,12 @@ export class YotoSpeakerAccessory {
336
357
  const volumeSteps = this.#deviceModel.status.volume
337
358
  const normalizedSteps = Number.isFinite(volumeSteps) ? volumeSteps : 0
338
359
  const clampedSteps = Math.max(0, Math.min(normalizedSteps, 16))
339
- 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
340
366
  }
341
367
 
342
368
  /**
@@ -379,7 +405,9 @@ export class YotoSpeakerAccessory {
379
405
  * @returns {Promise<CharacteristicValue>}
380
406
  */
381
407
  async getMute () {
382
- 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
383
411
  }
384
412
 
385
413
  /**
@@ -404,7 +432,9 @@ export class YotoSpeakerAccessory {
404
432
  * @returns {Promise<CharacteristicValue>}
405
433
  */
406
434
  async getStatusActive () {
407
- 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
408
438
  }
409
439
 
410
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.40",
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"