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