homebridge-yoto 0.0.40 → 0.0.41

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) {
@@ -917,7 +921,8 @@ export class YotoPlayerAccessory {
917
921
  })
918
922
 
919
923
  this.#deviceModel.on('error', (error) => {
920
- this.#log.error(`[${this.#device.name}] Device error:`, error.message)
924
+ const details = error instanceof Error ? (error.stack || error.message) : String(error)
925
+ this.#log.error(`[${this.#device.name}] Device error:`, details)
921
926
  })
922
927
  }
923
928
 
@@ -1186,19 +1191,26 @@ export class YotoPlayerAccessory {
1186
1191
  * @param {CharacteristicValue} value
1187
1192
  */
1188
1193
  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
1194
+ try {
1195
+ if (value) {
1196
+ // Turn ON - restore previous color or default to white
1197
+ const colorToSet = this.#lastDayColor || '0xffffff'
1198
+ this.#log.debug(LOG_PREFIX.ACCESSORY, `Turning day nightlight ON with color: ${colorToSet}`)
1199
+ await this.#deviceModel.updateConfig({ ambientColour: colorToSet })
1200
+ } else {
1201
+ // Turn OFF - save current color and set to black
1202
+ const currentColor = this.#deviceModel.config.ambientColour
1203
+ if (!this.isColorOff(currentColor)) {
1204
+ this.#lastDayColor = currentColor
1205
+ }
1206
+ this.#log.debug(LOG_PREFIX.ACCESSORY, 'Turning day nightlight OFF')
1207
+ await this.#deviceModel.updateConfig({ ambientColour: '0x000000' })
1199
1208
  }
1200
- this.#log.debug(LOG_PREFIX.ACCESSORY, 'Turning day nightlight OFF')
1201
- await this.#deviceModel.updateConfig({ ambientColour: '0x000000' })
1209
+ } catch (error) {
1210
+ this.#log.error(LOG_PREFIX.ACCESSORY, `[${this.#device.name}] Failed to set day nightlight:`, error)
1211
+ throw new this.#platform.api.hap.HapStatusError(
1212
+ this.#platform.api.hap.HAPStatus.SERVICE_COMMUNICATION_FAILURE
1213
+ )
1202
1214
  }
1203
1215
  }
1204
1216
 
@@ -1228,10 +1240,17 @@ export class YotoPlayerAccessory {
1228
1240
 
1229
1241
  const brightnessValue = Math.max(0, Math.min(Math.round(rawBrightness), 100))
1230
1242
  this.#log.debug(LOG_PREFIX.ACCESSORY, `Setting day display brightness: ${brightnessValue}`)
1231
- await this.#deviceModel.updateConfig({
1232
- dayDisplayBrightness: brightnessValue,
1233
- dayDisplayBrightnessAuto: false
1234
- })
1243
+ try {
1244
+ await this.#deviceModel.updateConfig({
1245
+ dayDisplayBrightness: brightnessValue,
1246
+ dayDisplayBrightnessAuto: false
1247
+ })
1248
+ } catch (error) {
1249
+ this.#log.error(LOG_PREFIX.ACCESSORY, `[${this.#device.name}] Failed to set day brightness:`, error)
1250
+ throw new this.#platform.api.hap.HapStatusError(
1251
+ this.#platform.api.hap.HAPStatus.SERVICE_COMMUNICATION_FAILURE
1252
+ )
1253
+ }
1235
1254
  }
1236
1255
 
1237
1256
  /**
@@ -1275,7 +1294,14 @@ export class YotoPlayerAccessory {
1275
1294
  const formattedColor = this.formatHexColor(newHex)
1276
1295
 
1277
1296
  this.#log.debug(LOG_PREFIX.ACCESSORY, `Setting day nightlight hue: ${hue}° → ${formattedColor}`)
1278
- await this.#deviceModel.updateConfig({ ambientColour: formattedColor })
1297
+ try {
1298
+ await this.#deviceModel.updateConfig({ ambientColour: formattedColor })
1299
+ } catch (error) {
1300
+ this.#log.error(LOG_PREFIX.ACCESSORY, `[${this.#device.name}] Failed to set day nightlight hue:`, error)
1301
+ throw new this.#platform.api.hap.HapStatusError(
1302
+ this.#platform.api.hap.HAPStatus.SERVICE_COMMUNICATION_FAILURE
1303
+ )
1304
+ }
1279
1305
  }
1280
1306
 
1281
1307
  /**
@@ -1319,7 +1345,14 @@ export class YotoPlayerAccessory {
1319
1345
  const formattedColor = this.formatHexColor(newHex)
1320
1346
 
1321
1347
  this.#log.debug(LOG_PREFIX.ACCESSORY, `Setting day nightlight saturation: ${saturation}% → ${formattedColor}`)
1322
- await this.#deviceModel.updateConfig({ ambientColour: formattedColor })
1348
+ try {
1349
+ await this.#deviceModel.updateConfig({ ambientColour: formattedColor })
1350
+ } catch (error) {
1351
+ this.#log.error(LOG_PREFIX.ACCESSORY, `[${this.#device.name}] Failed to set day nightlight saturation:`, error)
1352
+ throw new this.#platform.api.hap.HapStatusError(
1353
+ this.#platform.api.hap.HAPStatus.SERVICE_COMMUNICATION_FAILURE
1354
+ )
1355
+ }
1323
1356
  }
1324
1357
 
1325
1358
  // ---------- Night Nightlight Handlers ----------
@@ -1338,19 +1371,26 @@ export class YotoPlayerAccessory {
1338
1371
  * @param {CharacteristicValue} value
1339
1372
  */
1340
1373
  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
1374
+ try {
1375
+ if (value) {
1376
+ // Turn ON - restore previous color or default to white
1377
+ const colorToSet = this.#lastNightColor || '0xffffff'
1378
+ this.#log.debug(LOG_PREFIX.ACCESSORY, `Turning night nightlight ON with color: ${colorToSet}`)
1379
+ await this.#deviceModel.updateConfig({ nightAmbientColour: colorToSet })
1380
+ } else {
1381
+ // Turn OFF - save current color and set to black
1382
+ const currentColor = this.#deviceModel.config.nightAmbientColour
1383
+ if (!this.isColorOff(currentColor)) {
1384
+ this.#lastNightColor = currentColor
1385
+ }
1386
+ this.#log.debug(LOG_PREFIX.ACCESSORY, 'Turning night nightlight OFF')
1387
+ await this.#deviceModel.updateConfig({ nightAmbientColour: '0x000000' })
1351
1388
  }
1352
- this.#log.debug(LOG_PREFIX.ACCESSORY, 'Turning night nightlight OFF')
1353
- await this.#deviceModel.updateConfig({ nightAmbientColour: '0x000000' })
1389
+ } catch (error) {
1390
+ this.#log.error(LOG_PREFIX.ACCESSORY, `[${this.#device.name}] Failed to set night nightlight:`, error)
1391
+ throw new this.#platform.api.hap.HapStatusError(
1392
+ this.#platform.api.hap.HAPStatus.SERVICE_COMMUNICATION_FAILURE
1393
+ )
1354
1394
  }
1355
1395
  }
1356
1396
 
@@ -1380,10 +1420,17 @@ export class YotoPlayerAccessory {
1380
1420
 
1381
1421
  const brightnessValue = Math.max(0, Math.min(Math.round(rawBrightness), 100))
1382
1422
  this.#log.debug(LOG_PREFIX.ACCESSORY, `Setting night display brightness: ${brightnessValue}`)
1383
- await this.#deviceModel.updateConfig({
1384
- nightDisplayBrightness: brightnessValue,
1385
- nightDisplayBrightnessAuto: false
1386
- })
1423
+ try {
1424
+ await this.#deviceModel.updateConfig({
1425
+ nightDisplayBrightness: brightnessValue,
1426
+ nightDisplayBrightnessAuto: false
1427
+ })
1428
+ } catch (error) {
1429
+ this.#log.error(LOG_PREFIX.ACCESSORY, `[${this.#device.name}] Failed to set night brightness:`, error)
1430
+ throw new this.#platform.api.hap.HapStatusError(
1431
+ this.#platform.api.hap.HAPStatus.SERVICE_COMMUNICATION_FAILURE
1432
+ )
1433
+ }
1387
1434
  }
1388
1435
 
1389
1436
  /**
@@ -1427,7 +1474,14 @@ export class YotoPlayerAccessory {
1427
1474
  const formattedColor = this.formatHexColor(newHex)
1428
1475
 
1429
1476
  this.#log.debug(LOG_PREFIX.ACCESSORY, `Setting night nightlight hue: ${hue}° → ${formattedColor}`)
1430
- await this.#deviceModel.updateConfig({ nightAmbientColour: formattedColor })
1477
+ try {
1478
+ await this.#deviceModel.updateConfig({ nightAmbientColour: formattedColor })
1479
+ } catch (error) {
1480
+ this.#log.error(LOG_PREFIX.ACCESSORY, `[${this.#device.name}] Failed to set night nightlight hue:`, error)
1481
+ throw new this.#platform.api.hap.HapStatusError(
1482
+ this.#platform.api.hap.HAPStatus.SERVICE_COMMUNICATION_FAILURE
1483
+ )
1484
+ }
1431
1485
  }
1432
1486
 
1433
1487
  /**
@@ -1471,7 +1525,14 @@ export class YotoPlayerAccessory {
1471
1525
  const formattedColor = this.formatHexColor(newHex)
1472
1526
 
1473
1527
  this.#log.debug(LOG_PREFIX.ACCESSORY, `Setting night nightlight saturation: ${saturation}% → ${formattedColor}`)
1474
- 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 saturation:`, error)
1532
+ throw new this.#platform.api.hap.HapStatusError(
1533
+ this.#platform.api.hap.HAPStatus.SERVICE_COMMUNICATION_FAILURE
1534
+ )
1535
+ }
1475
1536
  }
1476
1537
 
1477
1538
  // ==================== Nightlight Status ContactSensor Getters ====================
@@ -1594,7 +1655,14 @@ export class YotoPlayerAccessory {
1594
1655
  async setBluetoothState (value) {
1595
1656
  const enabled = Boolean(value)
1596
1657
  this.#log.debug(LOG_PREFIX.ACCESSORY, `Setting Bluetooth: ${enabled ? 'ON' : 'OFF'}`)
1597
- await this.#deviceModel.updateConfig({ bluetoothEnabled: enabled })
1658
+ try {
1659
+ await this.#deviceModel.updateConfig({ bluetoothEnabled: enabled })
1660
+ } catch (error) {
1661
+ this.#log.error(LOG_PREFIX.ACCESSORY, `[${this.#device.name}] Failed to set Bluetooth:`, error)
1662
+ throw new this.#platform.api.hap.HapStatusError(
1663
+ this.#platform.api.hap.HAPStatus.SERVICE_COMMUNICATION_FAILURE
1664
+ )
1665
+ }
1598
1666
  }
1599
1667
 
1600
1668
  // ==================== Card Control Switch Setter ====================
@@ -1683,7 +1751,14 @@ export class YotoPlayerAccessory {
1683
1751
  LOG_PREFIX.ACCESSORY,
1684
1752
  `[${this.#device.name}] Set day max volume limit raw=${value} normalizedPercent=${normalizedPercent} requestedSteps=${requestedSteps} -> steps=${limit} percent=${limitPercent}`
1685
1753
  )
1686
- await this.#deviceModel.updateConfig({ maxVolumeLimit: limit })
1754
+ try {
1755
+ await this.#deviceModel.updateConfig({ maxVolumeLimit: limit })
1756
+ } catch (error) {
1757
+ this.#log.error(LOG_PREFIX.ACCESSORY, `[${this.#device.name}] Failed to set day max volume limit:`, error)
1758
+ throw new this.#platform.api.hap.HapStatusError(
1759
+ this.#platform.api.hap.HAPStatus.SERVICE_COMMUNICATION_FAILURE
1760
+ )
1761
+ }
1687
1762
  }
1688
1763
 
1689
1764
  /**
@@ -1722,7 +1797,14 @@ export class YotoPlayerAccessory {
1722
1797
  LOG_PREFIX.ACCESSORY,
1723
1798
  `[${this.#device.name}] Set night max volume limit raw=${value} normalizedPercent=${normalizedPercent} requestedSteps=${requestedSteps} -> steps=${limit} percent=${limitPercent}`
1724
1799
  )
1725
- await this.#deviceModel.updateConfig({ nightMaxVolumeLimit: limit })
1800
+ try {
1801
+ await this.#deviceModel.updateConfig({ nightMaxVolumeLimit: limit })
1802
+ } catch (error) {
1803
+ this.#log.error(LOG_PREFIX.ACCESSORY, `[${this.#device.name}] Failed to set night max volume limit:`, error)
1804
+ throw new this.#platform.api.hap.HapStatusError(
1805
+ this.#platform.api.hap.HAPStatus.SERVICE_COMMUNICATION_FAILURE
1806
+ )
1807
+ }
1726
1808
  }
1727
1809
 
1728
1810
  // ==================== Characteristic Update Methods ====================
@@ -1746,6 +1828,8 @@ export class YotoPlayerAccessory {
1746
1828
  * @param {number} volumeSteps - Volume level (0-16)
1747
1829
  */
1748
1830
  updateVolumeCharacteristic (volumeSteps) {
1831
+ if (!this.volumeService) return
1832
+
1749
1833
  if (volumeSteps > 0) {
1750
1834
  this.#lastNonZeroVolume = Math.round((volumeSteps / 16) * 100)
1751
1835
  }
@@ -1757,7 +1841,6 @@ export class YotoPlayerAccessory {
1757
1841
  LOG_PREFIX.ACCESSORY,
1758
1842
  `[${this.#device.name}] Update volume characteristic rawSteps=${volumeSteps} percent=${percent}`
1759
1843
  )
1760
- if (!this.volumeService) return
1761
1844
 
1762
1845
  const { Characteristic } = this.#platform
1763
1846
  this.volumeService
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
 
@@ -247,7 +247,8 @@ export class YotoSpeakerAccessory {
247
247
  })
248
248
 
249
249
  this.#deviceModel.on('error', (error) => {
250
- this.#log.error(`[${this.#device.name}] Speaker device error:`, error.message)
250
+ const details = error instanceof Error ? (error.stack || error.message) : String(error)
251
+ this.#log.error(`[${this.#device.name}] Speaker device error:`, details)
251
252
  })
252
253
  }
253
254
 
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.41",
5
5
  "author": "Bret Comnes <bcomnes@gmail.com> (https://bret.io)",
6
6
  "bugs": {
7
7
  "url": "https://github.com/bcomnes/homebridge-yoto/issues"