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 +8 -0
- package/config.schema.json +81 -0
- package/homebridge-ui/public/client.js +14 -8
- package/lib/accessory.js +294 -175
- package/lib/platform.js +2 -2
- package/lib/sync-service-names.js +10 -7
- package/package.json +1 -1
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
|
package/config.schema.json
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
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
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
|
266
|
+
* Setup online/offline ContactSensor service (PRIMARY)
|
|
182
267
|
*/
|
|
183
|
-
|
|
184
|
-
this
|
|
185
|
-
this.
|
|
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
|
|
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
|
|
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.
|
|
224
|
-
this.#accessory.addService(Service.
|
|
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.
|
|
230
|
-
.onGet(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.
|
|
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
|
-
|
|
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
|
|
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.
|
|
439
|
-
this.#accessory.addService(Service.
|
|
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.
|
|
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
|
|
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.
|
|
499
|
-
this.#accessory.addService(Service.
|
|
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.
|
|
504
|
-
.onGet(() =>
|
|
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.
|
|
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.
|
|
517
|
-
this.#accessory.addService(Service.
|
|
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.
|
|
522
|
-
.onGet(() =>
|
|
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.
|
|
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
|
|
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.
|
|
870
|
-
.updateValue(
|
|
964
|
+
.getCharacteristic(Characteristic.On)
|
|
965
|
+
.updateValue(steps > 0)
|
|
871
966
|
|
|
872
967
|
if (steps !== requestedSteps) {
|
|
873
968
|
this.volumeService
|
|
874
|
-
.getCharacteristic(Characteristic.
|
|
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
|
|
982
|
+
* Get volume On state (derived from volume > 0)
|
|
888
983
|
* @returns {Promise<CharacteristicValue>}
|
|
889
984
|
*/
|
|
890
|
-
async
|
|
891
|
-
|
|
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
|
|
990
|
+
* Set volume On state (mute/unmute)
|
|
899
991
|
* @param {CharacteristicValue} value
|
|
900
992
|
* @returns {Promise<void>}
|
|
901
993
|
*/
|
|
902
|
-
async
|
|
903
|
-
this.#log.debug(LOG_PREFIX.ACCESSORY, `[${this.#device.name}] Set volume
|
|
994
|
+
async setVolumeOn (value) {
|
|
995
|
+
this.#log.debug(LOG_PREFIX.ACCESSORY, `[${this.#device.name}] Set volume on:`, value)
|
|
904
996
|
|
|
905
|
-
const
|
|
906
|
-
const
|
|
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 (
|
|
914
|
-
|
|
1000
|
+
if (!isOn) {
|
|
1001
|
+
if (currentVolume !== 0) {
|
|
1002
|
+
await this.setVolume(0)
|
|
1003
|
+
}
|
|
915
1004
|
return
|
|
916
1005
|
}
|
|
917
1006
|
|
|
918
|
-
if (
|
|
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
|
-
|
|
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
|
|
963
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
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(
|
|
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.
|
|
1502
|
-
.updateValue(
|
|
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.
|
|
1539
|
-
.updateValue(
|
|
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
|
|
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
|
-
* -
|
|
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
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
.
|
|
1616
|
-
|
|
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
|
|
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
|
-
//
|
|
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
|
|
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.
|
|
1769
|
-
.updateValue(isNightMode ? Characteristic.
|
|
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
|
|
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.
|
|
1815
|
-
.updateValue(
|
|
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.
|
|
1822
|
-
.updateValue(
|
|
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.
|
|
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.
|
|
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
|
-
//
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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.
|
|
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"
|