homebridge-yoto 0.0.1 → 0.0.3
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/AGENTS.md +253 -0
- package/PLAN.md +609 -0
- package/README.md +315 -6
- package/config.schema.json +222 -0
- package/index.js +15 -2
- package/index.test.js +4 -4
- package/lib/auth.js +220 -0
- package/lib/constants.js +157 -0
- package/lib/platform.js +269 -0
- package/lib/playerAccessory.js +1452 -0
- package/lib/types.js +233 -0
- package/lib/yotoApi.js +260 -0
- package/lib/yotoMqtt.js +521 -0
- package/package.json +14 -7
- package/lib/.keep +0 -0
|
@@ -0,0 +1,1452 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Yoto Player Accessory implementation - handles HomeKit services for a single player
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/** @import { PlatformAccessory, CharacteristicValue } from 'homebridge' */
|
|
6
|
+
/** @import { YotoPlatform } from './platform.js' */
|
|
7
|
+
/** @import { YotoAccessoryContext, YotoDeviceStatus, YotoPlaybackEvents } from './types.js' */
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
DEFAULT_MANUFACTURER,
|
|
11
|
+
DEFAULT_MODEL,
|
|
12
|
+
LOW_BATTERY_THRESHOLD,
|
|
13
|
+
PLAYBACK_STATUS,
|
|
14
|
+
CARD_INSERTION_STATE,
|
|
15
|
+
DEFAULT_CONFIG,
|
|
16
|
+
LOG_PREFIX
|
|
17
|
+
} from './constants.js'
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Yoto Player Accessory Handler
|
|
21
|
+
*/
|
|
22
|
+
export class YotoPlayerAccessory {
|
|
23
|
+
/**
|
|
24
|
+
* @param {YotoPlatform} platform - Platform instance
|
|
25
|
+
* @param {PlatformAccessory<YotoAccessoryContext>} accessory - Platform accessory
|
|
26
|
+
*/
|
|
27
|
+
constructor (platform, accessory) {
|
|
28
|
+
this.platform = platform
|
|
29
|
+
this.accessory = accessory
|
|
30
|
+
this.log = platform.log
|
|
31
|
+
|
|
32
|
+
// Get device info from context
|
|
33
|
+
this.device = accessory.context.device
|
|
34
|
+
|
|
35
|
+
// Cache for current state
|
|
36
|
+
this.currentStatus = accessory.context.lastStatus || null
|
|
37
|
+
this.currentEvents = accessory.context.lastEvents || null
|
|
38
|
+
this.lastUpdateTime = Date.now()
|
|
39
|
+
|
|
40
|
+
this.log.debug(LOG_PREFIX.ACCESSORY, `[${this.device.name}] Initializing accessory`)
|
|
41
|
+
|
|
42
|
+
// Set up services
|
|
43
|
+
this.setupAccessoryInformation()
|
|
44
|
+
this.setupSmartSpeakerService()
|
|
45
|
+
|
|
46
|
+
// Optional services based on config
|
|
47
|
+
if (this.platform.config.exposeBattery !== false) {
|
|
48
|
+
this.setupBatteryService()
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (this.platform.config.exposeTemperature) {
|
|
52
|
+
this.setupTemperatureService()
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (this.platform.config.exposeConnectionStatus) {
|
|
56
|
+
this.setupConnectionStatusService()
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (this.platform.config.exposeCardDetection) {
|
|
60
|
+
this.setupCardDetectionService()
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Display brightness control (optional)
|
|
64
|
+
if (this.platform.config.exposeDisplayBrightness !== false) {
|
|
65
|
+
this.setupDisplayBrightnessService()
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Advanced control switches (optional)
|
|
69
|
+
if (this.platform.config.exposeAdvancedControls) {
|
|
70
|
+
this.setupAdvancedControlSwitches()
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Sleep timer control (optional)
|
|
74
|
+
if (this.platform.config.exposeSleepTimer) {
|
|
75
|
+
this.setupSleepTimerService()
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Volume limits control (optional)
|
|
79
|
+
if (this.platform.config.exposeVolumeLimits) {
|
|
80
|
+
this.setupVolumeLimitsServices()
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Ambient light control (optional)
|
|
84
|
+
if (this.platform.config.exposeAmbientLight) {
|
|
85
|
+
this.setupAmbientLightService()
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Active content tracking (optional)
|
|
89
|
+
if (this.platform.config.exposeActiveContent !== false) {
|
|
90
|
+
/** @type {string | null} */
|
|
91
|
+
this.activeContentCardId = null
|
|
92
|
+
/** @type {import('./types.js').YotoCardContent | null} */
|
|
93
|
+
this.activeContentInfo = null
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Subscribe to MQTT updates
|
|
97
|
+
this.subscribeMqtt()
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Set up accessory information service
|
|
102
|
+
*/
|
|
103
|
+
setupAccessoryInformation () {
|
|
104
|
+
const infoService = this.accessory.getService(this.platform.Service.AccessoryInformation)
|
|
105
|
+
if (infoService) {
|
|
106
|
+
infoService
|
|
107
|
+
.setCharacteristic(this.platform.Characteristic.Manufacturer, DEFAULT_MANUFACTURER)
|
|
108
|
+
.setCharacteristic(this.platform.Characteristic.Model, this.device.deviceType || DEFAULT_MODEL)
|
|
109
|
+
.setCharacteristic(this.platform.Characteristic.SerialNumber, this.device.deviceId)
|
|
110
|
+
.setCharacteristic(this.platform.Characteristic.FirmwareRevision, this.device.releaseChannel || '1.0.0')
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Set up smart speaker service for media control
|
|
116
|
+
*/
|
|
117
|
+
setupSmartSpeakerService () {
|
|
118
|
+
// Get or create the SmartSpeaker service
|
|
119
|
+
this.speakerService =
|
|
120
|
+
this.accessory.getService(this.platform.Service.SmartSpeaker) ||
|
|
121
|
+
this.accessory.addService(this.platform.Service.SmartSpeaker)
|
|
122
|
+
|
|
123
|
+
this.speakerService.setCharacteristic(this.platform.Characteristic.Name, this.device.name)
|
|
124
|
+
|
|
125
|
+
// Current Media State (read-only)
|
|
126
|
+
this.speakerService
|
|
127
|
+
.getCharacteristic(this.platform.Characteristic.CurrentMediaState)
|
|
128
|
+
.onGet(this.getCurrentMediaState.bind(this))
|
|
129
|
+
|
|
130
|
+
// Target Media State (control playback)
|
|
131
|
+
this.speakerService
|
|
132
|
+
.getCharacteristic(this.platform.Characteristic.TargetMediaState)
|
|
133
|
+
.onGet(this.getTargetMediaState.bind(this))
|
|
134
|
+
.onSet(this.setTargetMediaState.bind(this))
|
|
135
|
+
|
|
136
|
+
// Volume
|
|
137
|
+
this.speakerService
|
|
138
|
+
.getCharacteristic(this.platform.Characteristic.Volume)
|
|
139
|
+
.onGet(this.getVolume.bind(this))
|
|
140
|
+
.onSet(this.setVolume.bind(this))
|
|
141
|
+
|
|
142
|
+
// Mute
|
|
143
|
+
this.speakerService
|
|
144
|
+
.getCharacteristic(this.platform.Characteristic.Mute)
|
|
145
|
+
.onGet(this.getMute.bind(this))
|
|
146
|
+
.onSet(this.setMute.bind(this))
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Set up battery service
|
|
151
|
+
*/
|
|
152
|
+
setupBatteryService () {
|
|
153
|
+
this.batteryService =
|
|
154
|
+
this.accessory.getService(this.platform.Service.Battery) ||
|
|
155
|
+
this.accessory.addService(this.platform.Service.Battery)
|
|
156
|
+
|
|
157
|
+
this.batteryService.setCharacteristic(this.platform.Characteristic.Name, `${this.device.name} Battery`)
|
|
158
|
+
|
|
159
|
+
// Battery Level
|
|
160
|
+
this.batteryService
|
|
161
|
+
.getCharacteristic(this.platform.Characteristic.BatteryLevel)
|
|
162
|
+
.onGet(this.getBatteryLevel.bind(this))
|
|
163
|
+
|
|
164
|
+
// Charging State
|
|
165
|
+
this.batteryService
|
|
166
|
+
.getCharacteristic(this.platform.Characteristic.ChargingState)
|
|
167
|
+
.onGet(this.getChargingState.bind(this))
|
|
168
|
+
|
|
169
|
+
// Status Low Battery
|
|
170
|
+
this.batteryService
|
|
171
|
+
.getCharacteristic(this.platform.Characteristic.StatusLowBattery)
|
|
172
|
+
.onGet(this.getStatusLowBattery.bind(this))
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Set up temperature sensor service
|
|
177
|
+
*/
|
|
178
|
+
setupTemperatureService () {
|
|
179
|
+
this.temperatureService =
|
|
180
|
+
this.accessory.getService(this.platform.Service.TemperatureSensor) ||
|
|
181
|
+
this.accessory.addService(this.platform.Service.TemperatureSensor)
|
|
182
|
+
|
|
183
|
+
this.temperatureService.setCharacteristic(this.platform.Characteristic.Name, `${this.device.name} Temperature`)
|
|
184
|
+
|
|
185
|
+
this.temperatureService
|
|
186
|
+
.getCharacteristic(this.platform.Characteristic.CurrentTemperature)
|
|
187
|
+
.onGet(this.getCurrentTemperature.bind(this))
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Set up connection status sensor (occupancy sensor)
|
|
192
|
+
*/
|
|
193
|
+
setupConnectionStatusService () {
|
|
194
|
+
this.connectionService =
|
|
195
|
+
this.accessory.getService(this.platform.Service.OccupancySensor) ||
|
|
196
|
+
this.accessory.addService(this.platform.Service.OccupancySensor)
|
|
197
|
+
|
|
198
|
+
this.connectionService.setCharacteristic(this.platform.Characteristic.Name, `${this.device.name} Connection`)
|
|
199
|
+
|
|
200
|
+
this.connectionService
|
|
201
|
+
.getCharacteristic(this.platform.Characteristic.OccupancyDetected)
|
|
202
|
+
.onGet(this.getOccupancyDetected.bind(this))
|
|
203
|
+
|
|
204
|
+
this.connectionService
|
|
205
|
+
.getCharacteristic(this.platform.Characteristic.StatusActive)
|
|
206
|
+
.onGet(this.getStatusActive.bind(this))
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Set up card detection sensor (contact sensor)
|
|
211
|
+
*/
|
|
212
|
+
setupCardDetectionService () {
|
|
213
|
+
this.cardDetectionService =
|
|
214
|
+
this.accessory.getService(this.platform.Service.ContactSensor) ||
|
|
215
|
+
this.accessory.addService(this.platform.Service.ContactSensor)
|
|
216
|
+
|
|
217
|
+
this.cardDetectionService.setCharacteristic(this.platform.Characteristic.Name, `${this.device.name} Card`)
|
|
218
|
+
|
|
219
|
+
this.cardDetectionService
|
|
220
|
+
.getCharacteristic(this.platform.Characteristic.ContactSensorState)
|
|
221
|
+
.onGet(this.getContactSensorState.bind(this))
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Set up display brightness control (lightbulb service)
|
|
226
|
+
*/
|
|
227
|
+
setupDisplayBrightnessService () {
|
|
228
|
+
this.displayService =
|
|
229
|
+
this.accessory.getService(this.platform.Service.Lightbulb) ||
|
|
230
|
+
this.accessory.addService(this.platform.Service.Lightbulb)
|
|
231
|
+
|
|
232
|
+
this.displayService.setCharacteristic(this.platform.Characteristic.Name, `${this.device.name} Display`)
|
|
233
|
+
|
|
234
|
+
// On/Off state
|
|
235
|
+
this.displayService
|
|
236
|
+
.getCharacteristic(this.platform.Characteristic.On)
|
|
237
|
+
.onGet(this.getDisplayOn.bind(this))
|
|
238
|
+
.onSet(this.setDisplayOn.bind(this))
|
|
239
|
+
|
|
240
|
+
// Brightness
|
|
241
|
+
this.displayService
|
|
242
|
+
.getCharacteristic(this.platform.Characteristic.Brightness)
|
|
243
|
+
.onGet(this.getDisplayBrightness.bind(this))
|
|
244
|
+
.onSet(this.setDisplayBrightness.bind(this))
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Set up advanced control switches
|
|
249
|
+
*/
|
|
250
|
+
setupAdvancedControlSwitches () {
|
|
251
|
+
// Bluetooth enabled switch
|
|
252
|
+
this.bluetoothSwitch =
|
|
253
|
+
this.accessory.getService(`${this.device.name} Bluetooth`) ||
|
|
254
|
+
this.accessory.addService(this.platform.Service.Switch, `${this.device.name} Bluetooth`, 'bluetooth')
|
|
255
|
+
|
|
256
|
+
this.bluetoothSwitch.setCharacteristic(this.platform.Characteristic.Name, `${this.device.name} Bluetooth`)
|
|
257
|
+
|
|
258
|
+
this.bluetoothSwitch
|
|
259
|
+
.getCharacteristic(this.platform.Characteristic.On)
|
|
260
|
+
.onGet(this.getBluetoothEnabled.bind(this))
|
|
261
|
+
.onSet(this.setBluetoothEnabled.bind(this))
|
|
262
|
+
|
|
263
|
+
// Repeat all switch
|
|
264
|
+
this.repeatSwitch =
|
|
265
|
+
this.accessory.getService(`${this.device.name} Repeat`) ||
|
|
266
|
+
this.accessory.addService(this.platform.Service.Switch, `${this.device.name} Repeat`, 'repeat')
|
|
267
|
+
|
|
268
|
+
this.repeatSwitch.setCharacteristic(this.platform.Characteristic.Name, `${this.device.name} Repeat`)
|
|
269
|
+
|
|
270
|
+
this.repeatSwitch
|
|
271
|
+
.getCharacteristic(this.platform.Characteristic.On)
|
|
272
|
+
.onGet(this.getRepeatAll.bind(this))
|
|
273
|
+
.onSet(this.setRepeatAll.bind(this))
|
|
274
|
+
|
|
275
|
+
// Bluetooth headphones switch
|
|
276
|
+
this.btHeadphonesSwitch =
|
|
277
|
+
this.accessory.getService(`${this.device.name} BT Headphones`) ||
|
|
278
|
+
this.accessory.addService(this.platform.Service.Switch, `${this.device.name} BT Headphones`, 'bt-headphones')
|
|
279
|
+
|
|
280
|
+
this.btHeadphonesSwitch.setCharacteristic(this.platform.Characteristic.Name, `${this.device.name} BT Headphones`)
|
|
281
|
+
|
|
282
|
+
this.btHeadphonesSwitch
|
|
283
|
+
.getCharacteristic(this.platform.Characteristic.On)
|
|
284
|
+
.onGet(this.getBtHeadphonesEnabled.bind(this))
|
|
285
|
+
.onSet(this.setBtHeadphonesEnabled.bind(this))
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Set up sleep timer control (fan service)
|
|
290
|
+
*/
|
|
291
|
+
setupSleepTimerService () {
|
|
292
|
+
this.sleepTimerService =
|
|
293
|
+
this.accessory.getService(this.platform.Service.Fanv2) ||
|
|
294
|
+
this.accessory.addService(this.platform.Service.Fanv2)
|
|
295
|
+
|
|
296
|
+
this.sleepTimerService.setCharacteristic(this.platform.Characteristic.Name, `${this.device.name} Sleep Timer`)
|
|
297
|
+
|
|
298
|
+
// Active state (timer on/off)
|
|
299
|
+
this.sleepTimerService
|
|
300
|
+
.getCharacteristic(this.platform.Characteristic.Active)
|
|
301
|
+
.onGet(this.getSleepTimerActive.bind(this))
|
|
302
|
+
.onSet(this.setSleepTimerActive.bind(this))
|
|
303
|
+
|
|
304
|
+
// Rotation speed represents minutes (0-100 mapped to 0-120 minutes)
|
|
305
|
+
this.sleepTimerService
|
|
306
|
+
.getCharacteristic(this.platform.Characteristic.RotationSpeed)
|
|
307
|
+
.setProps({ minValue: 0, maxValue: 100, minStep: 1 })
|
|
308
|
+
.onGet(this.getSleepTimerMinutes.bind(this))
|
|
309
|
+
.onSet(this.setSleepTimerMinutes.bind(this))
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Set up volume limit controls (lightbulb services)
|
|
314
|
+
*/
|
|
315
|
+
setupVolumeLimitsServices () {
|
|
316
|
+
// Day volume limit
|
|
317
|
+
this.dayVolumeLimitService =
|
|
318
|
+
this.accessory.getService(`${this.device.name} Day Volume Limit`) ||
|
|
319
|
+
this.accessory.addService(this.platform.Service.Lightbulb, `${this.device.name} Day Volume Limit`, 'day-volume-limit')
|
|
320
|
+
|
|
321
|
+
this.dayVolumeLimitService.setCharacteristic(this.platform.Characteristic.Name, `${this.device.name} Day Volume Limit`)
|
|
322
|
+
|
|
323
|
+
this.dayVolumeLimitService
|
|
324
|
+
.getCharacteristic(this.platform.Characteristic.On)
|
|
325
|
+
.onGet(this.getDayVolumeLimitEnabled.bind(this))
|
|
326
|
+
.onSet(this.setDayVolumeLimitEnabled.bind(this))
|
|
327
|
+
|
|
328
|
+
this.dayVolumeLimitService
|
|
329
|
+
.getCharacteristic(this.platform.Characteristic.Brightness)
|
|
330
|
+
.setProps({ minValue: 0, maxValue: 100, minStep: 1 })
|
|
331
|
+
.onGet(this.getDayVolumeLimit.bind(this))
|
|
332
|
+
.onSet(this.setDayVolumeLimit.bind(this))
|
|
333
|
+
|
|
334
|
+
// Night volume limit
|
|
335
|
+
this.nightVolumeLimitService =
|
|
336
|
+
this.accessory.getService(`${this.device.name} Night Volume Limit`) ||
|
|
337
|
+
this.accessory.addService(this.platform.Service.Lightbulb, `${this.device.name} Night Volume Limit`, 'night-volume-limit')
|
|
338
|
+
|
|
339
|
+
this.nightVolumeLimitService.setCharacteristic(this.platform.Characteristic.Name, `${this.device.name} Night Volume Limit`)
|
|
340
|
+
|
|
341
|
+
this.nightVolumeLimitService
|
|
342
|
+
.getCharacteristic(this.platform.Characteristic.On)
|
|
343
|
+
.onGet(this.getNightVolumeLimitEnabled.bind(this))
|
|
344
|
+
.onSet(this.setNightVolumeLimitEnabled.bind(this))
|
|
345
|
+
|
|
346
|
+
this.nightVolumeLimitService
|
|
347
|
+
.getCharacteristic(this.platform.Characteristic.Brightness)
|
|
348
|
+
.setProps({ minValue: 0, maxValue: 100, minStep: 1 })
|
|
349
|
+
.onGet(this.getNightVolumeLimit.bind(this))
|
|
350
|
+
.onSet(this.setNightVolumeLimit.bind(this))
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Set up ambient light control (lightbulb service with color)
|
|
355
|
+
*/
|
|
356
|
+
setupAmbientLightService () {
|
|
357
|
+
this.ambientLightService =
|
|
358
|
+
this.accessory.getService(`${this.device.name} Ambient Light`) ||
|
|
359
|
+
this.accessory.addService(this.platform.Service.Lightbulb, `${this.device.name} Ambient Light`, 'ambient-light')
|
|
360
|
+
|
|
361
|
+
this.ambientLightService.setCharacteristic(this.platform.Characteristic.Name, `${this.device.name} Ambient Light`)
|
|
362
|
+
|
|
363
|
+
// On/Off state
|
|
364
|
+
this.ambientLightService
|
|
365
|
+
.getCharacteristic(this.platform.Characteristic.On)
|
|
366
|
+
.onGet(this.getAmbientLightOn.bind(this))
|
|
367
|
+
.onSet(this.setAmbientLightOn.bind(this))
|
|
368
|
+
|
|
369
|
+
// Hue
|
|
370
|
+
this.ambientLightService
|
|
371
|
+
.getCharacteristic(this.platform.Characteristic.Hue)
|
|
372
|
+
.onGet(this.getAmbientLightHue.bind(this))
|
|
373
|
+
.onSet(this.setAmbientLightHue.bind(this))
|
|
374
|
+
|
|
375
|
+
// Saturation
|
|
376
|
+
this.ambientLightService
|
|
377
|
+
.getCharacteristic(this.platform.Characteristic.Saturation)
|
|
378
|
+
.onGet(this.getAmbientLightSaturation.bind(this))
|
|
379
|
+
.onSet(this.setAmbientLightSaturation.bind(this))
|
|
380
|
+
|
|
381
|
+
// Brightness
|
|
382
|
+
this.ambientLightService
|
|
383
|
+
.getCharacteristic(this.platform.Characteristic.Brightness)
|
|
384
|
+
.onGet(this.getAmbientLightBrightness.bind(this))
|
|
385
|
+
.onSet(this.setAmbientLightBrightness.bind(this))
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Subscribe to MQTT updates for this device
|
|
390
|
+
*/
|
|
391
|
+
async subscribeMqtt () {
|
|
392
|
+
try {
|
|
393
|
+
await this.platform.yotoMqtt.subscribeToDevice(this.device.deviceId, {
|
|
394
|
+
onStatus: this.handleStatusUpdate.bind(this),
|
|
395
|
+
onEvents: this.handleEventsUpdate.bind(this),
|
|
396
|
+
onResponse: this.handleCommandResponse.bind(this)
|
|
397
|
+
})
|
|
398
|
+
} catch (error) {
|
|
399
|
+
this.log.error(LOG_PREFIX.ACCESSORY, `[${this.device.name}] Failed to subscribe to MQTT:`, error)
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Handle device status update from MQTT
|
|
405
|
+
* @param {YotoDeviceStatus} status - Device status
|
|
406
|
+
*/
|
|
407
|
+
handleStatusUpdate (status) {
|
|
408
|
+
this.log.debug(LOG_PREFIX.ACCESSORY, `[${this.device.name}] Status update received`)
|
|
409
|
+
this.currentStatus = status
|
|
410
|
+
this.lastUpdateTime = Date.now()
|
|
411
|
+
this.accessory.context.lastStatus = status
|
|
412
|
+
|
|
413
|
+
// Update all characteristics
|
|
414
|
+
this.updateCharacteristics()
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* Handle playback events update from MQTT
|
|
419
|
+
* @param {YotoPlaybackEvents} events - Playback events
|
|
420
|
+
*/
|
|
421
|
+
handleEventsUpdate (events) {
|
|
422
|
+
this.log.debug(LOG_PREFIX.ACCESSORY, `[${this.device.name}] Events update received`)
|
|
423
|
+
this.currentEvents = events
|
|
424
|
+
this.lastUpdateTime = Date.now()
|
|
425
|
+
this.accessory.context.lastEvents = events
|
|
426
|
+
|
|
427
|
+
// Track active content changes
|
|
428
|
+
if (this.platform.config.exposeActiveContent !== false && events.cardId) {
|
|
429
|
+
this.handleActiveContentChange(events.cardId)
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// Update playback-related characteristics
|
|
433
|
+
this.updatePlaybackCharacteristics()
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* Handle command response from MQTT
|
|
438
|
+
* @param {import('./types.js').MqttCommandResponse} response - Command response
|
|
439
|
+
*/
|
|
440
|
+
handleCommandResponse (response) {
|
|
441
|
+
this.log.debug(LOG_PREFIX.ACCESSORY, `[${this.device.name}] Command response:`, response)
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* Update all characteristics with current state
|
|
446
|
+
*/
|
|
447
|
+
updateCharacteristics () {
|
|
448
|
+
if (!this.currentStatus) {
|
|
449
|
+
return
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// Update volume
|
|
453
|
+
if (this.speakerService) {
|
|
454
|
+
this.speakerService.updateCharacteristic(
|
|
455
|
+
this.platform.Characteristic.Volume,
|
|
456
|
+
this.currentStatus.userVolume
|
|
457
|
+
)
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// Update battery
|
|
461
|
+
if (this.batteryService) {
|
|
462
|
+
this.batteryService.updateCharacteristic(
|
|
463
|
+
this.platform.Characteristic.BatteryLevel,
|
|
464
|
+
this.currentStatus.batteryLevel
|
|
465
|
+
)
|
|
466
|
+
|
|
467
|
+
this.batteryService.updateCharacteristic(
|
|
468
|
+
this.platform.Characteristic.ChargingState,
|
|
469
|
+
this.currentStatus.charging
|
|
470
|
+
? this.platform.Characteristic.ChargingState.CHARGING
|
|
471
|
+
: this.platform.Characteristic.ChargingState.NOT_CHARGING
|
|
472
|
+
)
|
|
473
|
+
|
|
474
|
+
this.batteryService.updateCharacteristic(
|
|
475
|
+
this.platform.Characteristic.StatusLowBattery,
|
|
476
|
+
this.currentStatus.batteryLevel < LOW_BATTERY_THRESHOLD
|
|
477
|
+
? this.platform.Characteristic.StatusLowBattery.BATTERY_LEVEL_LOW
|
|
478
|
+
: this.platform.Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL
|
|
479
|
+
)
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// Update temperature
|
|
483
|
+
if (this.temperatureService && this.currentStatus.temp) {
|
|
484
|
+
const temp = parseFloat(this.currentStatus.temp)
|
|
485
|
+
if (!isNaN(temp)) {
|
|
486
|
+
this.temperatureService.updateCharacteristic(
|
|
487
|
+
this.platform.Characteristic.CurrentTemperature,
|
|
488
|
+
temp
|
|
489
|
+
)
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// Update display brightness
|
|
493
|
+
if (this.displayService) {
|
|
494
|
+
const isOn = this.currentStatus.dnowBrightness > 0
|
|
495
|
+
this.displayService.updateCharacteristic(
|
|
496
|
+
this.platform.Characteristic.On,
|
|
497
|
+
isOn
|
|
498
|
+
)
|
|
499
|
+
|
|
500
|
+
// Map brightness (0-100) from device brightness value
|
|
501
|
+
const brightness = Math.min(100, Math.max(0, this.currentStatus.dnowBrightness))
|
|
502
|
+
this.displayService.updateCharacteristic(
|
|
503
|
+
this.platform.Characteristic.Brightness,
|
|
504
|
+
brightness
|
|
505
|
+
)
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// Update advanced control switches
|
|
509
|
+
if (this.bluetoothSwitch) {
|
|
510
|
+
const bluetoothEnabled = this.currentStatus.bluetoothHp
|
|
511
|
+
this.bluetoothSwitch.updateCharacteristic(
|
|
512
|
+
this.platform.Characteristic.On,
|
|
513
|
+
bluetoothEnabled
|
|
514
|
+
)
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
if (this.btHeadphonesSwitch) {
|
|
518
|
+
const btHeadphonesEnabled = this.currentStatus.bluetoothHp
|
|
519
|
+
this.btHeadphonesSwitch.updateCharacteristic(
|
|
520
|
+
this.platform.Characteristic.On,
|
|
521
|
+
btHeadphonesEnabled
|
|
522
|
+
)
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// Update repeat all from events if available
|
|
527
|
+
if (this.currentEvents && this.repeatSwitch) {
|
|
528
|
+
const repeatAll = this.currentEvents.repeatAll === 'true'
|
|
529
|
+
this.repeatSwitch.updateCharacteristic(
|
|
530
|
+
this.platform.Characteristic.On,
|
|
531
|
+
repeatAll
|
|
532
|
+
)
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// Update sleep timer from events if available
|
|
536
|
+
if (this.currentEvents && this.sleepTimerService) {
|
|
537
|
+
const sleepTimerActive = this.currentEvents.sleepTimerActive === 'true'
|
|
538
|
+
this.sleepTimerService.updateCharacteristic(
|
|
539
|
+
this.platform.Characteristic.Active,
|
|
540
|
+
sleepTimerActive
|
|
541
|
+
? this.platform.Characteristic.Active.ACTIVE
|
|
542
|
+
: this.platform.Characteristic.Active.INACTIVE
|
|
543
|
+
)
|
|
544
|
+
|
|
545
|
+
if (sleepTimerActive && this.currentEvents.sleepTimerSeconds) {
|
|
546
|
+
const seconds = parseInt(this.currentEvents.sleepTimerSeconds)
|
|
547
|
+
const minutes = Math.round(seconds / 60)
|
|
548
|
+
// Map minutes (0-120) to rotation speed (0-100)
|
|
549
|
+
const rotationSpeed = Math.min(100, Math.round((minutes / 120) * 100))
|
|
550
|
+
this.sleepTimerService.updateCharacteristic(
|
|
551
|
+
this.platform.Characteristic.RotationSpeed,
|
|
552
|
+
rotationSpeed
|
|
553
|
+
)
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
// Update connection status
|
|
558
|
+
if (this.connectionService) {
|
|
559
|
+
const isOnline = this.isDeviceOnline()
|
|
560
|
+
this.connectionService.updateCharacteristic(
|
|
561
|
+
this.platform.Characteristic.OccupancyDetected,
|
|
562
|
+
isOnline
|
|
563
|
+
? this.platform.Characteristic.OccupancyDetected.OCCUPANCY_DETECTED
|
|
564
|
+
: this.platform.Characteristic.OccupancyDetected.OCCUPANCY_NOT_DETECTED
|
|
565
|
+
)
|
|
566
|
+
|
|
567
|
+
this.connectionService.updateCharacteristic(
|
|
568
|
+
this.platform.Characteristic.StatusActive,
|
|
569
|
+
isOnline
|
|
570
|
+
)
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
// Update card detection
|
|
574
|
+
if (this.cardDetectionService) {
|
|
575
|
+
const cardInserted = this.currentStatus.cardInserted !== CARD_INSERTION_STATE.NONE
|
|
576
|
+
this.cardDetectionService.updateCharacteristic(
|
|
577
|
+
this.platform.Characteristic.ContactSensorState,
|
|
578
|
+
cardInserted
|
|
579
|
+
? this.platform.Characteristic.ContactSensorState.CONTACT_DETECTED
|
|
580
|
+
: this.platform.Characteristic.ContactSensorState.CONTACT_NOT_DETECTED
|
|
581
|
+
)
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
/**
|
|
586
|
+
* Update playback-related characteristics
|
|
587
|
+
*/
|
|
588
|
+
updatePlaybackCharacteristics () {
|
|
589
|
+
if (!this.currentEvents || !this.speakerService) {
|
|
590
|
+
return
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
// Map playback status to HomeKit media state
|
|
594
|
+
let mediaState = this.platform.Characteristic.CurrentMediaState.STOP
|
|
595
|
+
|
|
596
|
+
if (this.currentEvents.playbackStatus === PLAYBACK_STATUS.PLAYING) {
|
|
597
|
+
mediaState = this.platform.Characteristic.CurrentMediaState.PLAY
|
|
598
|
+
} else if (this.currentEvents.playbackStatus === PLAYBACK_STATUS.PAUSED) {
|
|
599
|
+
mediaState = this.platform.Characteristic.CurrentMediaState.PAUSE
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
this.speakerService.updateCharacteristic(
|
|
603
|
+
this.platform.Characteristic.CurrentMediaState,
|
|
604
|
+
mediaState
|
|
605
|
+
)
|
|
606
|
+
|
|
607
|
+
this.speakerService.updateCharacteristic(
|
|
608
|
+
this.platform.Characteristic.TargetMediaState,
|
|
609
|
+
mediaState
|
|
610
|
+
)
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
/**
|
|
614
|
+
* Check if device is considered online based on last update time
|
|
615
|
+
* @returns {boolean}
|
|
616
|
+
*/
|
|
617
|
+
isDeviceOnline () {
|
|
618
|
+
const timeoutMs = (this.platform.config.statusTimeoutSeconds || DEFAULT_CONFIG.statusTimeoutSeconds) * 1000
|
|
619
|
+
return Date.now() - this.lastUpdateTime < timeoutMs
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
// ==================== Characteristic Handlers ====================
|
|
623
|
+
|
|
624
|
+
/**
|
|
625
|
+
* Get current media state
|
|
626
|
+
* @returns {Promise<CharacteristicValue>}
|
|
627
|
+
*/
|
|
628
|
+
async getCurrentMediaState () {
|
|
629
|
+
if (!this.currentEvents) {
|
|
630
|
+
return this.platform.Characteristic.CurrentMediaState.STOP
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
if (this.currentEvents.playbackStatus === PLAYBACK_STATUS.PLAYING) {
|
|
634
|
+
return this.platform.Characteristic.CurrentMediaState.PLAY
|
|
635
|
+
} else if (this.currentEvents.playbackStatus === PLAYBACK_STATUS.PAUSED) {
|
|
636
|
+
return this.platform.Characteristic.CurrentMediaState.PAUSE
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
return this.platform.Characteristic.CurrentMediaState.STOP
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
/**
|
|
643
|
+
* Get target media state
|
|
644
|
+
* @returns {Promise<CharacteristicValue>}
|
|
645
|
+
*/
|
|
646
|
+
async getTargetMediaState () {
|
|
647
|
+
// Target state follows current state
|
|
648
|
+
return this.getCurrentMediaState()
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
/**
|
|
652
|
+
* Set target media state (play/pause/stop)
|
|
653
|
+
* @param {CharacteristicValue} value - Target state
|
|
654
|
+
*/
|
|
655
|
+
async setTargetMediaState (value) {
|
|
656
|
+
this.log.debug(LOG_PREFIX.ACCESSORY, `[${this.device.name}] Set target media state:`, value)
|
|
657
|
+
|
|
658
|
+
try {
|
|
659
|
+
if (value === this.platform.Characteristic.TargetMediaState.PLAY) {
|
|
660
|
+
await this.platform.yotoMqtt.resumeCard(this.device.deviceId)
|
|
661
|
+
} else if (value === this.platform.Characteristic.TargetMediaState.PAUSE) {
|
|
662
|
+
await this.platform.yotoMqtt.pauseCard(this.device.deviceId)
|
|
663
|
+
} else if (value === this.platform.Characteristic.TargetMediaState.STOP) {
|
|
664
|
+
await this.platform.yotoMqtt.stopCard(this.device.deviceId)
|
|
665
|
+
}
|
|
666
|
+
} catch (error) {
|
|
667
|
+
this.log.error(LOG_PREFIX.ACCESSORY, `[${this.device.name}] Failed to set media state:`, error)
|
|
668
|
+
throw new this.platform.api.hap.HapStatusError(
|
|
669
|
+
this.platform.api.hap.HAPStatus.SERVICE_COMMUNICATION_FAILURE
|
|
670
|
+
)
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
/**
|
|
675
|
+
* Get volume
|
|
676
|
+
* @returns {Promise<CharacteristicValue>}
|
|
677
|
+
*/
|
|
678
|
+
async getVolume () {
|
|
679
|
+
if (!this.currentStatus) {
|
|
680
|
+
return 50
|
|
681
|
+
}
|
|
682
|
+
return this.currentStatus.userVolume
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
/**
|
|
686
|
+
* Set volume
|
|
687
|
+
* @param {CharacteristicValue} value - Volume level (0-100)
|
|
688
|
+
*/
|
|
689
|
+
async setVolume (value) {
|
|
690
|
+
this.log.debug(LOG_PREFIX.ACCESSORY, `[${this.device.name}] Set volume:`, value)
|
|
691
|
+
|
|
692
|
+
try {
|
|
693
|
+
await this.platform.yotoMqtt.setVolume(this.device.deviceId, Number(value))
|
|
694
|
+
} catch (error) {
|
|
695
|
+
this.log.error(LOG_PREFIX.ACCESSORY, `[${this.device.name}] Failed to set volume:`, error)
|
|
696
|
+
throw new this.platform.api.hap.HapStatusError(
|
|
697
|
+
this.platform.api.hap.HAPStatus.SERVICE_COMMUNICATION_FAILURE
|
|
698
|
+
)
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
/**
|
|
703
|
+
* Get mute state
|
|
704
|
+
* @returns {Promise<CharacteristicValue>}
|
|
705
|
+
*/
|
|
706
|
+
async getMute () {
|
|
707
|
+
if (!this.currentStatus) {
|
|
708
|
+
return false
|
|
709
|
+
}
|
|
710
|
+
return this.currentStatus.userVolume === 0
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
/**
|
|
714
|
+
* Set mute state
|
|
715
|
+
* @param {CharacteristicValue} value - Mute state
|
|
716
|
+
*/
|
|
717
|
+
async setMute (value) {
|
|
718
|
+
this.log.debug(LOG_PREFIX.ACCESSORY, `[${this.device.name}] Set mute:`, value)
|
|
719
|
+
|
|
720
|
+
try {
|
|
721
|
+
if (value) {
|
|
722
|
+
// Mute - set volume to 0
|
|
723
|
+
await this.platform.yotoMqtt.setVolume(this.device.deviceId, 0)
|
|
724
|
+
} else {
|
|
725
|
+
// Unmute - restore to a reasonable volume if currently 0
|
|
726
|
+
const currentVolume = this.currentStatus?.userVolume || 0
|
|
727
|
+
const targetVolume = currentVolume === 0 ? 50 : currentVolume
|
|
728
|
+
await this.platform.yotoMqtt.setVolume(this.device.deviceId, targetVolume)
|
|
729
|
+
}
|
|
730
|
+
} catch (error) {
|
|
731
|
+
this.log.error(LOG_PREFIX.ACCESSORY, `[${this.device.name}] Failed to set mute:`, error)
|
|
732
|
+
throw new this.platform.api.hap.HapStatusError(
|
|
733
|
+
this.platform.api.hap.HAPStatus.SERVICE_COMMUNICATION_FAILURE
|
|
734
|
+
)
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
/**
|
|
739
|
+
* Get battery level
|
|
740
|
+
* @returns {Promise<CharacteristicValue>}
|
|
741
|
+
*/
|
|
742
|
+
async getBatteryLevel () {
|
|
743
|
+
if (!this.currentStatus) {
|
|
744
|
+
return 100
|
|
745
|
+
}
|
|
746
|
+
return this.currentStatus.batteryLevel
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
/**
|
|
750
|
+
* Get charging state
|
|
751
|
+
* @returns {Promise<CharacteristicValue>}
|
|
752
|
+
*/
|
|
753
|
+
async getChargingState () {
|
|
754
|
+
if (!this.currentStatus) {
|
|
755
|
+
return this.platform.Characteristic.ChargingState.NOT_CHARGING
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
return this.currentStatus.charging
|
|
759
|
+
? this.platform.Characteristic.ChargingState.CHARGING
|
|
760
|
+
: this.platform.Characteristic.ChargingState.NOT_CHARGING
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
/**
|
|
764
|
+
* Get low battery status
|
|
765
|
+
* @returns {Promise<CharacteristicValue>}
|
|
766
|
+
*/
|
|
767
|
+
async getStatusLowBattery () {
|
|
768
|
+
if (!this.currentStatus) {
|
|
769
|
+
return this.platform.Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
return this.currentStatus.batteryLevel < LOW_BATTERY_THRESHOLD
|
|
773
|
+
? this.platform.Characteristic.StatusLowBattery.BATTERY_LEVEL_LOW
|
|
774
|
+
: this.platform.Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
/**
|
|
778
|
+
* Get current temperature
|
|
779
|
+
* @returns {Promise<CharacteristicValue>}
|
|
780
|
+
*/
|
|
781
|
+
async getCurrentTemperature () {
|
|
782
|
+
if (!this.currentStatus || !this.currentStatus.temp) {
|
|
783
|
+
return 20
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
const temp = parseFloat(this.currentStatus.temp)
|
|
787
|
+
return isNaN(temp) ? 20 : temp
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
/**
|
|
791
|
+
* Get occupancy detected (connection status)
|
|
792
|
+
* @returns {Promise<CharacteristicValue>}
|
|
793
|
+
*/
|
|
794
|
+
async getOccupancyDetected () {
|
|
795
|
+
const isOnline = this.isDeviceOnline()
|
|
796
|
+
return isOnline
|
|
797
|
+
? this.platform.Characteristic.OccupancyDetected.OCCUPANCY_DETECTED
|
|
798
|
+
: this.platform.Characteristic.OccupancyDetected.OCCUPANCY_NOT_DETECTED
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
/**
|
|
802
|
+
* Get status active (connection status)
|
|
803
|
+
* @returns {Promise<CharacteristicValue>}
|
|
804
|
+
*/
|
|
805
|
+
async getStatusActive () {
|
|
806
|
+
return this.isDeviceOnline()
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
/**
|
|
810
|
+
* Get contact sensor state (card detection)
|
|
811
|
+
* @returns {Promise<CharacteristicValue>}
|
|
812
|
+
*/
|
|
813
|
+
async getContactSensorState () {
|
|
814
|
+
if (!this.currentStatus) {
|
|
815
|
+
return this.platform.Characteristic.ContactSensorState.CONTACT_NOT_DETECTED
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
const cardInserted = this.currentStatus.cardInserted !== CARD_INSERTION_STATE.NONE
|
|
819
|
+
return cardInserted
|
|
820
|
+
? this.platform.Characteristic.ContactSensorState.CONTACT_DETECTED
|
|
821
|
+
: this.platform.Characteristic.ContactSensorState.CONTACT_NOT_DETECTED
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
/**
|
|
825
|
+
* Get display on state
|
|
826
|
+
* @returns {Promise<CharacteristicValue>}
|
|
827
|
+
*/
|
|
828
|
+
async getDisplayOn () {
|
|
829
|
+
if (!this.currentStatus) {
|
|
830
|
+
return true
|
|
831
|
+
}
|
|
832
|
+
return this.currentStatus.dnowBrightness > 0
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
/**
|
|
836
|
+
* Set display on/off state
|
|
837
|
+
* @param {CharacteristicValue} value - On/off state
|
|
838
|
+
*/
|
|
839
|
+
async setDisplayOn (value) {
|
|
840
|
+
this.log.debug(LOG_PREFIX.ACCESSORY, `[${this.device.name}] Set display on:`, value)
|
|
841
|
+
|
|
842
|
+
try {
|
|
843
|
+
// Get current config to update brightness settings
|
|
844
|
+
const config = await this.platform.yotoApi.getDeviceConfig(this.device.deviceId)
|
|
845
|
+
|
|
846
|
+
if (value) {
|
|
847
|
+
// Turn on - restore to auto or previous brightness
|
|
848
|
+
config.config.dayDisplayBrightness = 'auto'
|
|
849
|
+
config.config.nightDisplayBrightness = 'auto'
|
|
850
|
+
} else {
|
|
851
|
+
// Turn off - set brightness to 0
|
|
852
|
+
config.config.dayDisplayBrightness = '0'
|
|
853
|
+
config.config.nightDisplayBrightness = '0'
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
await this.platform.yotoApi.updateDeviceConfig(this.device.deviceId, config)
|
|
857
|
+
} catch (error) {
|
|
858
|
+
this.log.error(LOG_PREFIX.ACCESSORY, `[${this.device.name}] Failed to set display on:`, error)
|
|
859
|
+
throw new this.platform.api.hap.HapStatusError(
|
|
860
|
+
this.platform.api.hap.HAPStatus.SERVICE_COMMUNICATION_FAILURE
|
|
861
|
+
)
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
/**
|
|
866
|
+
* Get display brightness
|
|
867
|
+
* @returns {Promise<CharacteristicValue>}
|
|
868
|
+
*/
|
|
869
|
+
async getDisplayBrightness () {
|
|
870
|
+
if (!this.currentStatus) {
|
|
871
|
+
return 100
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
// Use current brightness value (0-100)
|
|
875
|
+
return Math.min(100, Math.max(0, this.currentStatus.dnowBrightness))
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
/**
|
|
879
|
+
* Set display brightness
|
|
880
|
+
* @param {CharacteristicValue} value - Brightness level (0-100)
|
|
881
|
+
*/
|
|
882
|
+
async setDisplayBrightness (value) {
|
|
883
|
+
this.log.debug(LOG_PREFIX.ACCESSORY, `[${this.device.name}] Set display brightness:`, value)
|
|
884
|
+
|
|
885
|
+
try {
|
|
886
|
+
const config = await this.platform.yotoApi.getDeviceConfig(this.device.deviceId)
|
|
887
|
+
|
|
888
|
+
// Map 0-100 to brightness string
|
|
889
|
+
const brightness = String(Math.round(Number(value)))
|
|
890
|
+
|
|
891
|
+
// Update both day and night brightness
|
|
892
|
+
config.config.dayDisplayBrightness = brightness
|
|
893
|
+
config.config.nightDisplayBrightness = brightness
|
|
894
|
+
|
|
895
|
+
await this.platform.yotoApi.updateDeviceConfig(this.device.deviceId, config)
|
|
896
|
+
} catch (error) {
|
|
897
|
+
this.log.error(LOG_PREFIX.ACCESSORY, `[${this.device.name}] Failed to set display brightness:`, error)
|
|
898
|
+
throw new this.platform.api.hap.HapStatusError(
|
|
899
|
+
this.platform.api.hap.HAPStatus.SERVICE_COMMUNICATION_FAILURE
|
|
900
|
+
)
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
/**
|
|
905
|
+
* Get Bluetooth enabled state
|
|
906
|
+
* @returns {Promise<CharacteristicValue>}
|
|
907
|
+
*/
|
|
908
|
+
async getBluetoothEnabled () {
|
|
909
|
+
if (!this.currentStatus) {
|
|
910
|
+
return false
|
|
911
|
+
}
|
|
912
|
+
return this.currentStatus.bluetoothHp
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
/**
|
|
916
|
+
* Set Bluetooth enabled state
|
|
917
|
+
* @param {CharacteristicValue} value - Enabled state
|
|
918
|
+
*/
|
|
919
|
+
async setBluetoothEnabled (value) {
|
|
920
|
+
this.log.debug(LOG_PREFIX.ACCESSORY, `[${this.device.name}] Set Bluetooth enabled:`, value)
|
|
921
|
+
|
|
922
|
+
try {
|
|
923
|
+
const config = await this.platform.yotoApi.getDeviceConfig(this.device.deviceId)
|
|
924
|
+
config.config.bluetoothEnabled = value ? '1' : '0'
|
|
925
|
+
await this.platform.yotoApi.updateDeviceConfig(this.device.deviceId, config)
|
|
926
|
+
} catch (error) {
|
|
927
|
+
this.log.error(LOG_PREFIX.ACCESSORY, `[${this.device.name}] Failed to set Bluetooth:`, error)
|
|
928
|
+
throw new this.platform.api.hap.HapStatusError(
|
|
929
|
+
this.platform.api.hap.HAPStatus.SERVICE_COMMUNICATION_FAILURE
|
|
930
|
+
)
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
/**
|
|
935
|
+
* Get repeat all state
|
|
936
|
+
* @returns {Promise<CharacteristicValue>}
|
|
937
|
+
*/
|
|
938
|
+
async getRepeatAll () {
|
|
939
|
+
if (!this.currentEvents) {
|
|
940
|
+
return false
|
|
941
|
+
}
|
|
942
|
+
return this.currentEvents.repeatAll === 'true'
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
/**
|
|
946
|
+
* Set repeat all state
|
|
947
|
+
* @param {CharacteristicValue} value - Repeat state
|
|
948
|
+
*/
|
|
949
|
+
async setRepeatAll (value) {
|
|
950
|
+
this.log.debug(LOG_PREFIX.ACCESSORY, `[${this.device.name}] Set repeat all:`, value)
|
|
951
|
+
|
|
952
|
+
try {
|
|
953
|
+
const config = await this.platform.yotoApi.getDeviceConfig(this.device.deviceId)
|
|
954
|
+
config.config.repeatAll = Boolean(value)
|
|
955
|
+
await this.platform.yotoApi.updateDeviceConfig(this.device.deviceId, config)
|
|
956
|
+
} catch (error) {
|
|
957
|
+
this.log.error(LOG_PREFIX.ACCESSORY, `[${this.device.name}] Failed to set repeat:`, error)
|
|
958
|
+
throw new this.platform.api.hap.HapStatusError(
|
|
959
|
+
this.platform.api.hap.HAPStatus.SERVICE_COMMUNICATION_FAILURE
|
|
960
|
+
)
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
/**
|
|
965
|
+
* Get Bluetooth headphones enabled state
|
|
966
|
+
* @returns {Promise<CharacteristicValue>}
|
|
967
|
+
*/
|
|
968
|
+
async getBtHeadphonesEnabled () {
|
|
969
|
+
if (!this.currentStatus) {
|
|
970
|
+
return false
|
|
971
|
+
}
|
|
972
|
+
return this.currentStatus.bluetoothHp
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
/**
|
|
976
|
+
* Set Bluetooth headphones enabled state
|
|
977
|
+
* @param {CharacteristicValue} value - Enabled state
|
|
978
|
+
*/
|
|
979
|
+
async setBtHeadphonesEnabled (value) {
|
|
980
|
+
this.log.debug(LOG_PREFIX.ACCESSORY, `[${this.device.name}] Set BT headphones enabled:`, value)
|
|
981
|
+
|
|
982
|
+
try {
|
|
983
|
+
const config = await this.platform.yotoApi.getDeviceConfig(this.device.deviceId)
|
|
984
|
+
config.config.btHeadphonesEnabled = Boolean(value)
|
|
985
|
+
await this.platform.yotoApi.updateDeviceConfig(this.device.deviceId, config)
|
|
986
|
+
} catch (error) {
|
|
987
|
+
this.log.error(LOG_PREFIX.ACCESSORY, `[${this.device.name}] Failed to set BT headphones:`, error)
|
|
988
|
+
throw new this.platform.api.hap.HapStatusError(
|
|
989
|
+
this.platform.api.hap.HAPStatus.SERVICE_COMMUNICATION_FAILURE
|
|
990
|
+
)
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
/**
|
|
995
|
+
* Get sleep timer active state
|
|
996
|
+
* @returns {Promise<CharacteristicValue>}
|
|
997
|
+
*/
|
|
998
|
+
async getSleepTimerActive () {
|
|
999
|
+
if (!this.currentEvents) {
|
|
1000
|
+
return this.platform.Characteristic.Active.INACTIVE
|
|
1001
|
+
}
|
|
1002
|
+
return this.currentEvents.sleepTimerActive === 'true'
|
|
1003
|
+
? this.platform.Characteristic.Active.ACTIVE
|
|
1004
|
+
: this.platform.Characteristic.Active.INACTIVE
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
/**
|
|
1008
|
+
* Set sleep timer active state
|
|
1009
|
+
* @param {CharacteristicValue} value - Active state
|
|
1010
|
+
*/
|
|
1011
|
+
async setSleepTimerActive (value) {
|
|
1012
|
+
this.log.debug(LOG_PREFIX.ACCESSORY, `[${this.device.name}] Set sleep timer active:`, value)
|
|
1013
|
+
|
|
1014
|
+
try {
|
|
1015
|
+
if (value === this.platform.Characteristic.Active.INACTIVE) {
|
|
1016
|
+
// Turn off timer
|
|
1017
|
+
await this.platform.yotoMqtt.setSleepTimer(this.device.deviceId, 0)
|
|
1018
|
+
} else {
|
|
1019
|
+
// Turn on with default duration (30 minutes)
|
|
1020
|
+
await this.platform.yotoMqtt.setSleepTimer(this.device.deviceId, 30 * 60)
|
|
1021
|
+
}
|
|
1022
|
+
} catch (error) {
|
|
1023
|
+
this.log.error(LOG_PREFIX.ACCESSORY, `[${this.device.name}] Failed to set sleep timer active:`, error)
|
|
1024
|
+
throw new this.platform.api.hap.HapStatusError(
|
|
1025
|
+
this.platform.api.hap.HAPStatus.SERVICE_COMMUNICATION_FAILURE
|
|
1026
|
+
)
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
/**
|
|
1031
|
+
* Get sleep timer minutes (as rotation speed)
|
|
1032
|
+
* @returns {Promise<CharacteristicValue>}
|
|
1033
|
+
*/
|
|
1034
|
+
async getSleepTimerMinutes () {
|
|
1035
|
+
if (!this.currentEvents || this.currentEvents.sleepTimerActive !== 'true') {
|
|
1036
|
+
return 0
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
const seconds = parseInt(this.currentEvents.sleepTimerSeconds || '0')
|
|
1040
|
+
const minutes = Math.round(seconds / 60)
|
|
1041
|
+
// Map minutes (0-120) to rotation speed (0-100)
|
|
1042
|
+
return Math.min(100, Math.round((minutes / 120) * 100))
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
/**
|
|
1046
|
+
* Set sleep timer minutes (from rotation speed)
|
|
1047
|
+
* @param {CharacteristicValue} value - Rotation speed (0-100)
|
|
1048
|
+
*/
|
|
1049
|
+
async setSleepTimerMinutes (value) {
|
|
1050
|
+
this.log.debug(LOG_PREFIX.ACCESSORY, `[${this.device.name}] Set sleep timer minutes:`, value)
|
|
1051
|
+
|
|
1052
|
+
try {
|
|
1053
|
+
// Map rotation speed (0-100) to minutes (0-120)
|
|
1054
|
+
const minutes = Math.round((Number(value) / 100) * 120)
|
|
1055
|
+
const seconds = minutes * 60
|
|
1056
|
+
|
|
1057
|
+
if (seconds === 0) {
|
|
1058
|
+
// Turn off timer
|
|
1059
|
+
await this.platform.yotoMqtt.setSleepTimer(this.device.deviceId, 0)
|
|
1060
|
+
} else {
|
|
1061
|
+
// Set timer with specified duration
|
|
1062
|
+
await this.platform.yotoMqtt.setSleepTimer(this.device.deviceId, seconds)
|
|
1063
|
+
}
|
|
1064
|
+
} catch (error) {
|
|
1065
|
+
this.log.error(LOG_PREFIX.ACCESSORY, `[${this.device.name}] Failed to set sleep timer:`, error)
|
|
1066
|
+
throw new this.platform.api.hap.HapStatusError(
|
|
1067
|
+
this.platform.api.hap.HAPStatus.SERVICE_COMMUNICATION_FAILURE
|
|
1068
|
+
)
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
/**
|
|
1073
|
+
* Get day volume limit enabled state
|
|
1074
|
+
* @returns {Promise<CharacteristicValue>}
|
|
1075
|
+
*/
|
|
1076
|
+
async getDayVolumeLimitEnabled () {
|
|
1077
|
+
// Always enabled - brightness controls the limit
|
|
1078
|
+
return true
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
/**
|
|
1082
|
+
* Set day volume limit enabled state
|
|
1083
|
+
* @param {CharacteristicValue} _value - Enabled state (unused)
|
|
1084
|
+
*/
|
|
1085
|
+
async setDayVolumeLimitEnabled (_value) {
|
|
1086
|
+
// No-op - always enabled, use brightness to control
|
|
1087
|
+
this.log.debug(LOG_PREFIX.ACCESSORY, `[${this.device.name}] Day volume limit always enabled`)
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
/**
|
|
1091
|
+
* Get day volume limit (0-16 mapped to 0-100)
|
|
1092
|
+
* @returns {Promise<CharacteristicValue>}
|
|
1093
|
+
*/
|
|
1094
|
+
async getDayVolumeLimit () {
|
|
1095
|
+
try {
|
|
1096
|
+
const config = await this.platform.yotoApi.getDeviceConfig(this.device.deviceId)
|
|
1097
|
+
const limit = parseInt(config.config.maxVolumeLimit || '16')
|
|
1098
|
+
// Map 0-16 to 0-100
|
|
1099
|
+
return Math.round((limit / 16) * 100)
|
|
1100
|
+
} catch (error) {
|
|
1101
|
+
this.log.error(LOG_PREFIX.ACCESSORY, `[${this.device.name}] Failed to get day volume limit:`, error)
|
|
1102
|
+
return 100
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
/**
|
|
1107
|
+
* Set day volume limit (0-100 mapped to 0-16)
|
|
1108
|
+
* @param {CharacteristicValue} value - Brightness value (0-100)
|
|
1109
|
+
*/
|
|
1110
|
+
async setDayVolumeLimit (value) {
|
|
1111
|
+
this.log.debug(LOG_PREFIX.ACCESSORY, `[${this.device.name}] Set day volume limit:`, value)
|
|
1112
|
+
|
|
1113
|
+
try {
|
|
1114
|
+
const config = await this.platform.yotoApi.getDeviceConfig(this.device.deviceId)
|
|
1115
|
+
// Map 0-100 to 0-16
|
|
1116
|
+
const limit = Math.round((Number(value) / 100) * 16)
|
|
1117
|
+
config.config.maxVolumeLimit = String(limit)
|
|
1118
|
+
await this.platform.yotoApi.updateDeviceConfig(this.device.deviceId, config)
|
|
1119
|
+
} catch (error) {
|
|
1120
|
+
this.log.error(LOG_PREFIX.ACCESSORY, `[${this.device.name}] Failed to set day volume limit:`, error)
|
|
1121
|
+
throw new this.platform.api.hap.HapStatusError(
|
|
1122
|
+
this.platform.api.hap.HAPStatus.SERVICE_COMMUNICATION_FAILURE
|
|
1123
|
+
)
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
/**
|
|
1128
|
+
* Get night volume limit enabled state
|
|
1129
|
+
* @returns {Promise<CharacteristicValue>}
|
|
1130
|
+
*/
|
|
1131
|
+
async getNightVolumeLimitEnabled () {
|
|
1132
|
+
// Always enabled - brightness controls the limit
|
|
1133
|
+
return true
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
/**
|
|
1137
|
+
* Set night volume limit enabled state
|
|
1138
|
+
* @param {CharacteristicValue} _value - Enabled state (unused)
|
|
1139
|
+
*/
|
|
1140
|
+
async setNightVolumeLimitEnabled (_value) {
|
|
1141
|
+
// No-op - always enabled, use brightness to control
|
|
1142
|
+
this.log.debug(LOG_PREFIX.ACCESSORY, `[${this.device.name}] Night volume limit always enabled`)
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
/**
|
|
1146
|
+
* Get night volume limit (0-16 mapped to 0-100)
|
|
1147
|
+
* @returns {Promise<CharacteristicValue>}
|
|
1148
|
+
*/
|
|
1149
|
+
async getNightVolumeLimit () {
|
|
1150
|
+
try {
|
|
1151
|
+
const config = await this.platform.yotoApi.getDeviceConfig(this.device.deviceId)
|
|
1152
|
+
const limit = parseInt(config.config.nightMaxVolumeLimit || '16')
|
|
1153
|
+
// Map 0-16 to 0-100
|
|
1154
|
+
return Math.round((limit / 16) * 100)
|
|
1155
|
+
} catch (error) {
|
|
1156
|
+
this.log.error(LOG_PREFIX.ACCESSORY, `[${this.device.name}] Failed to get night volume limit:`, error)
|
|
1157
|
+
return 100
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
/**
|
|
1162
|
+
* Set night volume limit (0-100 mapped to 0-16)
|
|
1163
|
+
* @param {CharacteristicValue} value - Brightness value (0-100)
|
|
1164
|
+
*/
|
|
1165
|
+
async setNightVolumeLimit (value) {
|
|
1166
|
+
this.log.debug(LOG_PREFIX.ACCESSORY, `[${this.device.name}] Set night volume limit:`, value)
|
|
1167
|
+
|
|
1168
|
+
try {
|
|
1169
|
+
const config = await this.platform.yotoApi.getDeviceConfig(this.device.deviceId)
|
|
1170
|
+
// Map 0-100 to 0-16
|
|
1171
|
+
const limit = Math.round((Number(value) / 100) * 16)
|
|
1172
|
+
config.config.nightMaxVolumeLimit = String(limit)
|
|
1173
|
+
await this.platform.yotoApi.updateDeviceConfig(this.device.deviceId, config)
|
|
1174
|
+
} catch (error) {
|
|
1175
|
+
this.log.error(LOG_PREFIX.ACCESSORY, `[${this.device.name}] Failed to set night volume limit:`, error)
|
|
1176
|
+
throw new this.platform.api.hap.HapStatusError(
|
|
1177
|
+
this.platform.api.hap.HAPStatus.SERVICE_COMMUNICATION_FAILURE
|
|
1178
|
+
)
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
/**
|
|
1183
|
+
* Get ambient light on state
|
|
1184
|
+
* @returns {Promise<CharacteristicValue>}
|
|
1185
|
+
*/
|
|
1186
|
+
async getAmbientLightOn () {
|
|
1187
|
+
try {
|
|
1188
|
+
const config = await this.platform.yotoApi.getDeviceConfig(this.device.deviceId)
|
|
1189
|
+
const color = config.config.ambientColour || '#000000'
|
|
1190
|
+
// Off if color is black (#000000)
|
|
1191
|
+
return color !== '#000000' && color !== 'off'
|
|
1192
|
+
} catch (error) {
|
|
1193
|
+
this.log.error(LOG_PREFIX.ACCESSORY, `[${this.device.name}] Failed to get ambient light state:`, error)
|
|
1194
|
+
return false
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
/**
|
|
1199
|
+
* Set ambient light on/off state
|
|
1200
|
+
* @param {CharacteristicValue} value - On/off state
|
|
1201
|
+
*/
|
|
1202
|
+
async setAmbientLightOn (value) {
|
|
1203
|
+
this.log.debug(LOG_PREFIX.ACCESSORY, `[${this.device.name}] Set ambient light on:`, value)
|
|
1204
|
+
|
|
1205
|
+
try {
|
|
1206
|
+
if (value) {
|
|
1207
|
+
// Turn on - set to white or previous color
|
|
1208
|
+
await this.platform.yotoMqtt.setAmbientLight(this.device.deviceId, 255, 255, 255)
|
|
1209
|
+
} else {
|
|
1210
|
+
// Turn off - set to black
|
|
1211
|
+
await this.platform.yotoMqtt.setAmbientLight(this.device.deviceId, 0, 0, 0)
|
|
1212
|
+
}
|
|
1213
|
+
} catch (error) {
|
|
1214
|
+
this.log.error(LOG_PREFIX.ACCESSORY, `[${this.device.name}] Failed to set ambient light on:`, error)
|
|
1215
|
+
throw new this.platform.api.hap.HapStatusError(
|
|
1216
|
+
this.platform.api.hap.HAPStatus.SERVICE_COMMUNICATION_FAILURE
|
|
1217
|
+
)
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
/**
|
|
1222
|
+
* Get ambient light hue
|
|
1223
|
+
* @returns {Promise<CharacteristicValue>}
|
|
1224
|
+
*/
|
|
1225
|
+
async getAmbientLightHue () {
|
|
1226
|
+
try {
|
|
1227
|
+
const config = await this.platform.yotoApi.getDeviceConfig(this.device.deviceId)
|
|
1228
|
+
const hex = config.config.ambientColour || '#000000'
|
|
1229
|
+
const { h } = this.hexToHsv(hex)
|
|
1230
|
+
return h
|
|
1231
|
+
} catch (error) {
|
|
1232
|
+
this.log.error(LOG_PREFIX.ACCESSORY, `[${this.device.name}] Failed to get ambient light hue:`, error)
|
|
1233
|
+
return 0
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
/**
|
|
1238
|
+
* Set ambient light hue
|
|
1239
|
+
* @param {CharacteristicValue} value - Hue value (0-360)
|
|
1240
|
+
*/
|
|
1241
|
+
async setAmbientLightHue (value) {
|
|
1242
|
+
this.log.debug(LOG_PREFIX.ACCESSORY, `[${this.device.name}] Set ambient light hue:`, value)
|
|
1243
|
+
// Store for combined update with saturation and brightness
|
|
1244
|
+
this.pendingAmbientHue = Number(value)
|
|
1245
|
+
await this.updateAmbientLightColor()
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
/**
|
|
1249
|
+
* Get ambient light saturation
|
|
1250
|
+
* @returns {Promise<CharacteristicValue>}
|
|
1251
|
+
*/
|
|
1252
|
+
async getAmbientLightSaturation () {
|
|
1253
|
+
try {
|
|
1254
|
+
const config = await this.platform.yotoApi.getDeviceConfig(this.device.deviceId)
|
|
1255
|
+
const hex = config.config.ambientColour || '#000000'
|
|
1256
|
+
const { s } = this.hexToHsv(hex)
|
|
1257
|
+
return s
|
|
1258
|
+
} catch (error) {
|
|
1259
|
+
this.log.error(LOG_PREFIX.ACCESSORY, `[${this.device.name}] Failed to get ambient light saturation:`, error)
|
|
1260
|
+
return 0
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
/**
|
|
1265
|
+
* Set ambient light saturation
|
|
1266
|
+
* @param {CharacteristicValue} value - Saturation value (0-100)
|
|
1267
|
+
*/
|
|
1268
|
+
async setAmbientLightSaturation (value) {
|
|
1269
|
+
this.log.debug(LOG_PREFIX.ACCESSORY, `[${this.device.name}] Set ambient light saturation:`, value)
|
|
1270
|
+
// Store for combined update with hue and brightness
|
|
1271
|
+
this.pendingAmbientSaturation = Number(value)
|
|
1272
|
+
await this.updateAmbientLightColor()
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
/**
|
|
1276
|
+
* Get ambient light brightness
|
|
1277
|
+
* @returns {Promise<CharacteristicValue>}
|
|
1278
|
+
*/
|
|
1279
|
+
async getAmbientLightBrightness () {
|
|
1280
|
+
try {
|
|
1281
|
+
const config = await this.platform.yotoApi.getDeviceConfig(this.device.deviceId)
|
|
1282
|
+
const hex = config.config.ambientColour || '#000000'
|
|
1283
|
+
const { v } = this.hexToHsv(hex)
|
|
1284
|
+
return v
|
|
1285
|
+
} catch (error) {
|
|
1286
|
+
this.log.error(LOG_PREFIX.ACCESSORY, `[${this.device.name}] Failed to get ambient light brightness:`, error)
|
|
1287
|
+
return 100
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
/**
|
|
1292
|
+
* Set ambient light brightness
|
|
1293
|
+
* @param {CharacteristicValue} value - Brightness value (0-100)
|
|
1294
|
+
*/
|
|
1295
|
+
async setAmbientLightBrightness (value) {
|
|
1296
|
+
this.log.debug(LOG_PREFIX.ACCESSORY, `[${this.device.name}] Set ambient light brightness:`, value)
|
|
1297
|
+
// Store for combined update with hue and saturation
|
|
1298
|
+
this.pendingAmbientBrightness = Number(value)
|
|
1299
|
+
await this.updateAmbientLightColor()
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
/**
|
|
1303
|
+
* Update ambient light color using stored HSV values
|
|
1304
|
+
*/
|
|
1305
|
+
async updateAmbientLightColor () {
|
|
1306
|
+
try {
|
|
1307
|
+
// Get current config to read existing HSV values
|
|
1308
|
+
const config = await this.platform.yotoApi.getDeviceConfig(this.device.deviceId)
|
|
1309
|
+
const currentHex = config.config.ambientColour || '#ffffff'
|
|
1310
|
+
const currentHsv = this.hexToHsv(currentHex)
|
|
1311
|
+
|
|
1312
|
+
// Use pending values or current values
|
|
1313
|
+
const h = this.pendingAmbientHue !== undefined ? this.pendingAmbientHue : currentHsv.h
|
|
1314
|
+
const s = this.pendingAmbientSaturation !== undefined ? this.pendingAmbientSaturation : currentHsv.s
|
|
1315
|
+
const v = this.pendingAmbientBrightness !== undefined ? this.pendingAmbientBrightness : currentHsv.v
|
|
1316
|
+
|
|
1317
|
+
// Convert HSV to RGB
|
|
1318
|
+
const { r, g, b } = this.hsvToRgb(h, s, v)
|
|
1319
|
+
|
|
1320
|
+
// Send MQTT command
|
|
1321
|
+
await this.platform.yotoMqtt.setAmbientLight(this.device.deviceId, r, g, b)
|
|
1322
|
+
|
|
1323
|
+
// Clear pending values
|
|
1324
|
+
this.pendingAmbientHue = undefined
|
|
1325
|
+
this.pendingAmbientSaturation = undefined
|
|
1326
|
+
this.pendingAmbientBrightness = undefined
|
|
1327
|
+
} catch (error) {
|
|
1328
|
+
this.log.error(LOG_PREFIX.ACCESSORY, `[${this.device.name}] Failed to update ambient light:`, error)
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
/**
|
|
1333
|
+
* Convert hex color to HSV
|
|
1334
|
+
* @param {string} hex - Hex color string (e.g., "#ff3900")
|
|
1335
|
+
* @returns {{ h: number, s: number, v: number }} HSV values (h: 0-360, s: 0-100, v: 0-100)
|
|
1336
|
+
*/
|
|
1337
|
+
hexToHsv (hex) {
|
|
1338
|
+
// Remove # if present
|
|
1339
|
+
hex = hex.replace('#', '')
|
|
1340
|
+
|
|
1341
|
+
// Parse RGB
|
|
1342
|
+
const r = parseInt(hex.substring(0, 2), 16) / 255
|
|
1343
|
+
const g = parseInt(hex.substring(2, 4), 16) / 255
|
|
1344
|
+
const b = parseInt(hex.substring(4, 6), 16) / 255
|
|
1345
|
+
|
|
1346
|
+
const max = Math.max(r, g, b)
|
|
1347
|
+
const min = Math.min(r, g, b)
|
|
1348
|
+
const delta = max - min
|
|
1349
|
+
|
|
1350
|
+
let h = 0
|
|
1351
|
+
let s = 0
|
|
1352
|
+
const v = max
|
|
1353
|
+
|
|
1354
|
+
if (delta !== 0) {
|
|
1355
|
+
s = delta / max
|
|
1356
|
+
|
|
1357
|
+
if (max === r) {
|
|
1358
|
+
h = ((g - b) / delta + (g < b ? 6 : 0)) / 6
|
|
1359
|
+
} else if (max === g) {
|
|
1360
|
+
h = ((b - r) / delta + 2) / 6
|
|
1361
|
+
} else {
|
|
1362
|
+
h = ((r - g) / delta + 4) / 6
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
return {
|
|
1367
|
+
h: Math.round(h * 360),
|
|
1368
|
+
s: Math.round(s * 100),
|
|
1369
|
+
v: Math.round(v * 100)
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1373
|
+
/**
|
|
1374
|
+
* Convert HSV to RGB
|
|
1375
|
+
* @param {number} h - Hue (0-360)
|
|
1376
|
+
* @param {number} s - Saturation (0-100)
|
|
1377
|
+
* @param {number} v - Value/Brightness (0-100)
|
|
1378
|
+
* @returns {{ r: number, g: number, b: number }} RGB values (0-255)
|
|
1379
|
+
*/
|
|
1380
|
+
hsvToRgb (h, s, v) {
|
|
1381
|
+
h = h / 360
|
|
1382
|
+
s = s / 100
|
|
1383
|
+
v = v / 100
|
|
1384
|
+
|
|
1385
|
+
let r, g, b
|
|
1386
|
+
|
|
1387
|
+
const i = Math.floor(h * 6)
|
|
1388
|
+
const f = h * 6 - i
|
|
1389
|
+
const p = v * (1 - s)
|
|
1390
|
+
const q = v * (1 - f * s)
|
|
1391
|
+
const t = v * (1 - (1 - f) * s)
|
|
1392
|
+
|
|
1393
|
+
switch (i % 6) {
|
|
1394
|
+
case 0: r = v; g = t; b = p; break
|
|
1395
|
+
case 1: r = q; g = v; b = p; break
|
|
1396
|
+
case 2: r = p; g = v; b = t; break
|
|
1397
|
+
case 3: r = p; g = q; b = v; break
|
|
1398
|
+
case 4: r = t; g = p; b = v; break
|
|
1399
|
+
case 5: r = v; g = p; b = q; break
|
|
1400
|
+
default: r = 0; g = 0; b = 0
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
return {
|
|
1404
|
+
r: Math.round(r * 255),
|
|
1405
|
+
g: Math.round(g * 255),
|
|
1406
|
+
b: Math.round(b * 255)
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
/**
|
|
1411
|
+
* Handle active content change
|
|
1412
|
+
* @param {string} cardId - Card ID
|
|
1413
|
+
*/
|
|
1414
|
+
async handleActiveContentChange (cardId) {
|
|
1415
|
+
// Skip if same card
|
|
1416
|
+
if (this.activeContentCardId === cardId) {
|
|
1417
|
+
return
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1420
|
+
this.activeContentCardId = cardId
|
|
1421
|
+
this.log.info(LOG_PREFIX.ACCESSORY, `[${this.device.name}] Active card changed: ${cardId}`)
|
|
1422
|
+
|
|
1423
|
+
try {
|
|
1424
|
+
// Fetch card details
|
|
1425
|
+
const content = await this.platform.yotoApi.getContent(cardId)
|
|
1426
|
+
this.activeContentInfo = content
|
|
1427
|
+
|
|
1428
|
+
// Log content information
|
|
1429
|
+
const title = content.card?.title || 'Unknown'
|
|
1430
|
+
this.log.info(LOG_PREFIX.ACCESSORY, `[${this.device.name}] Now playing: "${title}"`)
|
|
1431
|
+
|
|
1432
|
+
if (content.card?.metadata?.author) {
|
|
1433
|
+
this.log.info(LOG_PREFIX.ACCESSORY, `[${this.device.name}] Author: ${content.card.metadata.author}`)
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1436
|
+
if (content.card?.metadata?.category) {
|
|
1437
|
+
this.log.info(LOG_PREFIX.ACCESSORY, `[${this.device.name}] Category: ${content.card.metadata.category}`)
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1440
|
+
// Optionally update accessory display name with current content
|
|
1441
|
+
if (this.platform.config.updateAccessoryName && content.card?.title) {
|
|
1442
|
+
this.accessory.displayName = `${this.device.name} - ${content.card.title}`
|
|
1443
|
+
this.log.debug(LOG_PREFIX.ACCESSORY, `[${this.device.name}] Updated display name`)
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1446
|
+
// Store in context for persistence
|
|
1447
|
+
this.accessory.context.activeContentInfo = this.activeContentInfo
|
|
1448
|
+
} catch (error) {
|
|
1449
|
+
this.log.error(LOG_PREFIX.ACCESSORY, `[${this.device.name}] Failed to fetch content details:`, error)
|
|
1450
|
+
}
|
|
1451
|
+
}
|
|
1452
|
+
}
|