homebridge-yoto 0.0.39 → 0.0.40
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +50 -0
- package/config.schema.json +63 -17
- package/lib/accessory.js +128 -43
- package/lib/card-control-accessory.js +200 -0
- package/lib/card-controls.js +80 -0
- package/lib/platform.js +322 -31
- package/lib/service-config.js +63 -0
- package/lib/speaker-accessory.js +490 -0
- package/package.json +2 -2
|
@@ -0,0 +1,490 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Yoto SmartSpeaker accessory implementation for external accessories.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/** @import { PlatformAccessory, CharacteristicValue, Service, Logger } from 'homebridge' */
|
|
6
|
+
/** @import { YotoPlatform } from './platform.js' */
|
|
7
|
+
/** @import { YotoDeviceModel } from 'yoto-nodejs-client' */
|
|
8
|
+
/** @import { YotoDevice } from 'yoto-nodejs-client/lib/api-endpoints/devices.js' */
|
|
9
|
+
/** @import { YotoAccessoryContext } from './platform.js' */
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
DEFAULT_MANUFACTURER,
|
|
13
|
+
DEFAULT_MODEL,
|
|
14
|
+
LOG_PREFIX,
|
|
15
|
+
} from './constants.js'
|
|
16
|
+
import { sanitizeName } from './sanitize-name.js'
|
|
17
|
+
import { syncServiceNames } from './sync-service-names.js'
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Yoto SmartSpeaker Accessory Handler (external)
|
|
21
|
+
* Manages SmartSpeaker service and characteristics for a single Yoto player.
|
|
22
|
+
*/
|
|
23
|
+
export class YotoSpeakerAccessory {
|
|
24
|
+
/** @type {YotoPlatform} */ #platform
|
|
25
|
+
/** @type {PlatformAccessory<YotoAccessoryContext>} */ #accessory
|
|
26
|
+
/** @type {YotoDeviceModel} */ #deviceModel
|
|
27
|
+
/** @type {Logger} */ #log
|
|
28
|
+
/** @type {YotoDevice} */ #device
|
|
29
|
+
/** @type {Service | undefined} */ speakerService
|
|
30
|
+
/** @type {number} */ #lastNonZeroVolume = 50
|
|
31
|
+
/** @type {Set<Service>} */ #currentServices = new Set()
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* @param {Object} params
|
|
35
|
+
* @param {YotoPlatform} params.platform - Platform instance
|
|
36
|
+
* @param {PlatformAccessory<YotoAccessoryContext>} params.accessory - Platform accessory
|
|
37
|
+
* @param {YotoDeviceModel} params.deviceModel - Yoto device model with live state
|
|
38
|
+
*/
|
|
39
|
+
constructor ({ platform, accessory, deviceModel }) {
|
|
40
|
+
this.#platform = platform
|
|
41
|
+
this.#accessory = accessory
|
|
42
|
+
this.#deviceModel = deviceModel
|
|
43
|
+
this.#log = platform.log
|
|
44
|
+
|
|
45
|
+
this.#device = accessory.context.device
|
|
46
|
+
this.#currentServices = new Set()
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Setup accessory - create services and setup event listeners
|
|
51
|
+
* @returns {Promise<void>}
|
|
52
|
+
*/
|
|
53
|
+
async setup () {
|
|
54
|
+
this.#log.debug(LOG_PREFIX.ACCESSORY, `Setting up speaker for ${this.#device.name}`)
|
|
55
|
+
|
|
56
|
+
this.#currentServices.clear()
|
|
57
|
+
|
|
58
|
+
this.setupAccessoryInformation()
|
|
59
|
+
this.setupSmartSpeakerService()
|
|
60
|
+
|
|
61
|
+
for (const service of this.#accessory.services) {
|
|
62
|
+
if (service.UUID !== this.#platform.Service.AccessoryInformation.UUID &&
|
|
63
|
+
!this.#currentServices.has(service)) {
|
|
64
|
+
this.#log.debug(LOG_PREFIX.ACCESSORY, `Removing stale speaker service: ${service.displayName || service.UUID}`)
|
|
65
|
+
this.#accessory.removeService(service)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
this.setupEventListeners()
|
|
70
|
+
|
|
71
|
+
this.#log.debug(LOG_PREFIX.ACCESSORY, `✓ ${this.#device.name} speaker ready`)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Setup AccessoryInformation service
|
|
76
|
+
*/
|
|
77
|
+
setupAccessoryInformation () {
|
|
78
|
+
const { Service, Characteristic } = this.#platform
|
|
79
|
+
const service = this.#accessory.getService(Service.AccessoryInformation) ||
|
|
80
|
+
this.#accessory.addService(Service.AccessoryInformation)
|
|
81
|
+
|
|
82
|
+
const hardwareRevision = [
|
|
83
|
+
this.#device.generation,
|
|
84
|
+
this.#device.formFactor,
|
|
85
|
+
].filter(Boolean).join(' ') || 'Unknown'
|
|
86
|
+
|
|
87
|
+
const model = this.#device.deviceFamily || this.#device.deviceType || DEFAULT_MODEL
|
|
88
|
+
|
|
89
|
+
service
|
|
90
|
+
.setCharacteristic(Characteristic.Manufacturer, DEFAULT_MANUFACTURER)
|
|
91
|
+
.setCharacteristic(Characteristic.Model, model)
|
|
92
|
+
.setCharacteristic(Characteristic.SerialNumber, this.#device.deviceId)
|
|
93
|
+
.setCharacteristic(Characteristic.HardwareRevision, hardwareRevision)
|
|
94
|
+
|
|
95
|
+
if (this.#deviceModel.status.firmwareVersion) {
|
|
96
|
+
service.setCharacteristic(
|
|
97
|
+
Characteristic.FirmwareRevision,
|
|
98
|
+
this.#deviceModel.status.firmwareVersion
|
|
99
|
+
)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
this.#currentServices.add(service)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Setup SmartSpeaker service (PRIMARY)
|
|
107
|
+
*/
|
|
108
|
+
setupSmartSpeakerService () {
|
|
109
|
+
const { Service, Characteristic } = this.#platform
|
|
110
|
+
const serviceName = sanitizeName(this.#accessory.displayName)
|
|
111
|
+
|
|
112
|
+
const service = this.#accessory.getService(Service.SmartSpeaker) ||
|
|
113
|
+
this.#accessory.addService(Service.SmartSpeaker, serviceName)
|
|
114
|
+
|
|
115
|
+
service.setPrimaryService(true)
|
|
116
|
+
|
|
117
|
+
syncServiceNames({ Characteristic, service, name: serviceName })
|
|
118
|
+
|
|
119
|
+
service
|
|
120
|
+
.getCharacteristic(Characteristic.CurrentMediaState)
|
|
121
|
+
.onGet(this.getCurrentMediaState.bind(this))
|
|
122
|
+
|
|
123
|
+
service
|
|
124
|
+
.getCharacteristic(Characteristic.TargetMediaState)
|
|
125
|
+
.onGet(this.getTargetMediaState.bind(this))
|
|
126
|
+
.onSet(this.setTargetMediaState.bind(this))
|
|
127
|
+
|
|
128
|
+
service
|
|
129
|
+
.getCharacteristic(Characteristic.Volume)
|
|
130
|
+
.setProps({
|
|
131
|
+
minValue: 0,
|
|
132
|
+
maxValue: 100,
|
|
133
|
+
minStep: 1,
|
|
134
|
+
})
|
|
135
|
+
.onGet(this.getVolume.bind(this))
|
|
136
|
+
.onSet(this.setVolume.bind(this))
|
|
137
|
+
|
|
138
|
+
service
|
|
139
|
+
.getCharacteristic(Characteristic.Mute)
|
|
140
|
+
.onGet(this.getMute.bind(this))
|
|
141
|
+
.onSet(this.setMute.bind(this))
|
|
142
|
+
|
|
143
|
+
const statusActiveUuid = Characteristic.StatusActive.UUID
|
|
144
|
+
const hasStatusActive = service.characteristics.some(c => c.UUID === statusActiveUuid)
|
|
145
|
+
const hasStatusActiveOptional = service.optionalCharacteristics.some(c => c.UUID === statusActiveUuid)
|
|
146
|
+
if (!hasStatusActive && !hasStatusActiveOptional) {
|
|
147
|
+
service.addOptionalCharacteristic(Characteristic.StatusActive)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
service
|
|
151
|
+
.getCharacteristic(Characteristic.StatusActive)
|
|
152
|
+
.onGet(this.getStatusActive.bind(this))
|
|
153
|
+
|
|
154
|
+
this.speakerService = service
|
|
155
|
+
this.#currentServices.add(service)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Setup event listeners for device model updates
|
|
160
|
+
*/
|
|
161
|
+
setupEventListeners () {
|
|
162
|
+
this.#deviceModel.on('statusUpdate', (status, _source, changedFields) => {
|
|
163
|
+
for (const field of changedFields) {
|
|
164
|
+
switch (field) {
|
|
165
|
+
case 'volume':
|
|
166
|
+
this.updateVolumeCharacteristic(status.volume)
|
|
167
|
+
break
|
|
168
|
+
|
|
169
|
+
case 'isOnline':
|
|
170
|
+
this.updateOnlineStatusCharacteristic(status.isOnline)
|
|
171
|
+
break
|
|
172
|
+
|
|
173
|
+
case 'firmwareVersion':
|
|
174
|
+
this.updateFirmwareVersionCharacteristic(status.firmwareVersion)
|
|
175
|
+
break
|
|
176
|
+
|
|
177
|
+
// Available but not mapped to SmartSpeaker characteristics
|
|
178
|
+
case 'batteryLevelPercentage':
|
|
179
|
+
case 'isCharging':
|
|
180
|
+
case 'maxVolume':
|
|
181
|
+
case 'temperatureCelsius':
|
|
182
|
+
case 'nightlightMode':
|
|
183
|
+
case 'dayMode':
|
|
184
|
+
case 'cardInsertionState':
|
|
185
|
+
case 'activeCardId':
|
|
186
|
+
case 'powerSource':
|
|
187
|
+
case 'wifiStrength':
|
|
188
|
+
case 'freeDiskSpaceBytes':
|
|
189
|
+
case 'totalDiskSpaceBytes':
|
|
190
|
+
case 'isAudioDeviceConnected':
|
|
191
|
+
case 'isBluetoothAudioConnected':
|
|
192
|
+
case 'ambientLightSensorReading':
|
|
193
|
+
case 'displayBrightness':
|
|
194
|
+
case 'timeFormat':
|
|
195
|
+
case 'uptime':
|
|
196
|
+
case 'updatedAt':
|
|
197
|
+
case 'source':
|
|
198
|
+
break
|
|
199
|
+
|
|
200
|
+
default: {
|
|
201
|
+
/** @type {never} */
|
|
202
|
+
const _exhaustive = field
|
|
203
|
+
this.#log.debug('Unhandled speaker status field:', _exhaustive)
|
|
204
|
+
break
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
this.#deviceModel.on('playbackUpdate', (playback, changedFields) => {
|
|
211
|
+
for (const field of changedFields) {
|
|
212
|
+
switch (field) {
|
|
213
|
+
case 'playbackStatus':
|
|
214
|
+
this.updateSmartSpeakerMediaStateCharacteristic(playback.playbackStatus)
|
|
215
|
+
break
|
|
216
|
+
|
|
217
|
+
case 'sleepTimerActive':
|
|
218
|
+
case 'position':
|
|
219
|
+
case 'trackLength':
|
|
220
|
+
case 'cardId':
|
|
221
|
+
case 'source':
|
|
222
|
+
case 'trackTitle':
|
|
223
|
+
case 'trackKey':
|
|
224
|
+
case 'chapterTitle':
|
|
225
|
+
case 'chapterKey':
|
|
226
|
+
case 'sleepTimerSeconds':
|
|
227
|
+
case 'streaming':
|
|
228
|
+
case 'updatedAt':
|
|
229
|
+
break
|
|
230
|
+
|
|
231
|
+
default: {
|
|
232
|
+
/** @type {never} */
|
|
233
|
+
const _exhaustive = field
|
|
234
|
+
this.#log.debug('Unhandled speaker playback field:', _exhaustive)
|
|
235
|
+
break
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
this.#deviceModel.on('online', ({ reason: _reason }) => {
|
|
242
|
+
this.updateOnlineStatusCharacteristic(true)
|
|
243
|
+
})
|
|
244
|
+
|
|
245
|
+
this.#deviceModel.on('offline', ({ reason: _reason }) => {
|
|
246
|
+
this.updateOnlineStatusCharacteristic(false)
|
|
247
|
+
})
|
|
248
|
+
|
|
249
|
+
this.#deviceModel.on('error', (error) => {
|
|
250
|
+
this.#log.error(`[${this.#device.name}] Speaker device error:`, error.message)
|
|
251
|
+
})
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* @param { "playing" | "paused" | "stopped" | "loading" | null} playbackStatus
|
|
256
|
+
* @returns {{ current: CharacteristicValue, target: CharacteristicValue }}
|
|
257
|
+
*/
|
|
258
|
+
getMediaStateValues (playbackStatus) {
|
|
259
|
+
const { Characteristic } = this.#platform
|
|
260
|
+
|
|
261
|
+
if (playbackStatus === 'playing') {
|
|
262
|
+
return {
|
|
263
|
+
current: Characteristic.CurrentMediaState.PLAY,
|
|
264
|
+
target: Characteristic.TargetMediaState.PLAY,
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (playbackStatus === 'paused') {
|
|
269
|
+
return {
|
|
270
|
+
current: Characteristic.CurrentMediaState.PAUSE,
|
|
271
|
+
target: Characteristic.TargetMediaState.PAUSE,
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return {
|
|
276
|
+
current: Characteristic.CurrentMediaState.STOP,
|
|
277
|
+
target: Characteristic.TargetMediaState.STOP,
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Get current media state from live playback state
|
|
283
|
+
* @returns {Promise<CharacteristicValue>}
|
|
284
|
+
*/
|
|
285
|
+
async getCurrentMediaState () {
|
|
286
|
+
const playbackStatus = this.#deviceModel.playback.playbackStatus ?? null
|
|
287
|
+
return this.getMediaStateValues(playbackStatus).current
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Get target media state (follows current state)
|
|
292
|
+
* @returns {Promise<CharacteristicValue>}
|
|
293
|
+
*/
|
|
294
|
+
async getTargetMediaState () {
|
|
295
|
+
const playbackStatus = this.#deviceModel.playback.playbackStatus ?? null
|
|
296
|
+
return this.getMediaStateValues(playbackStatus).target
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Set target media state (play/pause/stop)
|
|
301
|
+
* @param {CharacteristicValue} value - Target state
|
|
302
|
+
* @returns {Promise<void>}
|
|
303
|
+
*/
|
|
304
|
+
async setTargetMediaState (value) {
|
|
305
|
+
const { Characteristic } = this.#platform
|
|
306
|
+
const targetValue = typeof value === 'number' ? value : Number(value)
|
|
307
|
+
this.#log.debug(LOG_PREFIX.ACCESSORY, `[${this.#device.name}] Set target media state:`, value)
|
|
308
|
+
|
|
309
|
+
try {
|
|
310
|
+
if (targetValue === Characteristic.TargetMediaState.PLAY) {
|
|
311
|
+
await this.#deviceModel.resumeCard()
|
|
312
|
+
return
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
if (targetValue === Characteristic.TargetMediaState.PAUSE) {
|
|
316
|
+
await this.#deviceModel.pauseCard()
|
|
317
|
+
return
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
if (targetValue === Characteristic.TargetMediaState.STOP) {
|
|
321
|
+
await this.#deviceModel.pauseCard()
|
|
322
|
+
}
|
|
323
|
+
} catch (error) {
|
|
324
|
+
this.#log.error(LOG_PREFIX.ACCESSORY, `[${this.#device.name}] Failed to set media state:`, error)
|
|
325
|
+
throw new this.#platform.api.hap.HapStatusError(
|
|
326
|
+
this.#platform.api.hap.HAPStatus.SERVICE_COMMUNICATION_FAILURE
|
|
327
|
+
)
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Get volume level as percentage (mapped from 0-16 steps)
|
|
333
|
+
* @returns {Promise<CharacteristicValue>}
|
|
334
|
+
*/
|
|
335
|
+
async getVolume () {
|
|
336
|
+
const volumeSteps = this.#deviceModel.status.volume
|
|
337
|
+
const normalizedSteps = Number.isFinite(volumeSteps) ? volumeSteps : 0
|
|
338
|
+
const clampedSteps = Math.max(0, Math.min(normalizedSteps, 16))
|
|
339
|
+
return Math.round((clampedSteps / 16) * 100)
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Set volume level as percentage (mapped to 0-16 steps)
|
|
344
|
+
* @param {CharacteristicValue} value - Volume level percent
|
|
345
|
+
* @returns {Promise<void>}
|
|
346
|
+
*/
|
|
347
|
+
async setVolume (value) {
|
|
348
|
+
const deviceModel = this.#deviceModel
|
|
349
|
+
this.#log.debug(LOG_PREFIX.ACCESSORY, `[${this.#device.name}] Set speaker volume:`, value)
|
|
350
|
+
|
|
351
|
+
const requestedPercent = typeof value === 'number' ? value : Number(value)
|
|
352
|
+
if (!Number.isFinite(requestedPercent)) {
|
|
353
|
+
throw new this.#platform.api.hap.HapStatusError(
|
|
354
|
+
this.#platform.api.hap.HAPStatus.INVALID_VALUE_IN_REQUEST
|
|
355
|
+
)
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
const normalizedPercent = Math.max(0, Math.min(Math.round(requestedPercent), 100))
|
|
359
|
+
const requestedSteps = Math.round((normalizedPercent / 100) * 16)
|
|
360
|
+
const steps = Math.max(0, Math.min(Math.round(requestedSteps), 16))
|
|
361
|
+
|
|
362
|
+
if (steps > 0) {
|
|
363
|
+
this.#lastNonZeroVolume = Math.round((steps / 16) * 100)
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
try {
|
|
367
|
+
await deviceModel.setVolume(steps)
|
|
368
|
+
this.updateVolumeCharacteristic(steps)
|
|
369
|
+
} catch (error) {
|
|
370
|
+
this.#log.error(LOG_PREFIX.ACCESSORY, `[${this.#device.name}] Failed to set speaker volume:`, error)
|
|
371
|
+
throw new this.#platform.api.hap.HapStatusError(
|
|
372
|
+
this.#platform.api.hap.HAPStatus.SERVICE_COMMUNICATION_FAILURE
|
|
373
|
+
)
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* Get mute state (derived from volume === 0)
|
|
379
|
+
* @returns {Promise<CharacteristicValue>}
|
|
380
|
+
*/
|
|
381
|
+
async getMute () {
|
|
382
|
+
return this.#deviceModel.status.volume === 0
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Set mute state
|
|
387
|
+
* @param {CharacteristicValue} value - Mute state
|
|
388
|
+
* @returns {Promise<void>}
|
|
389
|
+
*/
|
|
390
|
+
async setMute (value) {
|
|
391
|
+
const isMuted = Boolean(value)
|
|
392
|
+
this.#log.debug(LOG_PREFIX.ACCESSORY, `[${this.#device.name}] Set speaker mute:`, isMuted)
|
|
393
|
+
|
|
394
|
+
if (isMuted) {
|
|
395
|
+
await this.setVolume(0)
|
|
396
|
+
return
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
await this.setVolume(this.#lastNonZeroVolume)
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Get status active (online/offline)
|
|
404
|
+
* @returns {Promise<CharacteristicValue>}
|
|
405
|
+
*/
|
|
406
|
+
async getStatusActive () {
|
|
407
|
+
return this.#deviceModel.status.isOnline
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* Update SmartSpeaker media state characteristics
|
|
412
|
+
* @param { "playing" | "paused" | "stopped" | "loading" | null} playbackStatus - Playback status
|
|
413
|
+
*/
|
|
414
|
+
updateSmartSpeakerMediaStateCharacteristic (playbackStatus) {
|
|
415
|
+
if (!this.speakerService) return
|
|
416
|
+
|
|
417
|
+
const { Characteristic } = this.#platform
|
|
418
|
+
const { current, target } = this.getMediaStateValues(playbackStatus)
|
|
419
|
+
|
|
420
|
+
this.speakerService
|
|
421
|
+
.getCharacteristic(Characteristic.CurrentMediaState)
|
|
422
|
+
.updateValue(current)
|
|
423
|
+
|
|
424
|
+
this.speakerService
|
|
425
|
+
.getCharacteristic(Characteristic.TargetMediaState)
|
|
426
|
+
.updateValue(target)
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* Update volume and mute characteristics
|
|
431
|
+
* @param {number} volumeSteps - Volume level (0-16)
|
|
432
|
+
*/
|
|
433
|
+
updateVolumeCharacteristic (volumeSteps) {
|
|
434
|
+
if (!this.speakerService) return
|
|
435
|
+
|
|
436
|
+
const normalizedVolume = Number.isFinite(volumeSteps) ? volumeSteps : 0
|
|
437
|
+
const clampedVolume = Math.max(0, Math.min(normalizedVolume, 16))
|
|
438
|
+
const percent = Math.round((clampedVolume / 16) * 100)
|
|
439
|
+
const isMuted = clampedVolume === 0
|
|
440
|
+
|
|
441
|
+
this.speakerService
|
|
442
|
+
.getCharacteristic(this.#platform.Characteristic.Volume)
|
|
443
|
+
.updateValue(percent)
|
|
444
|
+
|
|
445
|
+
this.speakerService
|
|
446
|
+
.getCharacteristic(this.#platform.Characteristic.Mute)
|
|
447
|
+
.updateValue(isMuted)
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* Update online status characteristic
|
|
452
|
+
* @param {boolean} isOnline - Online status
|
|
453
|
+
*/
|
|
454
|
+
updateOnlineStatusCharacteristic (isOnline) {
|
|
455
|
+
if (!this.speakerService) return
|
|
456
|
+
|
|
457
|
+
this.speakerService
|
|
458
|
+
.getCharacteristic(this.#platform.Characteristic.StatusActive)
|
|
459
|
+
.updateValue(isOnline)
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* Update firmware version characteristic
|
|
464
|
+
* @param {string} firmwareVersion - Firmware version
|
|
465
|
+
*/
|
|
466
|
+
updateFirmwareVersionCharacteristic (firmwareVersion) {
|
|
467
|
+
const { Service, Characteristic } = this.#platform
|
|
468
|
+
const infoService = this.#accessory.getService(Service.AccessoryInformation)
|
|
469
|
+
if (!infoService) return
|
|
470
|
+
|
|
471
|
+
infoService.setCharacteristic(
|
|
472
|
+
Characteristic.FirmwareRevision,
|
|
473
|
+
firmwareVersion
|
|
474
|
+
)
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* Stop accessory - cleanup event listeners
|
|
479
|
+
* @returns {Promise<void>}
|
|
480
|
+
*/
|
|
481
|
+
async stop () {
|
|
482
|
+
this.#log.debug(LOG_PREFIX.ACCESSORY, `Stopping speaker for ${this.#device.name}`)
|
|
483
|
+
|
|
484
|
+
this.#deviceModel.removeAllListeners('statusUpdate')
|
|
485
|
+
this.#deviceModel.removeAllListeners('playbackUpdate')
|
|
486
|
+
this.#deviceModel.removeAllListeners('online')
|
|
487
|
+
this.#deviceModel.removeAllListeners('offline')
|
|
488
|
+
this.#deviceModel.removeAllListeners('error')
|
|
489
|
+
}
|
|
490
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "homebridge-yoto",
|
|
3
3
|
"description": "Control your Yoto players through Apple HomeKit with real-time MQTT updates",
|
|
4
|
-
"version": "0.0.
|
|
4
|
+
"version": "0.0.40",
|
|
5
5
|
"author": "Bret Comnes <bcomnes@gmail.com> (https://bret.io)",
|
|
6
6
|
"bugs": {
|
|
7
7
|
"url": "https://github.com/bcomnes/homebridge-yoto/issues"
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"dependencies": {
|
|
10
10
|
"@homebridge/plugin-ui-utils": "^2.1.2",
|
|
11
11
|
"color-convert": "3.1.3",
|
|
12
|
-
"yoto-nodejs-client": "^0.0.
|
|
12
|
+
"yoto-nodejs-client": "^0.0.8"
|
|
13
13
|
},
|
|
14
14
|
"devDependencies": {
|
|
15
15
|
"@types/node": "^25.0.0",
|