homebridge-yoto 0.0.39 → 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.
package/lib/accessory.js CHANGED
@@ -8,6 +8,7 @@
8
8
  /** @import { YotoDevice } from 'yoto-nodejs-client/lib/api-endpoints/devices.js' */
9
9
  /** @import { YotoAccessoryContext } from './platform.js' */
10
10
  /** @import { ServiceSchemaKey } from '../config.schema.cjs' */
11
+ /** @import { CardControlConfig } from './card-controls.js' */
11
12
 
12
13
  /**
13
14
  * Device capabilities detected from metadata
@@ -22,10 +23,11 @@
22
23
  * @typedef {Object} YotoServiceToggles
23
24
  * @property {boolean} playback
24
25
  * @property {boolean} volume
26
+ * @property {boolean} battery
25
27
  * @property {boolean} temperature
26
28
  * @property {boolean} nightlight
27
29
  * @property {boolean} cardSlot
28
- * @property {boolean} nightMode
30
+ * @property {boolean} dayMode
29
31
  * @property {boolean} sleepTimer
30
32
  * @property {boolean} bluetooth
31
33
  * @property {boolean} volumeLimits
@@ -41,6 +43,8 @@ import {
41
43
  import { sanitizeName } from './sanitize-name.js'
42
44
  import { syncServiceNames } from './sync-service-names.js'
43
45
  import { serviceSchema } from '../config.schema.cjs'
46
+ import { getPlaybackAccessoryConfig } from './service-config.js'
47
+ import { getCardControlConfigs } from './card-controls.js'
44
48
 
45
49
  // Use syncServiceNames for every visible service so HomeKit labels stay stable.
46
50
  // Exceptions: AccessoryInformation (named in platform) and Battery (set Characteristic.Name only).
@@ -60,7 +64,7 @@ function getBooleanSetting (value, fallback) {
60
64
  */
61
65
  function getServiceDefault (key) {
62
66
  const entry = serviceSchema[key]
63
- if (entry?.default !== undefined && typeof entry.default === 'boolean') {
67
+ if (entry && 'default' in entry && typeof entry.default === 'boolean') {
64
68
  return entry.default
65
69
  }
66
70
  return false
@@ -87,7 +91,7 @@ export class YotoPlayerAccessory {
87
91
  /** @type {Service | undefined} */ dayNightlightActiveService
88
92
  /** @type {Service | undefined} */ nightNightlightActiveService
89
93
  /** @type {Service | undefined} */ cardSlotService
90
- /** @type {Service | undefined} */ nightModeService
94
+ /** @type {Service | undefined} */ dayModeService
91
95
  /** @type {Service | undefined} */ sleepTimerService
92
96
  /** @type {Service | undefined} */ bluetoothService
93
97
  /** @type {Service | undefined} */ dayMaxVolumeService
@@ -127,14 +131,16 @@ export class YotoPlayerAccessory {
127
131
  const serviceConfig = typeof services === 'object' && services !== null
128
132
  ? /** @type {Record<string, unknown>} */ (services)
129
133
  : {}
134
+ const playbackConfig = getPlaybackAccessoryConfig(this.#platform.config)
130
135
 
131
136
  return {
132
- playback: getBooleanSetting(serviceConfig['playback'], getServiceDefault('playback')),
133
- volume: getBooleanSetting(serviceConfig['volume'], getServiceDefault('volume')),
137
+ playback: playbackConfig.playbackEnabled,
138
+ volume: playbackConfig.volumeEnabled,
139
+ battery: getBooleanSetting(serviceConfig['battery'], getServiceDefault('battery')),
134
140
  temperature: getBooleanSetting(serviceConfig['temperature'], getServiceDefault('temperature')),
135
141
  nightlight: getBooleanSetting(serviceConfig['nightlight'], getServiceDefault('nightlight')),
136
142
  cardSlot: getBooleanSetting(serviceConfig['cardSlot'], getServiceDefault('cardSlot')),
137
- nightMode: getBooleanSetting(serviceConfig['nightMode'], getServiceDefault('nightMode')),
143
+ dayMode: getBooleanSetting(serviceConfig['dayMode'], getServiceDefault('dayMode')),
138
144
  sleepTimer: getBooleanSetting(serviceConfig['sleepTimer'], getServiceDefault('sleepTimer')),
139
145
  bluetooth: getBooleanSetting(serviceConfig['bluetooth'], getServiceDefault('bluetooth')),
140
146
  volumeLimits: getBooleanSetting(serviceConfig['volumeLimits'], getServiceDefault('volumeLimits')),
@@ -173,7 +179,9 @@ export class YotoPlayerAccessory {
173
179
  this.setupVolumeService()
174
180
  }
175
181
 
176
- this.setupBatteryService()
182
+ if (serviceToggles.battery) {
183
+ this.setupBatteryService()
184
+ }
177
185
 
178
186
  // Setup optional services based on device capabilities
179
187
  if (serviceToggles.temperature && this.#deviceModel.capabilities.hasTemperatureSensor) {
@@ -188,8 +196,8 @@ export class YotoPlayerAccessory {
188
196
  if (serviceToggles.cardSlot) {
189
197
  this.setupCardSlotService()
190
198
  }
191
- if (serviceToggles.nightMode) {
192
- this.setupNightModeService()
199
+ if (serviceToggles.dayMode) {
200
+ this.setupDayModeService()
193
201
  }
194
202
  if (serviceToggles.sleepTimer) {
195
203
  this.setupSleepTimerService()
@@ -200,6 +208,7 @@ export class YotoPlayerAccessory {
200
208
  if (serviceToggles.volumeLimits) {
201
209
  this.setupVolumeLimitServices()
202
210
  }
211
+ this.setupCardControlServices()
203
212
 
204
213
  // Remove any services that aren't in our current set
205
214
  // (except AccessoryInformation which should always be preserved)
@@ -515,21 +524,21 @@ export class YotoPlayerAccessory {
515
524
  }
516
525
 
517
526
  /**
518
- * Setup night mode ContactSensor service
519
- * Shows if device is in night mode (vs day mode)
527
+ * Setup day mode ContactSensor service
528
+ * Shows if device is in day mode (vs night mode)
520
529
  */
521
- setupNightModeService () {
530
+ setupDayModeService () {
522
531
  const { Service, Characteristic } = this.#platform
523
- const serviceName = this.generateServiceName('Night Mode')
532
+ const serviceName = this.generateServiceName('Day Mode')
524
533
 
525
- const service = this.#accessory.getServiceById(Service.ContactSensor, 'NightModeStatus') ||
526
- this.#accessory.addService(Service.ContactSensor, serviceName, 'NightModeStatus')
534
+ const service = this.#accessory.getServiceById(Service.ContactSensor, 'DayModeStatus') ||
535
+ this.#accessory.addService(Service.ContactSensor, serviceName, 'DayModeStatus')
527
536
  syncServiceNames({ Characteristic, service, name: serviceName })
528
537
 
529
538
  service.getCharacteristic(Characteristic.ContactSensorState)
530
- .onGet(this.getNightModeStatus.bind(this))
539
+ .onGet(this.getDayModeStatus.bind(this))
531
540
 
532
- this.nightModeService = service
541
+ this.dayModeService = service
533
542
  this.#currentServices.add(service)
534
543
  }
535
544
 
@@ -632,6 +641,39 @@ export class YotoPlayerAccessory {
632
641
  this.#currentServices.add(nightService)
633
642
  }
634
643
 
644
+ /**
645
+ * Setup card control Switch services
646
+ */
647
+ setupCardControlServices () {
648
+ const cardControls = getCardControlConfigs(this.#platform.config)
649
+ if (cardControls.length === 0) {
650
+ return
651
+ }
652
+
653
+ const { Service, Characteristic } = this.#platform
654
+
655
+ for (const control of cardControls) {
656
+ const serviceName = this.generateServiceName(control.label)
657
+ const subtype = `CardControl:${control.id}`
658
+
659
+ const service = this.#accessory.getServiceById(Service.Switch, subtype) ||
660
+ this.#accessory.addService(Service.Switch, serviceName, subtype)
661
+
662
+ syncServiceNames({ Characteristic, service, name: serviceName })
663
+
664
+ service
665
+ .getCharacteristic(Characteristic.On)
666
+ .onGet(() => false)
667
+ .onSet(async (value) => {
668
+ await this.setCardControl(service, control, value)
669
+ })
670
+
671
+ service.updateCharacteristic(Characteristic.On, false)
672
+
673
+ this.#currentServices.add(service)
674
+ }
675
+ }
676
+
635
677
  /**
636
678
  * Setup event listeners for device model updates
637
679
  * Uses exhaustive switch pattern for type safety
@@ -677,8 +719,8 @@ export class YotoPlayerAccessory {
677
719
  break
678
720
 
679
721
  case 'dayMode':
680
- // Update night mode ContactSensor
681
- this.updateNightModeCharacteristic()
722
+ // Update day mode ContactSensor
723
+ this.updateDayModeCharacteristic()
682
724
  // Update nightlight status ContactSensors (depends on dayMode)
683
725
  if (this.#deviceModel.capabilities.hasColoredNightlight) {
684
726
  this.updateNightlightStatusCharacteristics()
@@ -879,7 +921,8 @@ export class YotoPlayerAccessory {
879
921
  })
880
922
 
881
923
  this.#deviceModel.on('error', (error) => {
882
- 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)
883
926
  })
884
927
  }
885
928
 
@@ -927,7 +970,7 @@ export class YotoPlayerAccessory {
927
970
  const percent = Math.round((clampedSteps / 16) * 100)
928
971
  this.#log.debug(
929
972
  LOG_PREFIX.ACCESSORY,
930
- `[${this.#device.name}] Get volume rawSteps=${volumeSteps} steps=${clampedSteps} percent=${percent}`
973
+ `[${this.#device.name}] Get volume rawSteps=${volumeSteps} percent=${percent}`
931
974
  )
932
975
  return percent
933
976
  }
@@ -966,13 +1009,12 @@ export class YotoPlayerAccessory {
966
1009
  await deviceModel.setVolume(steps)
967
1010
  if (this.volumeService) {
968
1011
  const { Characteristic } = this.#platform
969
-
1012
+ const clampedPercent = Math.round((steps / 16) * 100)
970
1013
  this.volumeService
971
1014
  .getCharacteristic(Characteristic.On)
972
1015
  .updateValue(steps > 0)
973
1016
 
974
1017
  if (steps !== requestedSteps || normalizedPercent !== requestedPercent) {
975
- const clampedPercent = Math.round((steps / 16) * 100)
976
1018
  this.volumeService
977
1019
  .getCharacteristic(Characteristic.Brightness)
978
1020
  .updateValue(clampedPercent)
@@ -1149,19 +1191,26 @@ export class YotoPlayerAccessory {
1149
1191
  * @param {CharacteristicValue} value
1150
1192
  */
1151
1193
  async setDayNightlightOn (value) {
1152
- if (value) {
1153
- // Turn ON - restore previous color or default to white
1154
- const colorToSet = this.#lastDayColor || '0xffffff'
1155
- this.#log.debug(LOG_PREFIX.ACCESSORY, `Turning day nightlight ON with color: ${colorToSet}`)
1156
- await this.#deviceModel.updateConfig({ ambientColour: colorToSet })
1157
- } else {
1158
- // Turn OFF - save current color and set to black
1159
- const currentColor = this.#deviceModel.config.ambientColour
1160
- if (!this.isColorOff(currentColor)) {
1161
- 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' })
1162
1208
  }
1163
- this.#log.debug(LOG_PREFIX.ACCESSORY, 'Turning day nightlight OFF')
1164
- 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
+ )
1165
1214
  }
1166
1215
  }
1167
1216
 
@@ -1191,10 +1240,17 @@ export class YotoPlayerAccessory {
1191
1240
 
1192
1241
  const brightnessValue = Math.max(0, Math.min(Math.round(rawBrightness), 100))
1193
1242
  this.#log.debug(LOG_PREFIX.ACCESSORY, `Setting day display brightness: ${brightnessValue}`)
1194
- await this.#deviceModel.updateConfig({
1195
- dayDisplayBrightness: brightnessValue,
1196
- dayDisplayBrightnessAuto: false
1197
- })
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
+ }
1198
1254
  }
1199
1255
 
1200
1256
  /**
@@ -1238,7 +1294,14 @@ export class YotoPlayerAccessory {
1238
1294
  const formattedColor = this.formatHexColor(newHex)
1239
1295
 
1240
1296
  this.#log.debug(LOG_PREFIX.ACCESSORY, `Setting day nightlight hue: ${hue}° → ${formattedColor}`)
1241
- 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
+ }
1242
1305
  }
1243
1306
 
1244
1307
  /**
@@ -1282,7 +1345,14 @@ export class YotoPlayerAccessory {
1282
1345
  const formattedColor = this.formatHexColor(newHex)
1283
1346
 
1284
1347
  this.#log.debug(LOG_PREFIX.ACCESSORY, `Setting day nightlight saturation: ${saturation}% → ${formattedColor}`)
1285
- 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
+ }
1286
1356
  }
1287
1357
 
1288
1358
  // ---------- Night Nightlight Handlers ----------
@@ -1301,19 +1371,26 @@ export class YotoPlayerAccessory {
1301
1371
  * @param {CharacteristicValue} value
1302
1372
  */
1303
1373
  async setNightNightlightOn (value) {
1304
- if (value) {
1305
- // Turn ON - restore previous color or default to white
1306
- const colorToSet = this.#lastNightColor || '0xffffff'
1307
- this.#log.debug(LOG_PREFIX.ACCESSORY, `Turning night nightlight ON with color: ${colorToSet}`)
1308
- await this.#deviceModel.updateConfig({ nightAmbientColour: colorToSet })
1309
- } else {
1310
- // Turn OFF - save current color and set to black
1311
- const currentColor = this.#deviceModel.config.nightAmbientColour
1312
- if (!this.isColorOff(currentColor)) {
1313
- 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' })
1314
1388
  }
1315
- this.#log.debug(LOG_PREFIX.ACCESSORY, 'Turning night nightlight OFF')
1316
- 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
+ )
1317
1394
  }
1318
1395
  }
1319
1396
 
@@ -1343,10 +1420,17 @@ export class YotoPlayerAccessory {
1343
1420
 
1344
1421
  const brightnessValue = Math.max(0, Math.min(Math.round(rawBrightness), 100))
1345
1422
  this.#log.debug(LOG_PREFIX.ACCESSORY, `Setting night display brightness: ${brightnessValue}`)
1346
- await this.#deviceModel.updateConfig({
1347
- nightDisplayBrightness: brightnessValue,
1348
- nightDisplayBrightnessAuto: false
1349
- })
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
+ }
1350
1434
  }
1351
1435
 
1352
1436
  /**
@@ -1390,7 +1474,14 @@ export class YotoPlayerAccessory {
1390
1474
  const formattedColor = this.formatHexColor(newHex)
1391
1475
 
1392
1476
  this.#log.debug(LOG_PREFIX.ACCESSORY, `Setting night nightlight hue: ${hue}° → ${formattedColor}`)
1393
- 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
+ }
1394
1485
  }
1395
1486
 
1396
1487
  /**
@@ -1434,7 +1525,14 @@ export class YotoPlayerAccessory {
1434
1525
  const formattedColor = this.formatHexColor(newHex)
1435
1526
 
1436
1527
  this.#log.debug(LOG_PREFIX.ACCESSORY, `Setting night nightlight saturation: ${saturation}% → ${formattedColor}`)
1437
- 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
+ }
1438
1536
  }
1439
1537
 
1440
1538
  // ==================== Nightlight Status ContactSensor Getters ====================
@@ -1489,17 +1587,17 @@ export class YotoPlayerAccessory {
1489
1587
  return hasCard ? Characteristic.ContactSensorState.CONTACT_NOT_DETECTED : Characteristic.ContactSensorState.CONTACT_DETECTED
1490
1588
  }
1491
1589
 
1492
- // ==================== Night Mode ContactSensor Getter ====================
1590
+ // ==================== Day Mode ContactSensor Getter ====================
1493
1591
 
1494
1592
  /**
1495
- * Get night mode status
1593
+ * Get day mode status
1496
1594
  * @returns {Promise<CharacteristicValue>}
1497
1595
  */
1498
- async getNightModeStatus () {
1596
+ async getDayModeStatus () {
1499
1597
  const { Characteristic } = this.#platform
1500
1598
  const status = this.#deviceModel.status
1501
- const isNightMode = status.dayMode === 'night'
1502
- return isNightMode
1599
+ const isDayMode = status.dayMode === 'day'
1600
+ return isDayMode
1503
1601
  ? Characteristic.ContactSensorState.CONTACT_NOT_DETECTED
1504
1602
  : Characteristic.ContactSensorState.CONTACT_DETECTED
1505
1603
  }
@@ -1557,7 +1655,62 @@ export class YotoPlayerAccessory {
1557
1655
  async setBluetoothState (value) {
1558
1656
  const enabled = Boolean(value)
1559
1657
  this.#log.debug(LOG_PREFIX.ACCESSORY, `Setting Bluetooth: ${enabled ? 'ON' : 'OFF'}`)
1560
- 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
+ }
1666
+ }
1667
+
1668
+ // ==================== Card Control Switch Setter ====================
1669
+
1670
+ /**
1671
+ * Trigger card playback for a configured card control.
1672
+ * @param {Service} service
1673
+ * @param {CardControlConfig} control
1674
+ * @param {CharacteristicValue} value
1675
+ * @returns {Promise<void>}
1676
+ */
1677
+ async setCardControl (service, control, value) {
1678
+ const { Characteristic } = this.#platform
1679
+ const isOn = Boolean(value)
1680
+
1681
+ if (!isOn) {
1682
+ service.getCharacteristic(Characteristic.On).updateValue(false)
1683
+ return
1684
+ }
1685
+
1686
+ if (!this.#deviceModel.status.isOnline) {
1687
+ this.#log.warn(LOG_PREFIX.ACCESSORY, `[${this.#device.name}] Card control skipped (offline): ${control.label}`)
1688
+ service.getCharacteristic(Characteristic.On).updateValue(false)
1689
+ throw new this.#platform.api.hap.HapStatusError(
1690
+ this.#platform.api.hap.HAPStatus.SERVICE_COMMUNICATION_FAILURE
1691
+ )
1692
+ }
1693
+
1694
+ this.#log.debug(
1695
+ LOG_PREFIX.ACCESSORY,
1696
+ `[${this.#device.name}] Play card control: ${control.label} (${control.cardId})`
1697
+ )
1698
+
1699
+ try {
1700
+ await this.#deviceModel.startCard({ cardId: control.cardId })
1701
+ } catch (error) {
1702
+ this.#log.error(
1703
+ LOG_PREFIX.ACCESSORY,
1704
+ `[${this.#device.name}] Failed to play card ${control.cardId}:`,
1705
+ error
1706
+ )
1707
+ service.getCharacteristic(Characteristic.On).updateValue(false)
1708
+ throw new this.#platform.api.hap.HapStatusError(
1709
+ this.#platform.api.hap.HAPStatus.SERVICE_COMMUNICATION_FAILURE
1710
+ )
1711
+ }
1712
+
1713
+ service.getCharacteristic(Characteristic.On).updateValue(false)
1561
1714
  }
1562
1715
 
1563
1716
  // ==================== Volume Limit Lightbulb Getters/Setters ====================
@@ -1573,7 +1726,7 @@ export class YotoPlayerAccessory {
1573
1726
  const percent = Math.round((clampedSteps / 16) * 100)
1574
1727
  this.#log.debug(
1575
1728
  LOG_PREFIX.ACCESSORY,
1576
- `[${this.#device.name}] Get day max volume limit rawSteps=${limit} steps=${clampedSteps} percent=${percent}`
1729
+ `[${this.#device.name}] Get day max volume limit rawSteps=${limit} percent=${percent}`
1577
1730
  )
1578
1731
  return percent
1579
1732
  }
@@ -1598,7 +1751,14 @@ export class YotoPlayerAccessory {
1598
1751
  LOG_PREFIX.ACCESSORY,
1599
1752
  `[${this.#device.name}] Set day max volume limit raw=${value} normalizedPercent=${normalizedPercent} requestedSteps=${requestedSteps} -> steps=${limit} percent=${limitPercent}`
1600
1753
  )
1601
- 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
+ }
1602
1762
  }
1603
1763
 
1604
1764
  /**
@@ -1612,7 +1772,7 @@ export class YotoPlayerAccessory {
1612
1772
  const percent = Math.round((clampedSteps / 16) * 100)
1613
1773
  this.#log.debug(
1614
1774
  LOG_PREFIX.ACCESSORY,
1615
- `[${this.#device.name}] Get night max volume limit rawSteps=${limit} steps=${clampedSteps} percent=${percent}`
1775
+ `[${this.#device.name}] Get night max volume limit rawSteps=${limit} percent=${percent}`
1616
1776
  )
1617
1777
  return percent
1618
1778
  }
@@ -1637,7 +1797,14 @@ export class YotoPlayerAccessory {
1637
1797
  LOG_PREFIX.ACCESSORY,
1638
1798
  `[${this.#device.name}] Set night max volume limit raw=${value} normalizedPercent=${normalizedPercent} requestedSteps=${requestedSteps} -> steps=${limit} percent=${limitPercent}`
1639
1799
  )
1640
- 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
+ }
1641
1808
  }
1642
1809
 
1643
1810
  // ==================== Characteristic Update Methods ====================
@@ -1663,7 +1830,6 @@ export class YotoPlayerAccessory {
1663
1830
  updateVolumeCharacteristic (volumeSteps) {
1664
1831
  if (!this.volumeService) return
1665
1832
 
1666
- const { Characteristic } = this.#platform
1667
1833
  if (volumeSteps > 0) {
1668
1834
  this.#lastNonZeroVolume = Math.round((volumeSteps / 16) * 100)
1669
1835
  }
@@ -1673,8 +1839,10 @@ export class YotoPlayerAccessory {
1673
1839
  const percent = Math.round((clampedVolume / 16) * 100)
1674
1840
  this.#log.debug(
1675
1841
  LOG_PREFIX.ACCESSORY,
1676
- `[${this.#device.name}] Update volume characteristic rawSteps=${volumeSteps} steps=${clampedVolume} percent=${percent}`
1842
+ `[${this.#device.name}] Update volume characteristic rawSteps=${volumeSteps} percent=${percent}`
1677
1843
  )
1844
+
1845
+ const { Characteristic } = this.#platform
1678
1846
  this.volumeService
1679
1847
  .getCharacteristic(Characteristic.Brightness)
1680
1848
  .updateValue(percent)
@@ -1797,9 +1965,9 @@ export class YotoPlayerAccessory {
1797
1965
  .updateValue(isOnline)
1798
1966
  }
1799
1967
 
1800
- // Update night mode ContactSensor (device state)
1801
- if (this.nightModeService) {
1802
- this.nightModeService
1968
+ // Update day mode ContactSensor (device state)
1969
+ if (this.dayModeService) {
1970
+ this.dayModeService
1803
1971
  .getCharacteristic(Characteristic.StatusActive)
1804
1972
  .updateValue(isOnline)
1805
1973
  }
@@ -1883,20 +2051,20 @@ export class YotoPlayerAccessory {
1883
2051
  }
1884
2052
 
1885
2053
  /**
1886
- * Update night mode ContactSensor characteristic
2054
+ * Update day mode ContactSensor characteristic
1887
2055
  */
1888
- updateNightModeCharacteristic () {
1889
- if (!this.nightModeService) {
2056
+ updateDayModeCharacteristic () {
2057
+ if (!this.dayModeService) {
1890
2058
  return
1891
2059
  }
1892
2060
 
1893
2061
  const { Characteristic } = this.#platform
1894
2062
  const status = this.#deviceModel.status
1895
- const isNightMode = status.dayMode === 'night'
2063
+ const isDayMode = status.dayMode === 'day'
1896
2064
 
1897
- this.nightModeService
2065
+ this.dayModeService
1898
2066
  .getCharacteristic(Characteristic.ContactSensorState)
1899
- .updateValue(isNightMode ? Characteristic.ContactSensorState.CONTACT_NOT_DETECTED : Characteristic.ContactSensorState.CONTACT_DETECTED)
2067
+ .updateValue(isDayMode ? Characteristic.ContactSensorState.CONTACT_NOT_DETECTED : Characteristic.ContactSensorState.CONTACT_DETECTED)
1900
2068
  }
1901
2069
 
1902
2070
  /**
@@ -1944,7 +2112,7 @@ export class YotoPlayerAccessory {
1944
2112
  const percent = Math.round((clampedLimit / 16) * 100)
1945
2113
  this.#log.debug(
1946
2114
  LOG_PREFIX.ACCESSORY,
1947
- `[${this.#device.name}] Update day max volume characteristic rawSteps=${limit} steps=${clampedLimit} percent=${percent}`
2115
+ `[${this.#device.name}] Update day max volume characteristic rawSteps=${limit} percent=${percent}`
1948
2116
  )
1949
2117
  this.dayMaxVolumeService
1950
2118
  .getCharacteristic(Characteristic.Brightness)
@@ -1957,7 +2125,7 @@ export class YotoPlayerAccessory {
1957
2125
  const percent = Math.round((clampedLimit / 16) * 100)
1958
2126
  this.#log.debug(
1959
2127
  LOG_PREFIX.ACCESSORY,
1960
- `[${this.#device.name}] Update night max volume characteristic rawSteps=${limit} steps=${clampedLimit} percent=${percent}`
2128
+ `[${this.#device.name}] Update night max volume characteristic rawSteps=${limit} percent=${percent}`
1961
2129
  )
1962
2130
  this.nightMaxVolumeService
1963
2131
  .getCharacteristic(Characteristic.Brightness)