homebridge-yoto 0.0.38 → 0.0.40

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/README.md CHANGED
@@ -25,6 +25,56 @@
25
25
 
26
26
  THIS PLUGIN IS A WIP. DO NOT USE YET.
27
27
 
28
+ Homebridge plugin that exposes Yoto players to HomeKit with optional playback controls, device status, and nightlight settings.
29
+
30
+ ## Settings
31
+
32
+ **Playback Controls** (`services.playbackAccessory`)
33
+ - **Bridged (Switch + Dimmer)**: Adds Playback and Volume services on the main accessory.
34
+ - **External Smart Speaker**: Publishes a separate Smart Speaker accessory for playback and volume. Requires pairing the extra accessory in the Home app.
35
+ - **None**: Disables playback and volume services entirely.
36
+
37
+ **Card Controls** (`services.cardControls`)
38
+ - Adds a per-device switch that plays the configured card ID.
39
+ - Optional "Play on All Yotos" accessory per card control.
40
+
41
+ **Service toggles**
42
+ - **Temperature Sensor**: Adds a temperature sensor when supported by the device.
43
+ - **Nightlight**: Adds day/night nightlight controls and status sensors.
44
+ - **Card Slot**: Adds a card insertion sensor.
45
+ - **Day Mode**: Adds a day/night mode sensor.
46
+ - **Sleep Timer**: Adds a sleep timer switch.
47
+ - **Bluetooth**: Adds a Bluetooth toggle switch.
48
+ - **Volume Limits**: Adds day/night max volume controls.
49
+
50
+ ## HomeKit Services
51
+
52
+ **Playback (bridged)**
53
+ - **Playback**: Switch; On resumes, Off pauses.
54
+ - **Volume**: Lightbulb; On unmutes, Off mutes, Brightness maps 0-100% to device volume steps.
55
+
56
+ **Smart Speaker (external)**
57
+ - **Smart Speaker**: Current/Target Media State, Volume, Mute, and StatusActive (online state).
58
+
59
+ **Card Controls**
60
+ - **Card Control**: Switch on each device that plays the configured card ID.
61
+ - **Card Control (All Yotos)**: Optional switch accessory that plays the card on every Yoto.
62
+
63
+ **Device status**
64
+ - **Online Status**: Contact sensor; Contact Not Detected = online.
65
+ - **Battery**: Battery level, charging state, and low battery.
66
+ - **Temperature**: Temperature sensor with fault status when offline/unavailable.
67
+
68
+ **Nightlight**
69
+ - **Day Nightlight / Night Nightlight**: Lightbulbs with On/Off, Brightness, Hue, and Saturation.
70
+ - **Nightlight Active / Day Nightlight Active / Night Nightlight Active**: Contact sensors for live nightlight state.
71
+
72
+ **Other controls**
73
+ - **Card Slot**: Contact sensor for card insertion.
74
+ - **Day Mode**: Contact sensor; Contact Not Detected = day mode.
75
+ - **Sleep Timer**: Switch to enable/disable sleep timer.
76
+ - **Bluetooth**: Switch to toggle Bluetooth.
77
+ - **Day/Night Max Volume**: Lightbulb brightness sets max volume limits.
28
78
 
29
79
  ## License
30
80
 
@@ -49,17 +49,17 @@
49
49
  "title": "Accessory Services",
50
50
  "type": "object",
51
51
  "properties": {
52
- "playback": {
53
- "title": "Playback",
54
- "type": "boolean",
55
- "default": true,
56
- "description": "Expose a playback switch."
57
- },
58
- "volume": {
59
- "title": "Volume",
60
- "type": "boolean",
61
- "default": true,
62
- "description": "Expose volume controls."
52
+ "playbackAccessory": {
53
+ "title": "Playback Controls",
54
+ "type": "string",
55
+ "default": "none",
56
+ "enum": ["bridged", "external", "none"],
57
+ "enumNames": [
58
+ "Bridged (Switch + Dimmer)",
59
+ "External Smart Speaker",
60
+ "None"
61
+ ],
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
64
  "temperature": {
65
65
  "title": "Temperature Sensor",
@@ -79,11 +79,40 @@
79
79
  "default": true,
80
80
  "description": "Expose card insertion status."
81
81
  },
82
- "nightMode": {
83
- "title": "Night Mode",
82
+ "cardControls": {
83
+ "title": "Card Controls",
84
+ "type": "array",
85
+ "description": "Add switches that play a specific card ID on each Yoto device. Optionally add an accessory that plays the card on all Yotos.",
86
+ "items": {
87
+ "title": "Card Control",
88
+ "type": "object",
89
+ "properties": {
90
+ "label": {
91
+ "title": "Label",
92
+ "type": "string",
93
+ "required": true,
94
+ "description": "Name shown in HomeKit for this card control."
95
+ },
96
+ "cardId": {
97
+ "title": "Card ID",
98
+ "type": "string",
99
+ "required": true,
100
+ "description": "The Yoto card ID to play."
101
+ },
102
+ "playOnAll": {
103
+ "title": "Play on All Yotos",
104
+ "type": "boolean",
105
+ "default": false,
106
+ "description": "Create a separate accessory that plays this card on every Yoto."
107
+ }
108
+ }
109
+ }
110
+ },
111
+ "dayMode": {
112
+ "title": "Day Mode",
84
113
  "type": "boolean",
85
114
  "default": true,
86
- "description": "Expose night mode status."
115
+ "description": "Expose day mode status."
87
116
  },
88
117
  "sleepTimer": {
89
118
  "title": "Sleep Timer",
@@ -133,12 +162,29 @@
133
162
  "type": "help",
134
163
  "helpvalue": "<p>Select which HomeKit services to expose for each Yoto device.</p>"
135
164
  },
136
- "services.playback",
137
- "services.volume",
165
+ {
166
+ "type": "help",
167
+ "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
+ },
169
+ "services.playbackAccessory",
138
170
  "services.temperature",
139
171
  "services.nightlight",
140
172
  "services.cardSlot",
141
- "services.nightMode",
173
+ {
174
+ "type": "help",
175
+ "helpvalue": "<p><strong>Card Controls:</strong> Add switches that play a specific card ID on each Yoto. Enable \"Play on All Yotos\" to create a separate accessory.</p>"
176
+ },
177
+ {
178
+ "key": "services.cardControls",
179
+ "type": "array",
180
+ "buttonText": "Add Card Control",
181
+ "items": [
182
+ "services.cardControls[].label",
183
+ "services.cardControls[].cardId",
184
+ "services.cardControls[].playOnAll"
185
+ ]
186
+ },
187
+ "services.dayMode",
142
188
  "services.sleepTimer",
143
189
  "services.bluetooth",
144
190
  "services.volumeLimits"
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
@@ -25,7 +26,7 @@
25
26
  * @property {boolean} temperature
26
27
  * @property {boolean} nightlight
27
28
  * @property {boolean} cardSlot
28
- * @property {boolean} nightMode
29
+ * @property {boolean} dayMode
29
30
  * @property {boolean} sleepTimer
30
31
  * @property {boolean} bluetooth
31
32
  * @property {boolean} volumeLimits
@@ -41,6 +42,8 @@ import {
41
42
  import { sanitizeName } from './sanitize-name.js'
42
43
  import { syncServiceNames } from './sync-service-names.js'
43
44
  import { serviceSchema } from '../config.schema.cjs'
45
+ import { getPlaybackAccessoryConfig } from './service-config.js'
46
+ import { getCardControlConfigs } from './card-controls.js'
44
47
 
45
48
  // Use syncServiceNames for every visible service so HomeKit labels stay stable.
46
49
  // Exceptions: AccessoryInformation (named in platform) and Battery (set Characteristic.Name only).
@@ -60,7 +63,7 @@ function getBooleanSetting (value, fallback) {
60
63
  */
61
64
  function getServiceDefault (key) {
62
65
  const entry = serviceSchema[key]
63
- if (entry?.default !== undefined && typeof entry.default === 'boolean') {
66
+ if (entry && 'default' in entry && typeof entry.default === 'boolean') {
64
67
  return entry.default
65
68
  }
66
69
  return false
@@ -87,7 +90,7 @@ export class YotoPlayerAccessory {
87
90
  /** @type {Service | undefined} */ dayNightlightActiveService
88
91
  /** @type {Service | undefined} */ nightNightlightActiveService
89
92
  /** @type {Service | undefined} */ cardSlotService
90
- /** @type {Service | undefined} */ nightModeService
93
+ /** @type {Service | undefined} */ dayModeService
91
94
  /** @type {Service | undefined} */ sleepTimerService
92
95
  /** @type {Service | undefined} */ bluetoothService
93
96
  /** @type {Service | undefined} */ dayMaxVolumeService
@@ -127,14 +130,15 @@ export class YotoPlayerAccessory {
127
130
  const serviceConfig = typeof services === 'object' && services !== null
128
131
  ? /** @type {Record<string, unknown>} */ (services)
129
132
  : {}
133
+ const playbackConfig = getPlaybackAccessoryConfig(this.#platform.config)
130
134
 
131
135
  return {
132
- playback: getBooleanSetting(serviceConfig['playback'], getServiceDefault('playback')),
133
- volume: getBooleanSetting(serviceConfig['volume'], getServiceDefault('volume')),
136
+ playback: playbackConfig.playbackEnabled,
137
+ volume: playbackConfig.volumeEnabled,
134
138
  temperature: getBooleanSetting(serviceConfig['temperature'], getServiceDefault('temperature')),
135
139
  nightlight: getBooleanSetting(serviceConfig['nightlight'], getServiceDefault('nightlight')),
136
140
  cardSlot: getBooleanSetting(serviceConfig['cardSlot'], getServiceDefault('cardSlot')),
137
- nightMode: getBooleanSetting(serviceConfig['nightMode'], getServiceDefault('nightMode')),
141
+ dayMode: getBooleanSetting(serviceConfig['dayMode'], getServiceDefault('dayMode')),
138
142
  sleepTimer: getBooleanSetting(serviceConfig['sleepTimer'], getServiceDefault('sleepTimer')),
139
143
  bluetooth: getBooleanSetting(serviceConfig['bluetooth'], getServiceDefault('bluetooth')),
140
144
  volumeLimits: getBooleanSetting(serviceConfig['volumeLimits'], getServiceDefault('volumeLimits')),
@@ -188,8 +192,8 @@ export class YotoPlayerAccessory {
188
192
  if (serviceToggles.cardSlot) {
189
193
  this.setupCardSlotService()
190
194
  }
191
- if (serviceToggles.nightMode) {
192
- this.setupNightModeService()
195
+ if (serviceToggles.dayMode) {
196
+ this.setupDayModeService()
193
197
  }
194
198
  if (serviceToggles.sleepTimer) {
195
199
  this.setupSleepTimerService()
@@ -200,6 +204,7 @@ export class YotoPlayerAccessory {
200
204
  if (serviceToggles.volumeLimits) {
201
205
  this.setupVolumeLimitServices()
202
206
  }
207
+ this.setupCardControlServices()
203
208
 
204
209
  // Remove any services that aren't in our current set
205
210
  // (except AccessoryInformation which should always be preserved)
@@ -515,21 +520,21 @@ export class YotoPlayerAccessory {
515
520
  }
516
521
 
517
522
  /**
518
- * Setup night mode ContactSensor service
519
- * Shows if device is in night mode (vs day mode)
523
+ * Setup day mode ContactSensor service
524
+ * Shows if device is in day mode (vs night mode)
520
525
  */
521
- setupNightModeService () {
526
+ setupDayModeService () {
522
527
  const { Service, Characteristic } = this.#platform
523
- const serviceName = this.generateServiceName('Night Mode')
528
+ const serviceName = this.generateServiceName('Day Mode')
524
529
 
525
- const service = this.#accessory.getServiceById(Service.ContactSensor, 'NightModeStatus') ||
526
- this.#accessory.addService(Service.ContactSensor, serviceName, 'NightModeStatus')
530
+ const service = this.#accessory.getServiceById(Service.ContactSensor, 'DayModeStatus') ||
531
+ this.#accessory.addService(Service.ContactSensor, serviceName, 'DayModeStatus')
527
532
  syncServiceNames({ Characteristic, service, name: serviceName })
528
533
 
529
534
  service.getCharacteristic(Characteristic.ContactSensorState)
530
- .onGet(this.getNightModeStatus.bind(this))
535
+ .onGet(this.getDayModeStatus.bind(this))
531
536
 
532
- this.nightModeService = service
537
+ this.dayModeService = service
533
538
  this.#currentServices.add(service)
534
539
  }
535
540
 
@@ -632,6 +637,39 @@ export class YotoPlayerAccessory {
632
637
  this.#currentServices.add(nightService)
633
638
  }
634
639
 
640
+ /**
641
+ * Setup card control Switch services
642
+ */
643
+ setupCardControlServices () {
644
+ const cardControls = getCardControlConfigs(this.#platform.config)
645
+ if (cardControls.length === 0) {
646
+ return
647
+ }
648
+
649
+ const { Service, Characteristic } = this.#platform
650
+
651
+ for (const control of cardControls) {
652
+ const serviceName = this.generateServiceName(control.label)
653
+ const subtype = `CardControl:${control.id}`
654
+
655
+ const service = this.#accessory.getServiceById(Service.Switch, subtype) ||
656
+ this.#accessory.addService(Service.Switch, serviceName, subtype)
657
+
658
+ syncServiceNames({ Characteristic, service, name: serviceName })
659
+
660
+ service
661
+ .getCharacteristic(Characteristic.On)
662
+ .onGet(() => false)
663
+ .onSet(async (value) => {
664
+ await this.setCardControl(service, control, value)
665
+ })
666
+
667
+ service.updateCharacteristic(Characteristic.On, false)
668
+
669
+ this.#currentServices.add(service)
670
+ }
671
+ }
672
+
635
673
  /**
636
674
  * Setup event listeners for device model updates
637
675
  * Uses exhaustive switch pattern for type safety
@@ -677,8 +715,8 @@ export class YotoPlayerAccessory {
677
715
  break
678
716
 
679
717
  case 'dayMode':
680
- // Update night mode ContactSensor
681
- this.updateNightModeCharacteristic()
718
+ // Update day mode ContactSensor
719
+ this.updateDayModeCharacteristic()
682
720
  // Update nightlight status ContactSensors (depends on dayMode)
683
721
  if (this.#deviceModel.capabilities.hasColoredNightlight) {
684
722
  this.updateNightlightStatusCharacteristics()
@@ -927,7 +965,7 @@ export class YotoPlayerAccessory {
927
965
  const percent = Math.round((clampedSteps / 16) * 100)
928
966
  this.#log.debug(
929
967
  LOG_PREFIX.ACCESSORY,
930
- `[${this.#device.name}] Get volume rawSteps=${volumeSteps} steps=${clampedSteps} percent=${percent}`
968
+ `[${this.#device.name}] Get volume rawSteps=${volumeSteps} percent=${percent}`
931
969
  )
932
970
  return percent
933
971
  }
@@ -950,14 +988,11 @@ export class YotoPlayerAccessory {
950
988
 
951
989
  const normalizedPercent = Math.max(0, Math.min(Math.round(requestedPercent), 100))
952
990
  const requestedSteps = Math.round((normalizedPercent / 100) * 16)
953
- const maxVolumeSteps = Number.isFinite(deviceModel.status.maxVolume)
954
- ? deviceModel.status.maxVolume
955
- : 16
956
- const steps = Math.max(0, Math.min(Math.round(requestedSteps), maxVolumeSteps))
991
+ const steps = Math.max(0, Math.min(Math.round(requestedSteps), 16))
957
992
  const resultPercent = Math.round((steps / 16) * 100)
958
993
  this.#log.debug(
959
994
  LOG_PREFIX.ACCESSORY,
960
- `[${this.#device.name}] Set volume raw=${value} normalizedPercent=${normalizedPercent} requestedSteps=${requestedSteps} -> steps=${steps} percent=${resultPercent} (maxSteps=${maxVolumeSteps})`
995
+ `[${this.#device.name}] Set volume raw=${value} normalizedPercent=${normalizedPercent} requestedSteps=${requestedSteps} -> steps=${steps} percent=${resultPercent}`
961
996
  )
962
997
 
963
998
  // Track last non-zero volume for unmute
@@ -969,13 +1004,12 @@ export class YotoPlayerAccessory {
969
1004
  await deviceModel.setVolume(steps)
970
1005
  if (this.volumeService) {
971
1006
  const { Characteristic } = this.#platform
972
-
1007
+ const clampedPercent = Math.round((steps / 16) * 100)
973
1008
  this.volumeService
974
1009
  .getCharacteristic(Characteristic.On)
975
1010
  .updateValue(steps > 0)
976
1011
 
977
1012
  if (steps !== requestedSteps || normalizedPercent !== requestedPercent) {
978
- const clampedPercent = Math.round((steps / 16) * 100)
979
1013
  this.volumeService
980
1014
  .getCharacteristic(Characteristic.Brightness)
981
1015
  .updateValue(clampedPercent)
@@ -1035,8 +1069,8 @@ export class YotoPlayerAccessory {
1035
1069
  async getOnlineStatus () {
1036
1070
  const { Characteristic } = this.#platform
1037
1071
  return this.#deviceModel.status.isOnline
1038
- ? Characteristic.ContactSensorState.CONTACT_DETECTED
1039
- : Characteristic.ContactSensorState.CONTACT_NOT_DETECTED
1072
+ ? Characteristic.ContactSensorState.CONTACT_NOT_DETECTED
1073
+ : Characteristic.ContactSensorState.CONTACT_DETECTED
1040
1074
  }
1041
1075
 
1042
1076
  // ==================== Battery Characteristic Handlers ====================
@@ -1450,7 +1484,7 @@ export class YotoPlayerAccessory {
1450
1484
  const { Characteristic } = this.#platform
1451
1485
  const status = this.#deviceModel.status
1452
1486
  const isActive = status.nightlightMode !== 'off'
1453
- return isActive ? Characteristic.ContactSensorState.CONTACT_DETECTED : Characteristic.ContactSensorState.CONTACT_NOT_DETECTED
1487
+ return isActive ? Characteristic.ContactSensorState.CONTACT_NOT_DETECTED : Characteristic.ContactSensorState.CONTACT_DETECTED
1454
1488
  }
1455
1489
 
1456
1490
  /**
@@ -1463,7 +1497,7 @@ export class YotoPlayerAccessory {
1463
1497
  const isDay = status.dayMode === 'day'
1464
1498
  const isActive = status.nightlightMode !== 'off'
1465
1499
  const isShowing = isDay && isActive
1466
- return isShowing ? Characteristic.ContactSensorState.CONTACT_DETECTED : Characteristic.ContactSensorState.CONTACT_NOT_DETECTED
1500
+ return isShowing ? Characteristic.ContactSensorState.CONTACT_NOT_DETECTED : Characteristic.ContactSensorState.CONTACT_DETECTED
1467
1501
  }
1468
1502
 
1469
1503
  /**
@@ -1476,7 +1510,7 @@ export class YotoPlayerAccessory {
1476
1510
  const isNight = status.dayMode === 'night'
1477
1511
  const isActive = status.nightlightMode !== 'off'
1478
1512
  const isShowing = isNight && isActive
1479
- return isShowing ? Characteristic.ContactSensorState.CONTACT_DETECTED : Characteristic.ContactSensorState.CONTACT_NOT_DETECTED
1513
+ return isShowing ? Characteristic.ContactSensorState.CONTACT_NOT_DETECTED : Characteristic.ContactSensorState.CONTACT_DETECTED
1480
1514
  }
1481
1515
 
1482
1516
  // ==================== Card Slot ContactSensor Getter ====================
@@ -1489,22 +1523,22 @@ export class YotoPlayerAccessory {
1489
1523
  const { Characteristic } = this.#platform
1490
1524
  const status = this.#deviceModel.status
1491
1525
  const hasCard = status.cardInsertionState !== 'none'
1492
- return hasCard ? Characteristic.ContactSensorState.CONTACT_DETECTED : Characteristic.ContactSensorState.CONTACT_NOT_DETECTED
1526
+ return hasCard ? Characteristic.ContactSensorState.CONTACT_NOT_DETECTED : Characteristic.ContactSensorState.CONTACT_DETECTED
1493
1527
  }
1494
1528
 
1495
- // ==================== Night Mode ContactSensor Getter ====================
1529
+ // ==================== Day Mode ContactSensor Getter ====================
1496
1530
 
1497
1531
  /**
1498
- * Get night mode status
1532
+ * Get day mode status
1499
1533
  * @returns {Promise<CharacteristicValue>}
1500
1534
  */
1501
- async getNightModeStatus () {
1535
+ async getDayModeStatus () {
1502
1536
  const { Characteristic } = this.#platform
1503
1537
  const status = this.#deviceModel.status
1504
- const isNightMode = status.dayMode === 'night'
1505
- return isNightMode
1506
- ? Characteristic.ContactSensorState.CONTACT_DETECTED
1507
- : Characteristic.ContactSensorState.CONTACT_NOT_DETECTED
1538
+ const isDayMode = status.dayMode === 'day'
1539
+ return isDayMode
1540
+ ? Characteristic.ContactSensorState.CONTACT_NOT_DETECTED
1541
+ : Characteristic.ContactSensorState.CONTACT_DETECTED
1508
1542
  }
1509
1543
 
1510
1544
  // ==================== Sleep Timer Switch Getter/Setter ====================
@@ -1563,6 +1597,54 @@ export class YotoPlayerAccessory {
1563
1597
  await this.#deviceModel.updateConfig({ bluetoothEnabled: enabled })
1564
1598
  }
1565
1599
 
1600
+ // ==================== Card Control Switch Setter ====================
1601
+
1602
+ /**
1603
+ * Trigger card playback for a configured card control.
1604
+ * @param {Service} service
1605
+ * @param {CardControlConfig} control
1606
+ * @param {CharacteristicValue} value
1607
+ * @returns {Promise<void>}
1608
+ */
1609
+ async setCardControl (service, control, value) {
1610
+ const { Characteristic } = this.#platform
1611
+ const isOn = Boolean(value)
1612
+
1613
+ if (!isOn) {
1614
+ service.getCharacteristic(Characteristic.On).updateValue(false)
1615
+ return
1616
+ }
1617
+
1618
+ if (!this.#deviceModel.status.isOnline) {
1619
+ this.#log.warn(LOG_PREFIX.ACCESSORY, `[${this.#device.name}] Card control skipped (offline): ${control.label}`)
1620
+ service.getCharacteristic(Characteristic.On).updateValue(false)
1621
+ throw new this.#platform.api.hap.HapStatusError(
1622
+ this.#platform.api.hap.HAPStatus.SERVICE_COMMUNICATION_FAILURE
1623
+ )
1624
+ }
1625
+
1626
+ this.#log.debug(
1627
+ LOG_PREFIX.ACCESSORY,
1628
+ `[${this.#device.name}] Play card control: ${control.label} (${control.cardId})`
1629
+ )
1630
+
1631
+ try {
1632
+ await this.#deviceModel.startCard({ cardId: control.cardId })
1633
+ } catch (error) {
1634
+ this.#log.error(
1635
+ LOG_PREFIX.ACCESSORY,
1636
+ `[${this.#device.name}] Failed to play card ${control.cardId}:`,
1637
+ error
1638
+ )
1639
+ service.getCharacteristic(Characteristic.On).updateValue(false)
1640
+ throw new this.#platform.api.hap.HapStatusError(
1641
+ this.#platform.api.hap.HAPStatus.SERVICE_COMMUNICATION_FAILURE
1642
+ )
1643
+ }
1644
+
1645
+ service.getCharacteristic(Characteristic.On).updateValue(false)
1646
+ }
1647
+
1566
1648
  // ==================== Volume Limit Lightbulb Getters/Setters ====================
1567
1649
 
1568
1650
  /**
@@ -1576,7 +1658,7 @@ export class YotoPlayerAccessory {
1576
1658
  const percent = Math.round((clampedSteps / 16) * 100)
1577
1659
  this.#log.debug(
1578
1660
  LOG_PREFIX.ACCESSORY,
1579
- `[${this.#device.name}] Get day max volume limit rawSteps=${limit} steps=${clampedSteps} percent=${percent}`
1661
+ `[${this.#device.name}] Get day max volume limit rawSteps=${limit} percent=${percent}`
1580
1662
  )
1581
1663
  return percent
1582
1664
  }
@@ -1615,7 +1697,7 @@ export class YotoPlayerAccessory {
1615
1697
  const percent = Math.round((clampedSteps / 16) * 100)
1616
1698
  this.#log.debug(
1617
1699
  LOG_PREFIX.ACCESSORY,
1618
- `[${this.#device.name}] Get night max volume limit rawSteps=${limit} steps=${clampedSteps} percent=${percent}`
1700
+ `[${this.#device.name}] Get night max volume limit rawSteps=${limit} percent=${percent}`
1619
1701
  )
1620
1702
  return percent
1621
1703
  }
@@ -1664,9 +1746,6 @@ export class YotoPlayerAccessory {
1664
1746
  * @param {number} volumeSteps - Volume level (0-16)
1665
1747
  */
1666
1748
  updateVolumeCharacteristic (volumeSteps) {
1667
- if (!this.volumeService) return
1668
-
1669
- const { Characteristic } = this.#platform
1670
1749
  if (volumeSteps > 0) {
1671
1750
  this.#lastNonZeroVolume = Math.round((volumeSteps / 16) * 100)
1672
1751
  }
@@ -1676,8 +1755,11 @@ export class YotoPlayerAccessory {
1676
1755
  const percent = Math.round((clampedVolume / 16) * 100)
1677
1756
  this.#log.debug(
1678
1757
  LOG_PREFIX.ACCESSORY,
1679
- `[${this.#device.name}] Update volume characteristic rawSteps=${volumeSteps} steps=${clampedVolume} percent=${percent}`
1758
+ `[${this.#device.name}] Update volume characteristic rawSteps=${volumeSteps} percent=${percent}`
1680
1759
  )
1760
+ if (!this.volumeService) return
1761
+
1762
+ const { Characteristic } = this.#platform
1681
1763
  this.volumeService
1682
1764
  .getCharacteristic(Characteristic.Brightness)
1683
1765
  .updateValue(percent)
@@ -1765,8 +1847,8 @@ export class YotoPlayerAccessory {
1765
1847
  this.onlineStatusService
1766
1848
  .getCharacteristic(Characteristic.ContactSensorState)
1767
1849
  .updateValue(isOnline
1768
- ? Characteristic.ContactSensorState.CONTACT_DETECTED
1769
- : Characteristic.ContactSensorState.CONTACT_NOT_DETECTED)
1850
+ ? Characteristic.ContactSensorState.CONTACT_NOT_DETECTED
1851
+ : Characteristic.ContactSensorState.CONTACT_DETECTED)
1770
1852
  }
1771
1853
 
1772
1854
  // Update TemperatureSensor (temperature reading)
@@ -1800,9 +1882,9 @@ export class YotoPlayerAccessory {
1800
1882
  .updateValue(isOnline)
1801
1883
  }
1802
1884
 
1803
- // Update night mode ContactSensor (device state)
1804
- if (this.nightModeService) {
1805
- this.nightModeService
1885
+ // Update day mode ContactSensor (device state)
1886
+ if (this.dayModeService) {
1887
+ this.dayModeService
1806
1888
  .getCharacteristic(Characteristic.StatusActive)
1807
1889
  .updateValue(isOnline)
1808
1890
  }
@@ -1846,7 +1928,7 @@ export class YotoPlayerAccessory {
1846
1928
  const isActive = status.nightlightMode !== 'off'
1847
1929
  this.nightlightActiveService
1848
1930
  .getCharacteristic(Characteristic.ContactSensorState)
1849
- .updateValue(isActive ? Characteristic.ContactSensorState.CONTACT_DETECTED : Characteristic.ContactSensorState.CONTACT_NOT_DETECTED)
1931
+ .updateValue(isActive ? Characteristic.ContactSensorState.CONTACT_NOT_DETECTED : Characteristic.ContactSensorState.CONTACT_DETECTED)
1850
1932
  }
1851
1933
 
1852
1934
  if (this.dayNightlightActiveService) {
@@ -1855,7 +1937,7 @@ export class YotoPlayerAccessory {
1855
1937
  const isShowing = isDay && isActive
1856
1938
  this.dayNightlightActiveService
1857
1939
  .getCharacteristic(Characteristic.ContactSensorState)
1858
- .updateValue(isShowing ? Characteristic.ContactSensorState.CONTACT_DETECTED : Characteristic.ContactSensorState.CONTACT_NOT_DETECTED)
1940
+ .updateValue(isShowing ? Characteristic.ContactSensorState.CONTACT_NOT_DETECTED : Characteristic.ContactSensorState.CONTACT_DETECTED)
1859
1941
  }
1860
1942
 
1861
1943
  if (this.nightNightlightActiveService) {
@@ -1864,7 +1946,7 @@ export class YotoPlayerAccessory {
1864
1946
  const isShowing = isNight && isActive
1865
1947
  this.nightNightlightActiveService
1866
1948
  .getCharacteristic(Characteristic.ContactSensorState)
1867
- .updateValue(isShowing ? Characteristic.ContactSensorState.CONTACT_DETECTED : Characteristic.ContactSensorState.CONTACT_NOT_DETECTED)
1949
+ .updateValue(isShowing ? Characteristic.ContactSensorState.CONTACT_NOT_DETECTED : Characteristic.ContactSensorState.CONTACT_DETECTED)
1868
1950
  }
1869
1951
  }
1870
1952
 
@@ -1882,24 +1964,24 @@ export class YotoPlayerAccessory {
1882
1964
 
1883
1965
  this.cardSlotService
1884
1966
  .getCharacteristic(Characteristic.ContactSensorState)
1885
- .updateValue(hasCard ? Characteristic.ContactSensorState.CONTACT_DETECTED : Characteristic.ContactSensorState.CONTACT_NOT_DETECTED)
1967
+ .updateValue(hasCard ? Characteristic.ContactSensorState.CONTACT_NOT_DETECTED : Characteristic.ContactSensorState.CONTACT_DETECTED)
1886
1968
  }
1887
1969
 
1888
1970
  /**
1889
- * Update night mode ContactSensor characteristic
1971
+ * Update day mode ContactSensor characteristic
1890
1972
  */
1891
- updateNightModeCharacteristic () {
1892
- if (!this.nightModeService) {
1973
+ updateDayModeCharacteristic () {
1974
+ if (!this.dayModeService) {
1893
1975
  return
1894
1976
  }
1895
1977
 
1896
1978
  const { Characteristic } = this.#platform
1897
1979
  const status = this.#deviceModel.status
1898
- const isNightMode = status.dayMode === 'night'
1980
+ const isDayMode = status.dayMode === 'day'
1899
1981
 
1900
- this.nightModeService
1982
+ this.dayModeService
1901
1983
  .getCharacteristic(Characteristic.ContactSensorState)
1902
- .updateValue(isNightMode ? Characteristic.ContactSensorState.CONTACT_DETECTED : Characteristic.ContactSensorState.CONTACT_NOT_DETECTED)
1984
+ .updateValue(isDayMode ? Characteristic.ContactSensorState.CONTACT_NOT_DETECTED : Characteristic.ContactSensorState.CONTACT_DETECTED)
1903
1985
  }
1904
1986
 
1905
1987
  /**
@@ -1947,7 +2029,7 @@ export class YotoPlayerAccessory {
1947
2029
  const percent = Math.round((clampedLimit / 16) * 100)
1948
2030
  this.#log.debug(
1949
2031
  LOG_PREFIX.ACCESSORY,
1950
- `[${this.#device.name}] Update day max volume characteristic rawSteps=${limit} steps=${clampedLimit} percent=${percent}`
2032
+ `[${this.#device.name}] Update day max volume characteristic rawSteps=${limit} percent=${percent}`
1951
2033
  )
1952
2034
  this.dayMaxVolumeService
1953
2035
  .getCharacteristic(Characteristic.Brightness)
@@ -1960,7 +2042,7 @@ export class YotoPlayerAccessory {
1960
2042
  const percent = Math.round((clampedLimit / 16) * 100)
1961
2043
  this.#log.debug(
1962
2044
  LOG_PREFIX.ACCESSORY,
1963
- `[${this.#device.name}] Update night max volume characteristic rawSteps=${limit} steps=${clampedLimit} percent=${percent}`
2045
+ `[${this.#device.name}] Update night max volume characteristic rawSteps=${limit} percent=${percent}`
1964
2046
  )
1965
2047
  this.nightMaxVolumeService
1966
2048
  .getCharacteristic(Characteristic.Brightness)