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