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