homebridge-yoto 0.0.35 → 0.0.37

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/config.schema.cjs CHANGED
@@ -1,3 +1,11 @@
1
+ /** @type {import('./config.schema.json')} */
1
2
  const configSchema = require('./config.schema.json')
2
3
 
4
+ const serviceSchema = configSchema.schema.properties.services.properties
5
+
6
+ /**
7
+ * @typedef {keyof typeof serviceSchema} ServiceSchemaKey
8
+ */
9
+
3
10
  exports.configSchema = configSchema
11
+ exports.serviceSchema = serviceSchema
@@ -44,6 +44,66 @@
44
44
  "x-schema-form": {
45
45
  "hidden": true
46
46
  }
47
+ },
48
+ "services": {
49
+ "title": "Accessory Services",
50
+ "type": "object",
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."
63
+ },
64
+ "temperature": {
65
+ "title": "Temperature Sensor",
66
+ "type": "boolean",
67
+ "default": true,
68
+ "description": "Expose temperature sensor when supported."
69
+ },
70
+ "nightlight": {
71
+ "title": "Nightlight",
72
+ "type": "boolean",
73
+ "default": false,
74
+ "description": "Expose day/night nightlight controls and status."
75
+ },
76
+ "cardSlot": {
77
+ "title": "Card Slot",
78
+ "type": "boolean",
79
+ "default": true,
80
+ "description": "Expose card insertion status."
81
+ },
82
+ "nightMode": {
83
+ "title": "Night Mode",
84
+ "type": "boolean",
85
+ "default": true,
86
+ "description": "Expose night mode status."
87
+ },
88
+ "sleepTimer": {
89
+ "title": "Sleep Timer",
90
+ "type": "boolean",
91
+ "default": false,
92
+ "description": "Expose sleep timer switch."
93
+ },
94
+ "bluetooth": {
95
+ "title": "Bluetooth",
96
+ "type": "boolean",
97
+ "default": false,
98
+ "description": "Expose Bluetooth toggle."
99
+ },
100
+ "volumeLimits": {
101
+ "title": "Volume Limits",
102
+ "type": "boolean",
103
+ "default": true,
104
+ "description": "Expose day/night max volume controls."
105
+ }
106
+ }
47
107
  }
48
108
  }
49
109
  },
@@ -62,6 +122,27 @@
62
122
  "accessToken",
63
123
  "refreshToken"
64
124
  ]
125
+ },
126
+ {
127
+ "type": "section",
128
+ "title": "Accessory Services",
129
+ "expandable": true,
130
+ "expanded": false,
131
+ "items": [
132
+ {
133
+ "type": "help",
134
+ "helpvalue": "<p>Select which HomeKit services to expose for each Yoto device.</p>"
135
+ },
136
+ "services.playback",
137
+ "services.volume",
138
+ "services.temperature",
139
+ "services.nightlight",
140
+ "services.cardSlot",
141
+ "services.nightMode",
142
+ "services.sleepTimer",
143
+ "services.bluetooth",
144
+ "services.volumeLimits"
145
+ ]
65
146
  }
66
147
  ]
67
148
  }
@@ -52,8 +52,7 @@ async function initializeUI () {
52
52
  if (retryBtn) retryBtn.addEventListener('click', retryAuth)
53
53
  if (logoutBtn) logoutBtn.addEventListener('click', logout)
54
54
 
55
- // Show schema-based config form below custom UI
56
- homebridge.showSchemaForm()
55
+ homebridge.hideSchemaForm()
57
56
 
58
57
  // Load auth config and check authentication status
59
58
  await loadAuthConfig()
@@ -63,6 +62,17 @@ async function initializeUI () {
63
62
  // Initialize on ready
64
63
  homebridge.addEventListener('ready', initializeUI)
65
64
 
65
+ /**
66
+ * @param {boolean} shouldShow
67
+ */
68
+ function setSchemaFormVisibility (shouldShow) {
69
+ if (shouldShow) {
70
+ homebridge.showSchemaForm()
71
+ } else {
72
+ homebridge.hideSchemaForm()
73
+ }
74
+ }
75
+
66
76
  /**
67
77
  * Show a specific UI section and hide all others
68
78
  * @param {string} sectionToShow - ID of section to show
@@ -90,6 +100,8 @@ function showSection (sectionToShow, options = {}) {
90
100
  const errorMessageEl = document.getElementById('errorMessage')
91
101
  if (errorMessageEl) errorMessageEl.textContent = options.errorMessage
92
102
  }
103
+
104
+ setSchemaFormVisibility(sectionToShow === 'authSuccess')
93
105
  }
94
106
 
95
107
  /**
@@ -298,12 +310,6 @@ function startPolling () {
298
310
  await homebridge.updatePluginConfig(pluginConfig)
299
311
  await homebridge.savePluginConfig()
300
312
 
301
- // Refresh the schema form to show updated token fields
302
- homebridge.hideSchemaForm()
303
- setTimeout(() => {
304
- homebridge.showSchemaForm()
305
- }, 100)
306
-
307
313
  homebridge.toast.success('Authentication successful!')
308
314
  homebridge.toast.info('Please restart the plugin for changes to take effect', 'Restart Required')
309
315
  showAuthSuccess()
package/lib/accessory.js CHANGED
@@ -7,6 +7,7 @@
7
7
  /** @import { YotoDeviceModel } from 'yoto-nodejs-client' */
8
8
  /** @import { YotoDevice } from 'yoto-nodejs-client/lib/api-endpoints/devices.js' */
9
9
  /** @import { YotoAccessoryContext } from './platform.js' */
10
+ /** @import { ServiceSchemaKey } from '../config.schema.cjs' */
10
11
 
11
12
  /**
12
13
  * Device capabilities detected from metadata
@@ -16,6 +17,20 @@
16
17
  * @property {string | undefined} generation - Device generation (e.g., 'gen3')
17
18
  */
18
19
 
20
+ /**
21
+ * Accessory service toggles from config.
22
+ * @typedef {Object} YotoServiceToggles
23
+ * @property {boolean} playback
24
+ * @property {boolean} volume
25
+ * @property {boolean} temperature
26
+ * @property {boolean} nightlight
27
+ * @property {boolean} cardSlot
28
+ * @property {boolean} nightMode
29
+ * @property {boolean} sleepTimer
30
+ * @property {boolean} bluetooth
31
+ * @property {boolean} volumeLimits
32
+ */
33
+
19
34
  import convert from 'color-convert'
20
35
  import {
21
36
  DEFAULT_MANUFACTURER,
@@ -25,6 +40,31 @@ import {
25
40
  } from './constants.js'
26
41
  import { sanitizeName } from './sanitize-name.js'
27
42
  import { syncServiceNames } from './sync-service-names.js'
43
+ import { serviceSchema } from '../config.schema.cjs'
44
+
45
+ // Use syncServiceNames for every visible service so HomeKit labels stay stable.
46
+ // Exceptions: AccessoryInformation (named in platform) and Battery (set Characteristic.Name only).
47
+
48
+ /**
49
+ * @param {unknown} value
50
+ * @param {boolean} fallback
51
+ * @returns {boolean}
52
+ */
53
+ function getBooleanSetting (value, fallback) {
54
+ return typeof value === 'boolean' ? value : fallback
55
+ }
56
+
57
+ /**
58
+ * @param {ServiceSchemaKey} key
59
+ * @returns {boolean}
60
+ */
61
+ function getServiceDefault (key) {
62
+ const entry = serviceSchema[key]
63
+ if (entry?.default !== undefined && typeof entry.default === 'boolean') {
64
+ return entry.default
65
+ }
66
+ return false
67
+ }
28
68
 
29
69
  /**
30
70
  * Yoto Player Accessory Handler
@@ -39,6 +79,7 @@ export class YotoPlayerAccessory {
39
79
  /** @type {Service | undefined} */ playbackService
40
80
  /** @type {Service | undefined} */ volumeService
41
81
  /** @type {Service | undefined} */ batteryService
82
+ /** @type {Service | undefined} */ onlineStatusService
42
83
  /** @type {Service | undefined} */ temperatureSensorService
43
84
  /** @type {Service | undefined} */ dayNightlightService
44
85
  /** @type {Service | undefined} */ nightNightlightService
@@ -77,6 +118,29 @@ export class YotoPlayerAccessory {
77
118
  this.#currentServices = new Set()
78
119
  }
79
120
 
121
+ /**
122
+ * @returns {YotoServiceToggles}
123
+ */
124
+ getServiceToggles () {
125
+ const config = this.#platform.config
126
+ const services = config && typeof config === 'object' ? config['services'] : undefined
127
+ const serviceConfig = typeof services === 'object' && services !== null
128
+ ? /** @type {Record<string, unknown>} */ (services)
129
+ : {}
130
+
131
+ return {
132
+ playback: getBooleanSetting(serviceConfig['playback'], getServiceDefault('playback')),
133
+ volume: getBooleanSetting(serviceConfig['volume'], getServiceDefault('volume')),
134
+ temperature: getBooleanSetting(serviceConfig['temperature'], getServiceDefault('temperature')),
135
+ nightlight: getBooleanSetting(serviceConfig['nightlight'], getServiceDefault('nightlight')),
136
+ cardSlot: getBooleanSetting(serviceConfig['cardSlot'], getServiceDefault('cardSlot')),
137
+ nightMode: getBooleanSetting(serviceConfig['nightMode'], getServiceDefault('nightMode')),
138
+ sleepTimer: getBooleanSetting(serviceConfig['sleepTimer'], getServiceDefault('sleepTimer')),
139
+ bluetooth: getBooleanSetting(serviceConfig['bluetooth'], getServiceDefault('bluetooth')),
140
+ volumeLimits: getBooleanSetting(serviceConfig['volumeLimits'], getServiceDefault('volumeLimits')),
141
+ }
142
+ }
143
+
80
144
  /**
81
145
  * Setup accessory - create services and setup event listeners
82
146
  * @returns {Promise<void>}
@@ -96,25 +160,46 @@ export class YotoPlayerAccessory {
96
160
  this.#currentServices.clear()
97
161
 
98
162
  // 1. Setup services
163
+ const serviceToggles = this.getServiceToggles()
164
+
99
165
  this.setupAccessoryInformation()
100
- this.setupPlaybackServices()
166
+ this.setupOnlineStatusService()
167
+
168
+ if (serviceToggles.playback) {
169
+ this.setupPlaybackSwitchService()
170
+ }
171
+
172
+ if (serviceToggles.volume) {
173
+ this.setupVolumeService()
174
+ }
175
+
101
176
  this.setupBatteryService()
102
177
 
103
178
  // Setup optional services based on device capabilities
104
- if (this.#deviceModel.capabilities.hasTemperatureSensor) {
179
+ if (serviceToggles.temperature && this.#deviceModel.capabilities.hasTemperatureSensor) {
105
180
  this.setupTemperatureSensorService()
106
181
  }
107
182
 
108
- if (this.#deviceModel.capabilities.hasColoredNightlight) {
183
+ if (serviceToggles.nightlight && this.#deviceModel.capabilities.hasColoredNightlight) {
109
184
  this.setupNightlightServices()
110
185
  }
111
186
 
112
187
  // Setup universal services (available on all devices)
113
- this.setupCardSlotService()
114
- this.setupNightModeService()
115
- this.setupSleepTimerService()
116
- this.setupBluetoothService()
117
- this.setupVolumeLimitServices()
188
+ if (serviceToggles.cardSlot) {
189
+ this.setupCardSlotService()
190
+ }
191
+ if (serviceToggles.nightMode) {
192
+ this.setupNightModeService()
193
+ }
194
+ if (serviceToggles.sleepTimer) {
195
+ this.setupSleepTimerService()
196
+ }
197
+ if (serviceToggles.bluetooth) {
198
+ this.setupBluetoothService()
199
+ }
200
+ if (serviceToggles.volumeLimits) {
201
+ this.setupVolumeLimitServices()
202
+ }
118
203
 
119
204
  // Remove any services that aren't in our current set
120
205
  // (except AccessoryInformation which should always be preserved)
@@ -178,15 +263,29 @@ export class YotoPlayerAccessory {
178
263
  }
179
264
 
180
265
  /**
181
- * Setup basic playback services (non-SmartSpeaker)
266
+ * Setup online/offline ContactSensor service (PRIMARY)
182
267
  */
183
- setupPlaybackServices () {
184
- this.setupPlaybackSwitchService()
185
- this.setupVolumeService()
268
+ setupOnlineStatusService () {
269
+ const { Service, Characteristic } = this.#platform
270
+ const serviceName = this.generateServiceName('Online Status')
271
+
272
+ const service = this.#accessory.getServiceById(Service.ContactSensor, 'OnlineStatus') ||
273
+ this.#accessory.addService(Service.ContactSensor, serviceName, 'OnlineStatus')
274
+
275
+ service.setPrimaryService(true)
276
+
277
+ syncServiceNames({ Characteristic, service, name: serviceName })
278
+
279
+ service
280
+ .getCharacteristic(Characteristic.ContactSensorState)
281
+ .onGet(this.getOnlineStatus.bind(this))
282
+
283
+ this.onlineStatusService = service
284
+ this.#currentServices.add(service)
186
285
  }
187
286
 
188
287
  /**
189
- * Setup play/pause Switch service (PRIMARY)
288
+ * Setup play/pause Switch service
190
289
  */
191
290
  setupPlaybackSwitchService () {
192
291
  const { Service, Characteristic } = this.#platform
@@ -195,15 +294,8 @@ export class YotoPlayerAccessory {
195
294
  const service = this.#accessory.getServiceById(Service.Switch, 'Playback') ||
196
295
  this.#accessory.addService(Service.Switch, serviceName, 'Playback')
197
296
 
198
- service.setPrimaryService(true)
199
-
200
297
  syncServiceNames({ Characteristic, service, name: serviceName })
201
298
 
202
- service.addOptionalCharacteristic(Characteristic.StatusActive)
203
- service
204
- .getCharacteristic(Characteristic.StatusActive)
205
- .onGet(this.getStatusActive.bind(this))
206
-
207
299
  service
208
300
  .getCharacteristic(Characteristic.On)
209
301
  .onGet(this.getPlaybackOn.bind(this))
@@ -214,28 +306,23 @@ export class YotoPlayerAccessory {
214
306
  }
215
307
 
216
308
  /**
217
- * Setup Fanv2 service for volume/mute controls (Speaker service isn't shown in Home)
309
+ * Setup Lightbulb service for volume/mute controls (Speaker service isn't shown in Home)
218
310
  */
219
311
  setupVolumeService () {
220
312
  const { Service, Characteristic } = this.#platform
221
313
  const serviceName = this.generateServiceName('Volume')
222
314
 
223
- const service = this.#accessory.getServiceById(Service.Fanv2, 'Volume') ||
224
- this.#accessory.addService(Service.Fanv2, serviceName, 'Volume')
315
+ const service = this.#accessory.getServiceById(Service.Lightbulb, 'Volume') ||
316
+ this.#accessory.addService(Service.Lightbulb, serviceName, 'Volume')
225
317
  syncServiceNames({ Characteristic, service, name: serviceName })
226
318
 
227
- service.addOptionalCharacteristic(Characteristic.StatusActive)
228
319
  service
229
- .getCharacteristic(Characteristic.StatusActive)
230
- .onGet(this.getStatusActive.bind(this))
231
-
232
- service
233
- .getCharacteristic(Characteristic.Active)
234
- .onGet(this.getVolumeActive.bind(this))
235
- .onSet(this.setVolumeActive.bind(this))
320
+ .getCharacteristic(Characteristic.On)
321
+ .onGet(this.getVolumeOn.bind(this))
322
+ .onSet(this.setVolumeOn.bind(this))
236
323
 
237
324
  service
238
- .getCharacteristic(Characteristic.RotationSpeed)
325
+ .getCharacteristic(Characteristic.Brightness)
239
326
  .setProps({
240
327
  minValue: 0,
241
328
  maxValue: 16,
@@ -256,7 +343,7 @@ export class YotoPlayerAccessory {
256
343
  const serviceName = this.generateServiceName('Battery')
257
344
  const service = this.#accessory.getService(Service.Battery) ||
258
345
  this.#accessory.addService(Service.Battery, serviceName)
259
- syncServiceNames({ Characteristic, service, name: serviceName })
346
+ service.setCharacteristic(Characteristic.Name, serviceName)
260
347
 
261
348
  // BatteryLevel (GET only)
262
349
  service.getCharacteristic(Characteristic.BatteryLevel)
@@ -428,18 +515,18 @@ export class YotoPlayerAccessory {
428
515
  }
429
516
 
430
517
  /**
431
- * Setup night mode OccupancySensor service
518
+ * Setup night mode ContactSensor service
432
519
  * Shows if device is in night mode (vs day mode)
433
520
  */
434
521
  setupNightModeService () {
435
522
  const { Service, Characteristic } = this.#platform
436
523
  const serviceName = this.generateServiceName('Night Mode')
437
524
 
438
- const service = this.#accessory.getServiceById(Service.OccupancySensor, 'NightModeStatus') ||
439
- this.#accessory.addService(Service.OccupancySensor, serviceName, 'NightModeStatus')
525
+ const service = this.#accessory.getServiceById(Service.ContactSensor, 'NightModeStatus') ||
526
+ this.#accessory.addService(Service.ContactSensor, serviceName, 'NightModeStatus')
440
527
  syncServiceNames({ Characteristic, service, name: serviceName })
441
528
 
442
- service.getCharacteristic(Characteristic.OccupancyDetected)
529
+ service.getCharacteristic(Characteristic.ContactSensorState)
443
530
  .onGet(this.getNightModeStatus.bind(this))
444
531
 
445
532
  this.nightModeService = service
@@ -487,7 +574,7 @@ export class YotoPlayerAccessory {
487
574
  }
488
575
 
489
576
  /**
490
- * Setup volume limit Fanv2 services
577
+ * Setup volume limit Lightbulb services
491
578
  * Control day and night mode max volume limits
492
579
  */
493
580
  setupVolumeLimitServices () {
@@ -495,16 +582,22 @@ export class YotoPlayerAccessory {
495
582
 
496
583
  // Day Max Volume
497
584
  const dayName = this.generateServiceName('Day Max Volume')
498
- const dayService = this.#accessory.getServiceById(Service.Fanv2, 'DayMaxVolume') ||
499
- this.#accessory.addService(Service.Fanv2, dayName, 'DayMaxVolume')
585
+ const dayService = this.#accessory.getServiceById(Service.Lightbulb, 'DayMaxVolume') ||
586
+ this.#accessory.addService(Service.Lightbulb, dayName, 'DayMaxVolume')
500
587
  syncServiceNames({ Characteristic, service: dayService, name: dayName })
501
588
 
502
589
  dayService
503
- .getCharacteristic(Characteristic.Active)
504
- .onGet(() => Characteristic.Active.ACTIVE)
590
+ .getCharacteristic(Characteristic.On)
591
+ .onGet(() => true)
592
+ .onSet((value) => {
593
+ if (!value) {
594
+ dayService.updateCharacteristic(Characteristic.On, true)
595
+ }
596
+ })
597
+ dayService.setCharacteristic(Characteristic.On, true)
505
598
 
506
599
  dayService
507
- .getCharacteristic(Characteristic.RotationSpeed)
600
+ .getCharacteristic(Characteristic.Brightness)
508
601
  .setProps({ minValue: 0, maxValue: 16, minStep: 1 })
509
602
  .onGet(this.getDayMaxVolume.bind(this))
510
603
  .onSet(this.setDayMaxVolume.bind(this))
@@ -513,16 +606,22 @@ export class YotoPlayerAccessory {
513
606
 
514
607
  // Night Max Volume
515
608
  const nightName = this.generateServiceName('Night Max Volume')
516
- const nightService = this.#accessory.getServiceById(Service.Fanv2, 'NightMaxVolume') ||
517
- this.#accessory.addService(Service.Fanv2, nightName, 'NightMaxVolume')
609
+ const nightService = this.#accessory.getServiceById(Service.Lightbulb, 'NightMaxVolume') ||
610
+ this.#accessory.addService(Service.Lightbulb, nightName, 'NightMaxVolume')
518
611
  syncServiceNames({ Characteristic, service: nightService, name: nightName })
519
612
 
520
613
  nightService
521
- .getCharacteristic(Characteristic.Active)
522
- .onGet(() => Characteristic.Active.ACTIVE)
614
+ .getCharacteristic(Characteristic.On)
615
+ .onGet(() => true)
616
+ .onSet((value) => {
617
+ if (!value) {
618
+ nightService.updateCharacteristic(Characteristic.On, true)
619
+ }
620
+ })
621
+ nightService.setCharacteristic(Characteristic.On, true)
523
622
 
524
623
  nightService
525
- .getCharacteristic(Characteristic.RotationSpeed)
624
+ .getCharacteristic(Characteristic.Brightness)
526
625
  .setProps({ minValue: 0, maxValue: 16, minStep: 1 })
527
626
  .onGet(this.getNightMaxVolume.bind(this))
528
627
  .onSet(this.setNightMaxVolume.bind(this))
@@ -564,10 +663,6 @@ export class YotoPlayerAccessory {
564
663
  this.updateFirmwareVersionCharacteristic(status.firmwareVersion)
565
664
  break
566
665
 
567
- case 'maxVolume':
568
- this.updateVolumeLimitProps(status.maxVolume)
569
- break
570
-
571
666
  case 'temperatureCelsius':
572
667
  if (this.#deviceModel.capabilities.hasTemperatureSensor && status.temperatureCelsius !== null) {
573
668
  this.updateTemperatureCharacteristic(status.temperatureCelsius)
@@ -582,14 +677,12 @@ export class YotoPlayerAccessory {
582
677
  break
583
678
 
584
679
  case 'dayMode':
585
- // Update night mode OccupancySensor
680
+ // Update night mode ContactSensor
586
681
  this.updateNightModeCharacteristic()
587
682
  // Update nightlight status ContactSensors (depends on dayMode)
588
683
  if (this.#deviceModel.capabilities.hasColoredNightlight) {
589
684
  this.updateNightlightStatusCharacteristics()
590
685
  }
591
- // Day/night mode affects which volume limit is active
592
- this.updateVolumeLimitProps(status.maxVolume)
593
686
  break
594
687
 
595
688
  case 'cardInsertionState':
@@ -598,6 +691,7 @@ export class YotoPlayerAccessory {
598
691
 
599
692
  // Available but not yet mapped to characteristics
600
693
  case 'activeCardId':
694
+ case 'maxVolume':
601
695
  case 'powerSource':
602
696
  case 'wifiStrength':
603
697
  case 'freeDiskSpaceBytes':
@@ -774,12 +868,12 @@ export class YotoPlayerAccessory {
774
868
  })
775
869
 
776
870
  // Lifecycle events
777
- this.#deviceModel.on('online', ({ reason }) => {
871
+ this.#deviceModel.on('online', ({ reason: _reason }) => {
778
872
  // Platform logs online/offline events to avoid duplicate output.
779
873
  this.updateOnlineStatusCharacteristic(true)
780
874
  })
781
875
 
782
- this.#deviceModel.on('offline', ({ reason }) => {
876
+ this.#deviceModel.on('offline', ({ reason: _reason }) => {
783
877
  // Platform logs online/offline events to avoid duplicate output.
784
878
  this.updateOnlineStatusCharacteristic(false)
785
879
  })
@@ -827,6 +921,7 @@ export class YotoPlayerAccessory {
827
921
  * @returns {Promise<CharacteristicValue>}
828
922
  */
829
923
  async getVolume () {
924
+ this.#log.debug(LOG_PREFIX.ACCESSORY, `[${this.#device.name}] Get volume`)
830
925
  return this.#deviceModel.status.volume
831
926
  }
832
927
 
@@ -850,6 +945,10 @@ export class YotoPlayerAccessory {
850
945
  ? deviceModel.status.maxVolume
851
946
  : 16
852
947
  const steps = Math.max(0, Math.min(Math.round(requestedSteps), maxVolumeSteps))
948
+ this.#log.debug(
949
+ LOG_PREFIX.ACCESSORY,
950
+ `[${this.#device.name}] Set volume raw=${value} steps=${requestedSteps} -> ${steps} (max ${maxVolumeSteps}/16)`
951
+ )
853
952
 
854
953
  // Track last non-zero volume for unmute
855
954
  if (steps > 0) {
@@ -861,17 +960,13 @@ export class YotoPlayerAccessory {
861
960
  if (this.volumeService) {
862
961
  const { Characteristic } = this.#platform
863
962
 
864
- const active = steps === 0
865
- ? Characteristic.Active.INACTIVE
866
- : Characteristic.Active.ACTIVE
867
-
868
963
  this.volumeService
869
- .getCharacteristic(Characteristic.Active)
870
- .updateValue(active)
964
+ .getCharacteristic(Characteristic.On)
965
+ .updateValue(steps > 0)
871
966
 
872
967
  if (steps !== requestedSteps) {
873
968
  this.volumeService
874
- .getCharacteristic(Characteristic.RotationSpeed)
969
+ .getCharacteristic(Characteristic.Brightness)
875
970
  .updateValue(steps)
876
971
  }
877
972
  }
@@ -884,45 +979,34 @@ export class YotoPlayerAccessory {
884
979
  }
885
980
 
886
981
  /**
887
- * Get volume Active state (derived from volume === 0)
982
+ * Get volume On state (derived from volume > 0)
888
983
  * @returns {Promise<CharacteristicValue>}
889
984
  */
890
- async getVolumeActive () {
891
- const { Characteristic } = this.#platform
892
- return this.#deviceModel.status.volume === 0
893
- ? Characteristic.Active.INACTIVE
894
- : Characteristic.Active.ACTIVE
985
+ async getVolumeOn () {
986
+ return this.#deviceModel.status.volume > 0
895
987
  }
896
988
 
897
989
  /**
898
- * Set volume Active state (mute/unmute)
990
+ * Set volume On state (mute/unmute)
899
991
  * @param {CharacteristicValue} value
900
992
  * @returns {Promise<void>}
901
993
  */
902
- async setVolumeActive (value) {
903
- this.#log.debug(LOG_PREFIX.ACCESSORY, `[${this.#device.name}] Set volume active:`, value)
994
+ async setVolumeOn (value) {
995
+ this.#log.debug(LOG_PREFIX.ACCESSORY, `[${this.#device.name}] Set volume on:`, value)
904
996
 
905
- const { Characteristic } = this.#platform
906
- const active = typeof value === 'number' ? value : Number(value)
907
- if (!Number.isFinite(active)) {
908
- throw new this.#platform.api.hap.HapStatusError(
909
- this.#platform.api.hap.HAPStatus.INVALID_VALUE_IN_REQUEST
910
- )
911
- }
997
+ const isOn = Boolean(value)
998
+ const currentVolume = this.#deviceModel.status.volume
912
999
 
913
- if (active === Characteristic.Active.INACTIVE) {
914
- await this.setVolume(0)
1000
+ if (!isOn) {
1001
+ if (currentVolume !== 0) {
1002
+ await this.setVolume(0)
1003
+ }
915
1004
  return
916
1005
  }
917
1006
 
918
- if (active === Characteristic.Active.ACTIVE) {
1007
+ if (currentVolume === 0) {
919
1008
  await this.setVolume(this.#lastNonZeroVolume)
920
- return
921
1009
  }
922
-
923
- throw new this.#platform.api.hap.HapStatusError(
924
- this.#platform.api.hap.HAPStatus.INVALID_VALUE_IN_REQUEST
925
- )
926
1010
  }
927
1011
 
928
1012
  /**
@@ -933,6 +1017,17 @@ export class YotoPlayerAccessory {
933
1017
  return this.#deviceModel.status.isOnline
934
1018
  }
935
1019
 
1020
+ /**
1021
+ * Get online status as a ContactSensorState
1022
+ * @returns {Promise<CharacteristicValue>}
1023
+ */
1024
+ async getOnlineStatus () {
1025
+ const { Characteristic } = this.#platform
1026
+ return this.#deviceModel.status.isOnline
1027
+ ? Characteristic.ContactSensorState.CONTACT_DETECTED
1028
+ : Characteristic.ContactSensorState.CONTACT_NOT_DETECTED
1029
+ }
1030
+
936
1031
  // ==================== Battery Characteristic Handlers ====================
937
1032
 
938
1033
  /**
@@ -940,7 +1035,8 @@ export class YotoPlayerAccessory {
940
1035
  * @returns {Promise<CharacteristicValue>}
941
1036
  */
942
1037
  async getBatteryLevel () {
943
- return this.#deviceModel.status.batteryLevelPercentage || 100
1038
+ const battery = this.#deviceModel.status.batteryLevelPercentage
1039
+ return Number.isFinite(battery) ? battery : 100
944
1040
  }
945
1041
 
946
1042
  /**
@@ -959,8 +1055,9 @@ export class YotoPlayerAccessory {
959
1055
  * @returns {Promise<CharacteristicValue>}
960
1056
  */
961
1057
  async getStatusLowBattery () {
962
- const battery = this.#deviceModel.status.batteryLevelPercentage || 100
963
- return battery <= LOW_BATTERY_THRESHOLD
1058
+ const battery = this.#deviceModel.status.batteryLevelPercentage
1059
+ const batteryLevel = Number.isFinite(battery) ? battery : 100
1060
+ return batteryLevel <= LOW_BATTERY_THRESHOLD
964
1061
  ? this.#platform.Characteristic.StatusLowBattery.BATTERY_LEVEL_LOW
965
1062
  : this.#platform.Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL
966
1063
  }
@@ -1077,7 +1174,14 @@ export class YotoPlayerAccessory {
1077
1174
  * @param {CharacteristicValue} value
1078
1175
  */
1079
1176
  async setDayNightlightBrightness (value) {
1080
- const brightnessValue = Math.round(Number(value))
1177
+ const rawBrightness = Number(value)
1178
+ if (!Number.isFinite(rawBrightness)) {
1179
+ throw new this.#platform.api.hap.HapStatusError(
1180
+ this.#platform.api.hap.HAPStatus.INVALID_VALUE_IN_REQUEST
1181
+ )
1182
+ }
1183
+
1184
+ const brightnessValue = Math.max(0, Math.min(Math.round(rawBrightness), 100))
1081
1185
  this.#log.debug(LOG_PREFIX.ACCESSORY, `Setting day display brightness: ${brightnessValue}`)
1082
1186
  await this.#deviceModel.updateConfig({
1083
1187
  dayDisplayBrightness: brightnessValue,
@@ -1104,7 +1208,13 @@ export class YotoPlayerAccessory {
1104
1208
  * @param {CharacteristicValue} value
1105
1209
  */
1106
1210
  async setDayNightlightHue (value) {
1107
- const hue = Number(value)
1211
+ const rawHue = Number(value)
1212
+ if (!Number.isFinite(rawHue)) {
1213
+ throw new this.#platform.api.hap.HapStatusError(
1214
+ this.#platform.api.hap.HAPStatus.INVALID_VALUE_IN_REQUEST
1215
+ )
1216
+ }
1217
+ const hue = Math.max(0, Math.min(rawHue, 360))
1108
1218
 
1109
1219
  // Get current saturation to maintain it
1110
1220
  const currentColor = this.#deviceModel.config.ambientColour
@@ -1142,7 +1252,13 @@ export class YotoPlayerAccessory {
1142
1252
  * @param {CharacteristicValue} value
1143
1253
  */
1144
1254
  async setDayNightlightSaturation (value) {
1145
- const saturation = Number(value)
1255
+ const rawSaturation = Number(value)
1256
+ if (!Number.isFinite(rawSaturation)) {
1257
+ throw new this.#platform.api.hap.HapStatusError(
1258
+ this.#platform.api.hap.HAPStatus.INVALID_VALUE_IN_REQUEST
1259
+ )
1260
+ }
1261
+ const saturation = Math.max(0, Math.min(rawSaturation, 100))
1146
1262
 
1147
1263
  // Get current hue to maintain it
1148
1264
  const currentColor = this.#deviceModel.config.ambientColour
@@ -1210,7 +1326,14 @@ export class YotoPlayerAccessory {
1210
1326
  * @param {CharacteristicValue} value
1211
1327
  */
1212
1328
  async setNightNightlightBrightness (value) {
1213
- const brightnessValue = Math.round(Number(value))
1329
+ const rawBrightness = Number(value)
1330
+ if (!Number.isFinite(rawBrightness)) {
1331
+ throw new this.#platform.api.hap.HapStatusError(
1332
+ this.#platform.api.hap.HAPStatus.INVALID_VALUE_IN_REQUEST
1333
+ )
1334
+ }
1335
+
1336
+ const brightnessValue = Math.max(0, Math.min(Math.round(rawBrightness), 100))
1214
1337
  this.#log.debug(LOG_PREFIX.ACCESSORY, `Setting night display brightness: ${brightnessValue}`)
1215
1338
  await this.#deviceModel.updateConfig({
1216
1339
  nightDisplayBrightness: brightnessValue,
@@ -1237,7 +1360,13 @@ export class YotoPlayerAccessory {
1237
1360
  * @param {CharacteristicValue} value
1238
1361
  */
1239
1362
  async setNightNightlightHue (value) {
1240
- const hue = Number(value)
1363
+ const rawHue = Number(value)
1364
+ if (!Number.isFinite(rawHue)) {
1365
+ throw new this.#platform.api.hap.HapStatusError(
1366
+ this.#platform.api.hap.HAPStatus.INVALID_VALUE_IN_REQUEST
1367
+ )
1368
+ }
1369
+ const hue = Math.max(0, Math.min(rawHue, 360))
1241
1370
 
1242
1371
  // Get current saturation to maintain it
1243
1372
  const currentColor = this.#deviceModel.config.nightAmbientColour
@@ -1275,7 +1404,13 @@ export class YotoPlayerAccessory {
1275
1404
  * @param {CharacteristicValue} value
1276
1405
  */
1277
1406
  async setNightNightlightSaturation (value) {
1278
- const saturation = Number(value)
1407
+ const rawSaturation = Number(value)
1408
+ if (!Number.isFinite(rawSaturation)) {
1409
+ throw new this.#platform.api.hap.HapStatusError(
1410
+ this.#platform.api.hap.HAPStatus.INVALID_VALUE_IN_REQUEST
1411
+ )
1412
+ }
1413
+ const saturation = Math.max(0, Math.min(rawSaturation, 100))
1279
1414
 
1280
1415
  // Get current hue to maintain it
1281
1416
  const currentColor = this.#deviceModel.config.nightAmbientColour
@@ -1346,7 +1481,7 @@ export class YotoPlayerAccessory {
1346
1481
  return hasCard ? Characteristic.ContactSensorState.CONTACT_DETECTED : Characteristic.ContactSensorState.CONTACT_NOT_DETECTED
1347
1482
  }
1348
1483
 
1349
- // ==================== Night Mode OccupancySensor Getter ====================
1484
+ // ==================== Night Mode ContactSensor Getter ====================
1350
1485
 
1351
1486
  /**
1352
1487
  * Get night mode status
@@ -1356,7 +1491,9 @@ export class YotoPlayerAccessory {
1356
1491
  const { Characteristic } = this.#platform
1357
1492
  const status = this.#deviceModel.status
1358
1493
  const isNightMode = status.dayMode === 'night'
1359
- return isNightMode ? Characteristic.OccupancyDetected.OCCUPANCY_DETECTED : Characteristic.OccupancyDetected.OCCUPANCY_NOT_DETECTED
1494
+ return isNightMode
1495
+ ? Characteristic.ContactSensorState.CONTACT_DETECTED
1496
+ : Characteristic.ContactSensorState.CONTACT_NOT_DETECTED
1360
1497
  }
1361
1498
 
1362
1499
  // ==================== Sleep Timer Switch Getter/Setter ====================
@@ -1402,7 +1539,7 @@ export class YotoPlayerAccessory {
1402
1539
  * @returns {Promise<CharacteristicValue>}
1403
1540
  */
1404
1541
  async getBluetoothState () {
1405
- return this.#deviceModel.config.bluetoothEnabled
1542
+ return this.#deviceModel.config.bluetoothEnabled ?? false
1406
1543
  }
1407
1544
 
1408
1545
  /**
@@ -1415,7 +1552,7 @@ export class YotoPlayerAccessory {
1415
1552
  await this.#deviceModel.updateConfig({ bluetoothEnabled: enabled })
1416
1553
  }
1417
1554
 
1418
- // ==================== Volume Limit Fanv2 Getters/Setters ====================
1555
+ // ==================== Volume Limit Lightbulb Getters/Setters ====================
1419
1556
 
1420
1557
  /**
1421
1558
  * Get day max volume limit
@@ -1423,6 +1560,10 @@ export class YotoPlayerAccessory {
1423
1560
  */
1424
1561
  async getDayMaxVolume () {
1425
1562
  const limit = this.#deviceModel.config.maxVolumeLimit
1563
+ this.#log.debug(
1564
+ LOG_PREFIX.ACCESSORY,
1565
+ `[${this.#device.name}] Get day max volume limit: ${limit}`
1566
+ )
1426
1567
  return Number.isFinite(limit) ? limit : 16
1427
1568
  }
1428
1569
 
@@ -1439,7 +1580,10 @@ export class YotoPlayerAccessory {
1439
1580
  }
1440
1581
 
1441
1582
  const limit = Math.max(0, Math.min(Math.round(requested), 16))
1442
- this.#log.debug(LOG_PREFIX.ACCESSORY, `Setting day max volume limit: ${limit}/16`)
1583
+ this.#log.debug(
1584
+ LOG_PREFIX.ACCESSORY,
1585
+ `[${this.#device.name}] Set day max volume limit raw=${value} requested=${requested} -> ${limit}/16`
1586
+ )
1443
1587
  await this.#deviceModel.updateConfig({ maxVolumeLimit: limit })
1444
1588
  }
1445
1589
 
@@ -1449,6 +1593,10 @@ export class YotoPlayerAccessory {
1449
1593
  */
1450
1594
  async getNightMaxVolume () {
1451
1595
  const limit = this.#deviceModel.config.nightMaxVolumeLimit
1596
+ this.#log.debug(
1597
+ LOG_PREFIX.ACCESSORY,
1598
+ `[${this.#device.name}] Get night max volume limit: ${limit}`
1599
+ )
1452
1600
  return Number.isFinite(limit) ? limit : 10
1453
1601
  }
1454
1602
 
@@ -1465,7 +1613,10 @@ export class YotoPlayerAccessory {
1465
1613
  }
1466
1614
 
1467
1615
  const limit = Math.max(0, Math.min(Math.round(requested), 16))
1468
- this.#log.debug(LOG_PREFIX.ACCESSORY, `Setting night max volume limit: ${limit}/16`)
1616
+ this.#log.debug(
1617
+ LOG_PREFIX.ACCESSORY,
1618
+ `[${this.#device.name}] Set night max volume limit raw=${value} requested=${requested} -> ${limit}/16`
1619
+ )
1469
1620
  await this.#deviceModel.updateConfig({ nightMaxVolumeLimit: limit })
1470
1621
  }
1471
1622
 
@@ -1497,30 +1648,15 @@ export class YotoPlayerAccessory {
1497
1648
  this.#lastNonZeroVolume = volumeSteps
1498
1649
  }
1499
1650
 
1651
+ const normalizedVolume = Number.isFinite(volumeSteps) ? volumeSteps : 0
1652
+ const clampedVolume = Math.max(0, Math.min(normalizedVolume, 16))
1653
+ this.#log.debug(
1654
+ LOG_PREFIX.ACCESSORY,
1655
+ `[${this.#device.name}] Update volume characteristic raw=${volumeSteps} -> ${clampedVolume}`
1656
+ )
1500
1657
  this.volumeService
1501
- .getCharacteristic(Characteristic.RotationSpeed)
1502
- .updateValue(volumeSteps)
1503
- }
1504
-
1505
- /**
1506
- * Update volume limit props - adjusts max value based on day/night mode
1507
- * @param {number} maxVolume - Maximum volume limit (0-16)
1508
- */
1509
- updateVolumeLimitProps (maxVolume) {
1510
- if (!this.volumeService) return
1511
-
1512
- const { Characteristic } = this.#platform
1513
- const maxVolumeSteps = Number.isFinite(maxVolume) ? maxVolume : 16
1514
- const clampedMaxVolume = Math.max(0, Math.min(maxVolumeSteps, 16))
1515
- this.volumeService
1516
- .getCharacteristic(Characteristic.RotationSpeed)
1517
- .setProps({
1518
- minValue: 0,
1519
- maxValue: clampedMaxVolume,
1520
- minStep: 1,
1521
- })
1522
-
1523
- this.#log.debug(`[${this.#device.name}] Volume max is ${clampedMaxVolume}/16`)
1658
+ .getCharacteristic(Characteristic.Brightness)
1659
+ .updateValue(clampedVolume)
1524
1660
  }
1525
1661
 
1526
1662
  /**
@@ -1531,12 +1667,9 @@ export class YotoPlayerAccessory {
1531
1667
  if (!this.volumeService) return
1532
1668
 
1533
1669
  const { Characteristic } = this.#platform
1534
- const active = volume === 0
1535
- ? Characteristic.Active.INACTIVE
1536
- : Characteristic.Active.ACTIVE
1537
1670
  this.volumeService
1538
- .getCharacteristic(Characteristic.Active)
1539
- .updateValue(active)
1671
+ .getCharacteristic(Characteristic.On)
1672
+ .updateValue(volume > 0)
1540
1673
  }
1541
1674
 
1542
1675
  /**
@@ -1590,17 +1723,12 @@ export class YotoPlayerAccessory {
1590
1723
  * Update online status characteristic for all device-state services
1591
1724
  *
1592
1725
  * Services that need StatusActive (read device state, unavailable when offline):
1593
- * - Switch services (playback, seek)
1594
- * - Speaker (volume, mute)
1595
- * - Battery (battery level, charging state)
1596
1726
  * - TemperatureSensor (temperature reading)
1597
- * - ContactSensor (card insertion state) - when implemented
1598
- * - OccupancySensor (day/night mode from device) - when implemented
1599
- * - Switch (Sleep Timer - reads device playback state) - when implemented
1727
+ * - ContactSensor (online status, card insertion, day/night mode, nightlight status)
1600
1728
  *
1601
1729
  * Services that DON'T need StatusActive (config-based, work offline):
1602
1730
  * - Lightbulb services (ambient lights - config only)
1603
- * - Fanv2 services (max volume - config only)
1731
+ * - Lightbulb services (max volume - config only)
1604
1732
  * - Switch (Bluetooth - config only)
1605
1733
  * - StatelessProgrammableSwitch (shortcuts - config only)
1606
1734
  *
@@ -1609,26 +1737,14 @@ export class YotoPlayerAccessory {
1609
1737
  updateOnlineStatusCharacteristic (isOnline) {
1610
1738
  const { Characteristic } = this.#platform
1611
1739
 
1612
- // Update playback/volume services (device state)
1613
- if (this.playbackService) {
1614
- this.playbackService
1615
- .getCharacteristic(Characteristic.StatusActive)
1616
- .updateValue(isOnline)
1617
- }
1618
- if (this.volumeService) {
1619
- this.volumeService
1620
- .getCharacteristic(Characteristic.StatusActive)
1621
- .updateValue(isOnline)
1740
+ if (this.onlineStatusService) {
1741
+ this.onlineStatusService
1742
+ .getCharacteristic(Characteristic.ContactSensorState)
1743
+ .updateValue(isOnline
1744
+ ? Characteristic.ContactSensorState.CONTACT_DETECTED
1745
+ : Characteristic.ContactSensorState.CONTACT_NOT_DETECTED)
1622
1746
  }
1623
1747
 
1624
- // Update Battery (battery level, charging state)
1625
- // Battery Doesn't support this
1626
- // if (this.batteryService) {
1627
- // this.batteryService
1628
- // .getCharacteristic(Characteristic.StatusActive)
1629
- // .updateValue(isOnline)
1630
- // }
1631
-
1632
1748
  // Update TemperatureSensor (temperature reading)
1633
1749
  if (this.temperatureSensorService) {
1634
1750
  this.temperatureSensorService
@@ -1660,21 +1776,14 @@ export class YotoPlayerAccessory {
1660
1776
  .updateValue(isOnline)
1661
1777
  }
1662
1778
 
1663
- // Update night mode OccupancySensor (device state)
1779
+ // Update night mode ContactSensor (device state)
1664
1780
  if (this.nightModeService) {
1665
1781
  this.nightModeService
1666
1782
  .getCharacteristic(Characteristic.StatusActive)
1667
1783
  .updateValue(isOnline)
1668
1784
  }
1669
1785
 
1670
- // Update sleep timer Switch (reads device playback state)
1671
- if (this.sleepTimerService) {
1672
- this.sleepTimerService
1673
- .getCharacteristic(Characteristic.StatusActive)
1674
- .updateValue(isOnline)
1675
- }
1676
-
1677
- // Note: Config-based services (Nightlight Lightbulbs, Fanv2, Bluetooth Switch, Shortcuts)
1786
+ // Note: Config-based services (Nightlight Lightbulbs, Volume Limit Lightbulbs, Bluetooth Switch, Shortcuts)
1678
1787
  // do NOT get StatusActive updated - they work offline since they only read/write config
1679
1788
 
1680
1789
  // TODO: Add shortcut services when implemented
@@ -1753,7 +1862,7 @@ export class YotoPlayerAccessory {
1753
1862
  }
1754
1863
 
1755
1864
  /**
1756
- * Update night mode OccupancySensor characteristic
1865
+ * Update night mode ContactSensor characteristic
1757
1866
  */
1758
1867
  updateNightModeCharacteristic () {
1759
1868
  if (!this.nightModeService) {
@@ -1765,8 +1874,8 @@ export class YotoPlayerAccessory {
1765
1874
  const isNightMode = status.dayMode === 'night'
1766
1875
 
1767
1876
  this.nightModeService
1768
- .getCharacteristic(Characteristic.OccupancyDetected)
1769
- .updateValue(isNightMode ? Characteristic.OccupancyDetected.OCCUPANCY_DETECTED : Characteristic.OccupancyDetected.OCCUPANCY_NOT_DETECTED)
1877
+ .getCharacteristic(Characteristic.ContactSensorState)
1878
+ .updateValue(isNightMode ? Characteristic.ContactSensorState.CONTACT_DETECTED : Characteristic.ContactSensorState.CONTACT_NOT_DETECTED)
1770
1879
  }
1771
1880
 
1772
1881
  /**
@@ -1782,7 +1891,7 @@ export class YotoPlayerAccessory {
1782
1891
 
1783
1892
  this.sleepTimerService
1784
1893
  .getCharacteristic(Characteristic.On)
1785
- .updateValue(playback.sleepTimerActive)
1894
+ .updateValue(playback.sleepTimerActive ?? false)
1786
1895
  }
1787
1896
 
1788
1897
  /**
@@ -1794,7 +1903,7 @@ export class YotoPlayerAccessory {
1794
1903
  }
1795
1904
 
1796
1905
  const { Characteristic } = this.#platform
1797
- const enabled = this.#deviceModel.config.bluetoothEnabled
1906
+ const enabled = this.#deviceModel.config.bluetoothEnabled ?? false
1798
1907
 
1799
1908
  this.bluetoothService
1800
1909
  .getCharacteristic(Characteristic.On)
@@ -1802,7 +1911,7 @@ export class YotoPlayerAccessory {
1802
1911
  }
1803
1912
 
1804
1913
  /**
1805
- * Update volume limit Fanv2 characteristics
1914
+ * Update volume limit Lightbulb characteristics
1806
1915
  */
1807
1916
  updateVolumeLimitCharacteristics () {
1808
1917
  const config = this.#deviceModel.config
@@ -1810,16 +1919,26 @@ export class YotoPlayerAccessory {
1810
1919
 
1811
1920
  if (this.dayMaxVolumeService) {
1812
1921
  const limit = Number.isFinite(config.maxVolumeLimit) ? config.maxVolumeLimit : 16
1922
+ const clampedLimit = Math.max(0, Math.min(limit, 16))
1923
+ this.#log.debug(
1924
+ LOG_PREFIX.ACCESSORY,
1925
+ `[${this.#device.name}] Update day max volume characteristic raw=${limit} -> ${clampedLimit}`
1926
+ )
1813
1927
  this.dayMaxVolumeService
1814
- .getCharacteristic(Characteristic.RotationSpeed)
1815
- .updateValue(limit)
1928
+ .getCharacteristic(Characteristic.Brightness)
1929
+ .updateValue(clampedLimit)
1816
1930
  }
1817
1931
 
1818
1932
  if (this.nightMaxVolumeService) {
1819
1933
  const limit = Number.isFinite(config.nightMaxVolumeLimit) ? config.nightMaxVolumeLimit : 10
1934
+ const clampedLimit = Math.max(0, Math.min(limit, 16))
1935
+ this.#log.debug(
1936
+ LOG_PREFIX.ACCESSORY,
1937
+ `[${this.#device.name}] Update night max volume characteristic raw=${limit} -> ${clampedLimit}`
1938
+ )
1820
1939
  this.nightMaxVolumeService
1821
- .getCharacteristic(Characteristic.RotationSpeed)
1822
- .updateValue(limit)
1940
+ .getCharacteristic(Characteristic.Brightness)
1941
+ .updateValue(clampedLimit)
1823
1942
  }
1824
1943
  }
1825
1944
 
package/lib/platform.js CHANGED
@@ -70,7 +70,7 @@ export class YotoPlatform {
70
70
  return
71
71
  }
72
72
 
73
- log.info('Authentication tokens found, initializing Yoto account...')
73
+ log.debug('Authentication tokens found, initializing Yoto account...')
74
74
 
75
75
  const { updateHomebridgeConfig, sessionId } = this
76
76
 
@@ -158,7 +158,7 @@ export class YotoPlatform {
158
158
  }
159
159
 
160
160
  try {
161
- this.log.info('Starting Yoto account...')
161
+ this.log.debug('Starting Yoto account...')
162
162
 
163
163
  // Listen for devices being added
164
164
  this.yotoAccount.on('deviceAdded', async ({ deviceId }) => {
@@ -22,13 +22,16 @@ export function syncServiceNames ({
22
22
 
23
23
  service.updateCharacteristic(Characteristic.Name, sanitizedName)
24
24
 
25
- // Set ConfiguredName on all services, not just ones that say they support it.
26
- // This is the only way to set the service name inside an accessory.
27
- // const hasConfiguredNameCharacteristic = service.characteristics.some(c => c.UUID === Characteristic.ConfiguredName.UUID)
28
- // const hasConfiguredNameOptional = service.optionalCharacteristics.some(c => c.UUID === Characteristic.ConfiguredName.UUID)
29
- // if (!hasConfiguredNameCharacteristic && !hasConfiguredNameOptional) {
30
- // service.addOptionalCharacteristic(Characteristic.ConfiguredName)
31
- // }
25
+ // Add ConfiguredName when missing so we avoid HAP warnings on update.
26
+ const configuredNameUuid = Characteristic.ConfiguredName.UUID
27
+ const hasConfiguredNameCharacteristic = service.characteristics
28
+ .some((characteristic) => characteristic.UUID === configuredNameUuid)
29
+ const hasConfiguredNameOptional = service.optionalCharacteristics
30
+ .some((characteristic) => characteristic.UUID === configuredNameUuid)
31
+
32
+ if (!hasConfiguredNameCharacteristic && !hasConfiguredNameOptional) {
33
+ service.addOptionalCharacteristic(Characteristic.ConfiguredName)
34
+ }
32
35
 
33
36
  service.updateCharacteristic(Characteristic.ConfiguredName, sanitizedName)
34
37
  }
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.35",
4
+ "version": "0.0.37",
5
5
  "author": "Bret Comnes <bcomnes@gmail.com> (https://bret.io)",
6
6
  "bugs": {
7
7
  "url": "https://github.com/bcomnes/homebridge-yoto/issues"