homebridge-yoto 0.0.40 → 0.0.42
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/config.schema.json +7 -0
- package/lib/accessory.js +222 -65
- package/lib/card-control-accessory.js +12 -1
- package/lib/platform.js +68 -50
- package/lib/speaker-accessory.js +36 -6
- package/package.json +1 -1
package/config.schema.json
CHANGED
|
@@ -61,6 +61,12 @@
|
|
|
61
61
|
],
|
|
62
62
|
"description": "Choose how playback and volume controls are exposed. External Smart Speaker requires additional pairing steps and appears as a separate accessory."
|
|
63
63
|
},
|
|
64
|
+
"battery": {
|
|
65
|
+
"title": "Battery",
|
|
66
|
+
"type": "boolean",
|
|
67
|
+
"default": true,
|
|
68
|
+
"description": "Expose battery status service."
|
|
69
|
+
},
|
|
64
70
|
"temperature": {
|
|
65
71
|
"title": "Temperature Sensor",
|
|
66
72
|
"type": "boolean",
|
|
@@ -167,6 +173,7 @@
|
|
|
167
173
|
"helpvalue": "<p><strong>Playback Controls:</strong> External Smart Speaker requires additional pairing steps in the Home app and will appear as a separate accessory.</p>"
|
|
168
174
|
},
|
|
169
175
|
"services.playbackAccessory",
|
|
176
|
+
"services.battery",
|
|
170
177
|
"services.temperature",
|
|
171
178
|
"services.nightlight",
|
|
172
179
|
"services.cardSlot",
|
package/lib/accessory.js
CHANGED
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
* @typedef {Object} YotoServiceToggles
|
|
24
24
|
* @property {boolean} playback
|
|
25
25
|
* @property {boolean} volume
|
|
26
|
+
* @property {boolean} battery
|
|
26
27
|
* @property {boolean} temperature
|
|
27
28
|
* @property {boolean} nightlight
|
|
28
29
|
* @property {boolean} cardSlot
|
|
@@ -135,6 +136,7 @@ export class YotoPlayerAccessory {
|
|
|
135
136
|
return {
|
|
136
137
|
playback: playbackConfig.playbackEnabled,
|
|
137
138
|
volume: playbackConfig.volumeEnabled,
|
|
139
|
+
battery: getBooleanSetting(serviceConfig['battery'], getServiceDefault('battery')),
|
|
138
140
|
temperature: getBooleanSetting(serviceConfig['temperature'], getServiceDefault('temperature')),
|
|
139
141
|
nightlight: getBooleanSetting(serviceConfig['nightlight'], getServiceDefault('nightlight')),
|
|
140
142
|
cardSlot: getBooleanSetting(serviceConfig['cardSlot'], getServiceDefault('cardSlot')),
|
|
@@ -177,7 +179,9 @@ export class YotoPlayerAccessory {
|
|
|
177
179
|
this.setupVolumeService()
|
|
178
180
|
}
|
|
179
181
|
|
|
180
|
-
|
|
182
|
+
if (serviceToggles.battery) {
|
|
183
|
+
this.setupBatteryService()
|
|
184
|
+
}
|
|
181
185
|
|
|
182
186
|
// Setup optional services based on device capabilities
|
|
183
187
|
if (serviceToggles.temperature && this.#deviceModel.capabilities.hasTemperatureSensor) {
|
|
@@ -239,6 +243,11 @@ export class YotoPlayerAccessory {
|
|
|
239
243
|
const { Service, Characteristic } = this.#platform
|
|
240
244
|
const service = this.#accessory.getService(Service.AccessoryInformation) ||
|
|
241
245
|
this.#accessory.addService(Service.AccessoryInformation)
|
|
246
|
+
const displayName = sanitizeName(this.#accessory.displayName)
|
|
247
|
+
const nameCharacteristic = service.getCharacteristic(Characteristic.Name)
|
|
248
|
+
const configuredCharacteristic = service.getCharacteristic(Characteristic.ConfiguredName)
|
|
249
|
+
const previousName = nameCharacteristic.value
|
|
250
|
+
const configuredName = configuredCharacteristic.value
|
|
242
251
|
|
|
243
252
|
// Build hardware revision from generation and form factor
|
|
244
253
|
const hardwareRevision = [
|
|
@@ -251,11 +260,16 @@ export class YotoPlayerAccessory {
|
|
|
251
260
|
|
|
252
261
|
// Set standard characteristics
|
|
253
262
|
service
|
|
263
|
+
.setCharacteristic(Characteristic.Name, displayName)
|
|
254
264
|
.setCharacteristic(Characteristic.Manufacturer, DEFAULT_MANUFACTURER)
|
|
255
265
|
.setCharacteristic(Characteristic.Model, model)
|
|
256
266
|
.setCharacteristic(Characteristic.SerialNumber, this.#device.deviceId)
|
|
257
267
|
.setCharacteristic(Characteristic.HardwareRevision, hardwareRevision)
|
|
258
268
|
|
|
269
|
+
if (typeof configuredName !== 'string' || configuredName === previousName) {
|
|
270
|
+
service.setCharacteristic(Characteristic.ConfiguredName, displayName)
|
|
271
|
+
}
|
|
272
|
+
|
|
259
273
|
// Set firmware version from live status if available
|
|
260
274
|
if (this.#deviceModel.status.firmwareVersion) {
|
|
261
275
|
service.setCharacteristic(
|
|
@@ -917,7 +931,8 @@ export class YotoPlayerAccessory {
|
|
|
917
931
|
})
|
|
918
932
|
|
|
919
933
|
this.#deviceModel.on('error', (error) => {
|
|
920
|
-
|
|
934
|
+
const details = error instanceof Error ? (error.stack || error.message) : String(error)
|
|
935
|
+
this.#log.error(`[${this.#device.name}] Device error:`, details)
|
|
921
936
|
})
|
|
922
937
|
}
|
|
923
938
|
|
|
@@ -928,7 +943,9 @@ export class YotoPlayerAccessory {
|
|
|
928
943
|
* @returns {Promise<CharacteristicValue>}
|
|
929
944
|
*/
|
|
930
945
|
async getPlaybackOn () {
|
|
931
|
-
|
|
946
|
+
const isOn = this.#deviceModel.playback.playbackStatus === 'playing'
|
|
947
|
+
this.#log.debug(LOG_PREFIX.ACCESSORY, `[${this.#device.name}] Get playback switch -> ${isOn}`)
|
|
948
|
+
return isOn
|
|
932
949
|
}
|
|
933
950
|
|
|
934
951
|
/**
|
|
@@ -1028,7 +1045,9 @@ export class YotoPlayerAccessory {
|
|
|
1028
1045
|
* @returns {Promise<CharacteristicValue>}
|
|
1029
1046
|
*/
|
|
1030
1047
|
async getVolumeOn () {
|
|
1031
|
-
|
|
1048
|
+
const isOn = this.#deviceModel.status.volume > 0
|
|
1049
|
+
this.#log.debug(LOG_PREFIX.ACCESSORY, `[${this.#device.name}] Get volume on -> ${isOn}`)
|
|
1050
|
+
return isOn
|
|
1032
1051
|
}
|
|
1033
1052
|
|
|
1034
1053
|
/**
|
|
@@ -1059,7 +1078,9 @@ export class YotoPlayerAccessory {
|
|
|
1059
1078
|
* @returns {Promise<CharacteristicValue>}
|
|
1060
1079
|
*/
|
|
1061
1080
|
async getStatusActive () {
|
|
1062
|
-
|
|
1081
|
+
const isOnline = this.#deviceModel.status.isOnline
|
|
1082
|
+
this.#log.debug(LOG_PREFIX.ACCESSORY, `[${this.#device.name}] Get status active -> ${isOnline}`)
|
|
1083
|
+
return isOnline
|
|
1063
1084
|
}
|
|
1064
1085
|
|
|
1065
1086
|
/**
|
|
@@ -1068,7 +1089,9 @@ export class YotoPlayerAccessory {
|
|
|
1068
1089
|
*/
|
|
1069
1090
|
async getOnlineStatus () {
|
|
1070
1091
|
const { Characteristic } = this.#platform
|
|
1071
|
-
|
|
1092
|
+
const isOnline = this.#deviceModel.status.isOnline
|
|
1093
|
+
this.#log.debug(LOG_PREFIX.ACCESSORY, `[${this.#device.name}] Get online status -> ${isOnline}`)
|
|
1094
|
+
return isOnline
|
|
1072
1095
|
? Characteristic.ContactSensorState.CONTACT_NOT_DETECTED
|
|
1073
1096
|
: Characteristic.ContactSensorState.CONTACT_DETECTED
|
|
1074
1097
|
}
|
|
@@ -1081,7 +1104,9 @@ export class YotoPlayerAccessory {
|
|
|
1081
1104
|
*/
|
|
1082
1105
|
async getBatteryLevel () {
|
|
1083
1106
|
const battery = this.#deviceModel.status.batteryLevelPercentage
|
|
1084
|
-
|
|
1107
|
+
const level = Number.isFinite(battery) ? battery : 100
|
|
1108
|
+
this.#log.debug(LOG_PREFIX.ACCESSORY, `[${this.#device.name}] Get battery level -> ${level}`)
|
|
1109
|
+
return level
|
|
1085
1110
|
}
|
|
1086
1111
|
|
|
1087
1112
|
/**
|
|
@@ -1090,6 +1115,7 @@ export class YotoPlayerAccessory {
|
|
|
1090
1115
|
*/
|
|
1091
1116
|
async getChargingState () {
|
|
1092
1117
|
const isCharging = this.#deviceModel.status.isCharging
|
|
1118
|
+
this.#log.debug(LOG_PREFIX.ACCESSORY, `[${this.#device.name}] Get charging state -> ${isCharging}`)
|
|
1093
1119
|
return isCharging
|
|
1094
1120
|
? this.#platform.Characteristic.ChargingState.CHARGING
|
|
1095
1121
|
: this.#platform.Characteristic.ChargingState.NOT_CHARGING
|
|
@@ -1102,6 +1128,7 @@ export class YotoPlayerAccessory {
|
|
|
1102
1128
|
async getStatusLowBattery () {
|
|
1103
1129
|
const battery = this.#deviceModel.status.batteryLevelPercentage
|
|
1104
1130
|
const batteryLevel = Number.isFinite(battery) ? battery : 100
|
|
1131
|
+
this.#log.debug(LOG_PREFIX.ACCESSORY, `[${this.#device.name}] Get low battery -> ${batteryLevel}`)
|
|
1105
1132
|
return batteryLevel <= LOW_BATTERY_THRESHOLD
|
|
1106
1133
|
? this.#platform.Characteristic.StatusLowBattery.BATTERY_LEVEL_LOW
|
|
1107
1134
|
: this.#platform.Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL
|
|
@@ -1118,9 +1145,11 @@ export class YotoPlayerAccessory {
|
|
|
1118
1145
|
|
|
1119
1146
|
// Return a default value if temperature is not available
|
|
1120
1147
|
if (temp === null || temp === 'notSupported') {
|
|
1148
|
+
this.#log.debug(LOG_PREFIX.ACCESSORY, `[${this.#device.name}] Get temperature -> unavailable`)
|
|
1121
1149
|
return 0
|
|
1122
1150
|
}
|
|
1123
1151
|
|
|
1152
|
+
this.#log.debug(LOG_PREFIX.ACCESSORY, `[${this.#device.name}] Get temperature -> ${temp}`)
|
|
1124
1153
|
return Number(temp)
|
|
1125
1154
|
}
|
|
1126
1155
|
|
|
@@ -1130,13 +1159,18 @@ export class YotoPlayerAccessory {
|
|
|
1130
1159
|
*/
|
|
1131
1160
|
async getTemperatureSensorFault () {
|
|
1132
1161
|
// Report fault if device is offline or temperature is not available
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1162
|
+
const isOffline = !this.#deviceModel.status.isOnline
|
|
1163
|
+
const temp = this.#deviceModel.status.temperatureCelsius
|
|
1164
|
+
const isUnavailable = temp === null || temp === 'notSupported'
|
|
1165
|
+
const isFault = isOffline || isUnavailable
|
|
1166
|
+
const fault = isFault
|
|
1167
|
+
? this.#platform.Characteristic.StatusFault.GENERAL_FAULT
|
|
1168
|
+
: this.#platform.Characteristic.StatusFault.NO_FAULT
|
|
1169
|
+
this.#log.debug(
|
|
1170
|
+
LOG_PREFIX.ACCESSORY,
|
|
1171
|
+
`[${this.#device.name}] Get temperature sensor fault -> ${isFault} (online=${!isOffline} temp=${temp})`
|
|
1172
|
+
)
|
|
1173
|
+
return fault
|
|
1140
1174
|
}
|
|
1141
1175
|
|
|
1142
1176
|
// ==================== Nightlight Characteristic Handlers ====================
|
|
@@ -1178,7 +1212,9 @@ export class YotoPlayerAccessory {
|
|
|
1178
1212
|
*/
|
|
1179
1213
|
async getDayNightlightOn () {
|
|
1180
1214
|
const color = this.#deviceModel.config.ambientColour
|
|
1181
|
-
|
|
1215
|
+
const isOn = !this.isColorOff(color)
|
|
1216
|
+
this.#log.debug(LOG_PREFIX.ACCESSORY, `[${this.#device.name}] Get day nightlight on -> ${isOn} (${color})`)
|
|
1217
|
+
return isOn
|
|
1182
1218
|
}
|
|
1183
1219
|
|
|
1184
1220
|
/**
|
|
@@ -1186,19 +1222,26 @@ export class YotoPlayerAccessory {
|
|
|
1186
1222
|
* @param {CharacteristicValue} value
|
|
1187
1223
|
*/
|
|
1188
1224
|
async setDayNightlightOn (value) {
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
this
|
|
1225
|
+
try {
|
|
1226
|
+
if (value) {
|
|
1227
|
+
// Turn ON - restore previous color or default to white
|
|
1228
|
+
const colorToSet = this.#lastDayColor || '0xffffff'
|
|
1229
|
+
this.#log.debug(LOG_PREFIX.ACCESSORY, `Turning day nightlight ON with color: ${colorToSet}`)
|
|
1230
|
+
await this.#deviceModel.updateConfig({ ambientColour: colorToSet })
|
|
1231
|
+
} else {
|
|
1232
|
+
// Turn OFF - save current color and set to black
|
|
1233
|
+
const currentColor = this.#deviceModel.config.ambientColour
|
|
1234
|
+
if (!this.isColorOff(currentColor)) {
|
|
1235
|
+
this.#lastDayColor = currentColor
|
|
1236
|
+
}
|
|
1237
|
+
this.#log.debug(LOG_PREFIX.ACCESSORY, 'Turning day nightlight OFF')
|
|
1238
|
+
await this.#deviceModel.updateConfig({ ambientColour: '0x000000' })
|
|
1199
1239
|
}
|
|
1200
|
-
|
|
1201
|
-
|
|
1240
|
+
} catch (error) {
|
|
1241
|
+
this.#log.error(LOG_PREFIX.ACCESSORY, `[${this.#device.name}] Failed to set day nightlight:`, error)
|
|
1242
|
+
throw new this.#platform.api.hap.HapStatusError(
|
|
1243
|
+
this.#platform.api.hap.HAPStatus.SERVICE_COMMUNICATION_FAILURE
|
|
1244
|
+
)
|
|
1202
1245
|
}
|
|
1203
1246
|
}
|
|
1204
1247
|
|
|
@@ -1208,10 +1251,16 @@ export class YotoPlayerAccessory {
|
|
|
1208
1251
|
*/
|
|
1209
1252
|
async getDayNightlightBrightness () {
|
|
1210
1253
|
const config = this.#deviceModel.config
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1254
|
+
const isAuto = config.dayDisplayBrightnessAuto
|
|
1255
|
+
const raw = config.dayDisplayBrightness
|
|
1256
|
+
const brightness = isAuto || raw === null
|
|
1257
|
+
? 100
|
|
1258
|
+
: Math.max(0, Math.min(Math.round(raw), 100))
|
|
1259
|
+
this.#log.debug(
|
|
1260
|
+
LOG_PREFIX.ACCESSORY,
|
|
1261
|
+
`[${this.#device.name}] Get day nightlight brightness -> ${brightness} (raw=${raw} auto=${isAuto})`
|
|
1262
|
+
)
|
|
1263
|
+
return brightness
|
|
1215
1264
|
}
|
|
1216
1265
|
|
|
1217
1266
|
/**
|
|
@@ -1228,10 +1277,17 @@ export class YotoPlayerAccessory {
|
|
|
1228
1277
|
|
|
1229
1278
|
const brightnessValue = Math.max(0, Math.min(Math.round(rawBrightness), 100))
|
|
1230
1279
|
this.#log.debug(LOG_PREFIX.ACCESSORY, `Setting day display brightness: ${brightnessValue}`)
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1280
|
+
try {
|
|
1281
|
+
await this.#deviceModel.updateConfig({
|
|
1282
|
+
dayDisplayBrightness: brightnessValue,
|
|
1283
|
+
dayDisplayBrightnessAuto: false
|
|
1284
|
+
})
|
|
1285
|
+
} catch (error) {
|
|
1286
|
+
this.#log.error(LOG_PREFIX.ACCESSORY, `[${this.#device.name}] Failed to set day brightness:`, error)
|
|
1287
|
+
throw new this.#platform.api.hap.HapStatusError(
|
|
1288
|
+
this.#platform.api.hap.HAPStatus.SERVICE_COMMUNICATION_FAILURE
|
|
1289
|
+
)
|
|
1290
|
+
}
|
|
1235
1291
|
}
|
|
1236
1292
|
|
|
1237
1293
|
/**
|
|
@@ -1241,10 +1297,12 @@ export class YotoPlayerAccessory {
|
|
|
1241
1297
|
async getDayNightlightHue () {
|
|
1242
1298
|
const color = this.#deviceModel.config.ambientColour
|
|
1243
1299
|
if (this.isColorOff(color)) {
|
|
1300
|
+
this.#log.debug(LOG_PREFIX.ACCESSORY, `[${this.#device.name}] Get day nightlight hue -> 0 (off)`)
|
|
1244
1301
|
return 0
|
|
1245
1302
|
}
|
|
1246
1303
|
const hex = this.parseHexColor(color)
|
|
1247
1304
|
const [h] = convert.hex.hsv(hex)
|
|
1305
|
+
this.#log.debug(LOG_PREFIX.ACCESSORY, `[${this.#device.name}] Get day nightlight hue -> ${h} (${color})`)
|
|
1248
1306
|
return h
|
|
1249
1307
|
}
|
|
1250
1308
|
|
|
@@ -1275,7 +1333,14 @@ export class YotoPlayerAccessory {
|
|
|
1275
1333
|
const formattedColor = this.formatHexColor(newHex)
|
|
1276
1334
|
|
|
1277
1335
|
this.#log.debug(LOG_PREFIX.ACCESSORY, `Setting day nightlight hue: ${hue}° → ${formattedColor}`)
|
|
1278
|
-
|
|
1336
|
+
try {
|
|
1337
|
+
await this.#deviceModel.updateConfig({ ambientColour: formattedColor })
|
|
1338
|
+
} catch (error) {
|
|
1339
|
+
this.#log.error(LOG_PREFIX.ACCESSORY, `[${this.#device.name}] Failed to set day nightlight hue:`, error)
|
|
1340
|
+
throw new this.#platform.api.hap.HapStatusError(
|
|
1341
|
+
this.#platform.api.hap.HAPStatus.SERVICE_COMMUNICATION_FAILURE
|
|
1342
|
+
)
|
|
1343
|
+
}
|
|
1279
1344
|
}
|
|
1280
1345
|
|
|
1281
1346
|
/**
|
|
@@ -1285,10 +1350,12 @@ export class YotoPlayerAccessory {
|
|
|
1285
1350
|
async getDayNightlightSaturation () {
|
|
1286
1351
|
const color = this.#deviceModel.config.ambientColour
|
|
1287
1352
|
if (this.isColorOff(color)) {
|
|
1353
|
+
this.#log.debug(LOG_PREFIX.ACCESSORY, `[${this.#device.name}] Get day nightlight saturation -> 0 (off)`)
|
|
1288
1354
|
return 0
|
|
1289
1355
|
}
|
|
1290
1356
|
const hex = this.parseHexColor(color)
|
|
1291
1357
|
const [, s] = convert.hex.hsv(hex)
|
|
1358
|
+
this.#log.debug(LOG_PREFIX.ACCESSORY, `[${this.#device.name}] Get day nightlight saturation -> ${s} (${color})`)
|
|
1292
1359
|
return s
|
|
1293
1360
|
}
|
|
1294
1361
|
|
|
@@ -1319,7 +1386,14 @@ export class YotoPlayerAccessory {
|
|
|
1319
1386
|
const formattedColor = this.formatHexColor(newHex)
|
|
1320
1387
|
|
|
1321
1388
|
this.#log.debug(LOG_PREFIX.ACCESSORY, `Setting day nightlight saturation: ${saturation}% → ${formattedColor}`)
|
|
1322
|
-
|
|
1389
|
+
try {
|
|
1390
|
+
await this.#deviceModel.updateConfig({ ambientColour: formattedColor })
|
|
1391
|
+
} catch (error) {
|
|
1392
|
+
this.#log.error(LOG_PREFIX.ACCESSORY, `[${this.#device.name}] Failed to set day nightlight saturation:`, error)
|
|
1393
|
+
throw new this.#platform.api.hap.HapStatusError(
|
|
1394
|
+
this.#platform.api.hap.HAPStatus.SERVICE_COMMUNICATION_FAILURE
|
|
1395
|
+
)
|
|
1396
|
+
}
|
|
1323
1397
|
}
|
|
1324
1398
|
|
|
1325
1399
|
// ---------- Night Nightlight Handlers ----------
|
|
@@ -1330,7 +1404,9 @@ export class YotoPlayerAccessory {
|
|
|
1330
1404
|
*/
|
|
1331
1405
|
async getNightNightlightOn () {
|
|
1332
1406
|
const color = this.#deviceModel.config.nightAmbientColour
|
|
1333
|
-
|
|
1407
|
+
const isOn = !this.isColorOff(color)
|
|
1408
|
+
this.#log.debug(LOG_PREFIX.ACCESSORY, `[${this.#device.name}] Get night nightlight on -> ${isOn} (${color})`)
|
|
1409
|
+
return isOn
|
|
1334
1410
|
}
|
|
1335
1411
|
|
|
1336
1412
|
/**
|
|
@@ -1338,19 +1414,26 @@ export class YotoPlayerAccessory {
|
|
|
1338
1414
|
* @param {CharacteristicValue} value
|
|
1339
1415
|
*/
|
|
1340
1416
|
async setNightNightlightOn (value) {
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
this
|
|
1417
|
+
try {
|
|
1418
|
+
if (value) {
|
|
1419
|
+
// Turn ON - restore previous color or default to white
|
|
1420
|
+
const colorToSet = this.#lastNightColor || '0xffffff'
|
|
1421
|
+
this.#log.debug(LOG_PREFIX.ACCESSORY, `Turning night nightlight ON with color: ${colorToSet}`)
|
|
1422
|
+
await this.#deviceModel.updateConfig({ nightAmbientColour: colorToSet })
|
|
1423
|
+
} else {
|
|
1424
|
+
// Turn OFF - save current color and set to black
|
|
1425
|
+
const currentColor = this.#deviceModel.config.nightAmbientColour
|
|
1426
|
+
if (!this.isColorOff(currentColor)) {
|
|
1427
|
+
this.#lastNightColor = currentColor
|
|
1428
|
+
}
|
|
1429
|
+
this.#log.debug(LOG_PREFIX.ACCESSORY, 'Turning night nightlight OFF')
|
|
1430
|
+
await this.#deviceModel.updateConfig({ nightAmbientColour: '0x000000' })
|
|
1351
1431
|
}
|
|
1352
|
-
|
|
1353
|
-
|
|
1432
|
+
} catch (error) {
|
|
1433
|
+
this.#log.error(LOG_PREFIX.ACCESSORY, `[${this.#device.name}] Failed to set night nightlight:`, error)
|
|
1434
|
+
throw new this.#platform.api.hap.HapStatusError(
|
|
1435
|
+
this.#platform.api.hap.HAPStatus.SERVICE_COMMUNICATION_FAILURE
|
|
1436
|
+
)
|
|
1354
1437
|
}
|
|
1355
1438
|
}
|
|
1356
1439
|
|
|
@@ -1360,10 +1443,16 @@ export class YotoPlayerAccessory {
|
|
|
1360
1443
|
*/
|
|
1361
1444
|
async getNightNightlightBrightness () {
|
|
1362
1445
|
const config = this.#deviceModel.config
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1446
|
+
const isAuto = config.nightDisplayBrightnessAuto
|
|
1447
|
+
const raw = config.nightDisplayBrightness
|
|
1448
|
+
const brightness = isAuto || raw === null
|
|
1449
|
+
? 100
|
|
1450
|
+
: Math.max(0, Math.min(Math.round(raw), 100))
|
|
1451
|
+
this.#log.debug(
|
|
1452
|
+
LOG_PREFIX.ACCESSORY,
|
|
1453
|
+
`[${this.#device.name}] Get night nightlight brightness -> ${brightness} (raw=${raw} auto=${isAuto})`
|
|
1454
|
+
)
|
|
1455
|
+
return brightness
|
|
1367
1456
|
}
|
|
1368
1457
|
|
|
1369
1458
|
/**
|
|
@@ -1380,10 +1469,17 @@ export class YotoPlayerAccessory {
|
|
|
1380
1469
|
|
|
1381
1470
|
const brightnessValue = Math.max(0, Math.min(Math.round(rawBrightness), 100))
|
|
1382
1471
|
this.#log.debug(LOG_PREFIX.ACCESSORY, `Setting night display brightness: ${brightnessValue}`)
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1472
|
+
try {
|
|
1473
|
+
await this.#deviceModel.updateConfig({
|
|
1474
|
+
nightDisplayBrightness: brightnessValue,
|
|
1475
|
+
nightDisplayBrightnessAuto: false
|
|
1476
|
+
})
|
|
1477
|
+
} catch (error) {
|
|
1478
|
+
this.#log.error(LOG_PREFIX.ACCESSORY, `[${this.#device.name}] Failed to set night brightness:`, error)
|
|
1479
|
+
throw new this.#platform.api.hap.HapStatusError(
|
|
1480
|
+
this.#platform.api.hap.HAPStatus.SERVICE_COMMUNICATION_FAILURE
|
|
1481
|
+
)
|
|
1482
|
+
}
|
|
1387
1483
|
}
|
|
1388
1484
|
|
|
1389
1485
|
/**
|
|
@@ -1393,10 +1489,12 @@ export class YotoPlayerAccessory {
|
|
|
1393
1489
|
async getNightNightlightHue () {
|
|
1394
1490
|
const color = this.#deviceModel.config.nightAmbientColour
|
|
1395
1491
|
if (this.isColorOff(color)) {
|
|
1492
|
+
this.#log.debug(LOG_PREFIX.ACCESSORY, `[${this.#device.name}] Get night nightlight hue -> 0 (off)`)
|
|
1396
1493
|
return 0
|
|
1397
1494
|
}
|
|
1398
1495
|
const hex = this.parseHexColor(color)
|
|
1399
1496
|
const [h] = convert.hex.hsv(hex)
|
|
1497
|
+
this.#log.debug(LOG_PREFIX.ACCESSORY, `[${this.#device.name}] Get night nightlight hue -> ${h} (${color})`)
|
|
1400
1498
|
return h
|
|
1401
1499
|
}
|
|
1402
1500
|
|
|
@@ -1427,7 +1525,14 @@ export class YotoPlayerAccessory {
|
|
|
1427
1525
|
const formattedColor = this.formatHexColor(newHex)
|
|
1428
1526
|
|
|
1429
1527
|
this.#log.debug(LOG_PREFIX.ACCESSORY, `Setting night nightlight hue: ${hue}° → ${formattedColor}`)
|
|
1430
|
-
|
|
1528
|
+
try {
|
|
1529
|
+
await this.#deviceModel.updateConfig({ nightAmbientColour: formattedColor })
|
|
1530
|
+
} catch (error) {
|
|
1531
|
+
this.#log.error(LOG_PREFIX.ACCESSORY, `[${this.#device.name}] Failed to set night nightlight hue:`, error)
|
|
1532
|
+
throw new this.#platform.api.hap.HapStatusError(
|
|
1533
|
+
this.#platform.api.hap.HAPStatus.SERVICE_COMMUNICATION_FAILURE
|
|
1534
|
+
)
|
|
1535
|
+
}
|
|
1431
1536
|
}
|
|
1432
1537
|
|
|
1433
1538
|
/**
|
|
@@ -1437,10 +1542,12 @@ export class YotoPlayerAccessory {
|
|
|
1437
1542
|
async getNightNightlightSaturation () {
|
|
1438
1543
|
const color = this.#deviceModel.config.nightAmbientColour
|
|
1439
1544
|
if (this.isColorOff(color)) {
|
|
1545
|
+
this.#log.debug(LOG_PREFIX.ACCESSORY, `[${this.#device.name}] Get night nightlight saturation -> 0 (off)`)
|
|
1440
1546
|
return 0
|
|
1441
1547
|
}
|
|
1442
1548
|
const hex = this.parseHexColor(color)
|
|
1443
1549
|
const [, s] = convert.hex.hsv(hex)
|
|
1550
|
+
this.#log.debug(LOG_PREFIX.ACCESSORY, `[${this.#device.name}] Get night nightlight saturation -> ${s} (${color})`)
|
|
1444
1551
|
return s
|
|
1445
1552
|
}
|
|
1446
1553
|
|
|
@@ -1471,7 +1578,14 @@ export class YotoPlayerAccessory {
|
|
|
1471
1578
|
const formattedColor = this.formatHexColor(newHex)
|
|
1472
1579
|
|
|
1473
1580
|
this.#log.debug(LOG_PREFIX.ACCESSORY, `Setting night nightlight saturation: ${saturation}% → ${formattedColor}`)
|
|
1474
|
-
|
|
1581
|
+
try {
|
|
1582
|
+
await this.#deviceModel.updateConfig({ nightAmbientColour: formattedColor })
|
|
1583
|
+
} catch (error) {
|
|
1584
|
+
this.#log.error(LOG_PREFIX.ACCESSORY, `[${this.#device.name}] Failed to set night nightlight saturation:`, error)
|
|
1585
|
+
throw new this.#platform.api.hap.HapStatusError(
|
|
1586
|
+
this.#platform.api.hap.HAPStatus.SERVICE_COMMUNICATION_FAILURE
|
|
1587
|
+
)
|
|
1588
|
+
}
|
|
1475
1589
|
}
|
|
1476
1590
|
|
|
1477
1591
|
// ==================== Nightlight Status ContactSensor Getters ====================
|
|
@@ -1484,6 +1598,7 @@ export class YotoPlayerAccessory {
|
|
|
1484
1598
|
const { Characteristic } = this.#platform
|
|
1485
1599
|
const status = this.#deviceModel.status
|
|
1486
1600
|
const isActive = status.nightlightMode !== 'off'
|
|
1601
|
+
this.#log.debug(LOG_PREFIX.ACCESSORY, `[${this.#device.name}] Get nightlight active -> ${isActive}`)
|
|
1487
1602
|
return isActive ? Characteristic.ContactSensorState.CONTACT_NOT_DETECTED : Characteristic.ContactSensorState.CONTACT_DETECTED
|
|
1488
1603
|
}
|
|
1489
1604
|
|
|
@@ -1497,6 +1612,10 @@ export class YotoPlayerAccessory {
|
|
|
1497
1612
|
const isDay = status.dayMode === 'day'
|
|
1498
1613
|
const isActive = status.nightlightMode !== 'off'
|
|
1499
1614
|
const isShowing = isDay && isActive
|
|
1615
|
+
this.#log.debug(
|
|
1616
|
+
LOG_PREFIX.ACCESSORY,
|
|
1617
|
+
`[${this.#device.name}] Get day nightlight active -> ${isShowing} (day=${isDay} active=${isActive})`
|
|
1618
|
+
)
|
|
1500
1619
|
return isShowing ? Characteristic.ContactSensorState.CONTACT_NOT_DETECTED : Characteristic.ContactSensorState.CONTACT_DETECTED
|
|
1501
1620
|
}
|
|
1502
1621
|
|
|
@@ -1510,6 +1629,10 @@ export class YotoPlayerAccessory {
|
|
|
1510
1629
|
const isNight = status.dayMode === 'night'
|
|
1511
1630
|
const isActive = status.nightlightMode !== 'off'
|
|
1512
1631
|
const isShowing = isNight && isActive
|
|
1632
|
+
this.#log.debug(
|
|
1633
|
+
LOG_PREFIX.ACCESSORY,
|
|
1634
|
+
`[${this.#device.name}] Get night nightlight active -> ${isShowing} (night=${isNight} active=${isActive})`
|
|
1635
|
+
)
|
|
1513
1636
|
return isShowing ? Characteristic.ContactSensorState.CONTACT_NOT_DETECTED : Characteristic.ContactSensorState.CONTACT_DETECTED
|
|
1514
1637
|
}
|
|
1515
1638
|
|
|
@@ -1523,6 +1646,7 @@ export class YotoPlayerAccessory {
|
|
|
1523
1646
|
const { Characteristic } = this.#platform
|
|
1524
1647
|
const status = this.#deviceModel.status
|
|
1525
1648
|
const hasCard = status.cardInsertionState !== 'none'
|
|
1649
|
+
this.#log.debug(LOG_PREFIX.ACCESSORY, `[${this.#device.name}] Get card slot -> ${hasCard} (${status.cardInsertionState})`)
|
|
1526
1650
|
return hasCard ? Characteristic.ContactSensorState.CONTACT_NOT_DETECTED : Characteristic.ContactSensorState.CONTACT_DETECTED
|
|
1527
1651
|
}
|
|
1528
1652
|
|
|
@@ -1536,6 +1660,7 @@ export class YotoPlayerAccessory {
|
|
|
1536
1660
|
const { Characteristic } = this.#platform
|
|
1537
1661
|
const status = this.#deviceModel.status
|
|
1538
1662
|
const isDayMode = status.dayMode === 'day'
|
|
1663
|
+
this.#log.debug(LOG_PREFIX.ACCESSORY, `[${this.#device.name}] Get day mode -> ${isDayMode}`)
|
|
1539
1664
|
return isDayMode
|
|
1540
1665
|
? Characteristic.ContactSensorState.CONTACT_NOT_DETECTED
|
|
1541
1666
|
: Characteristic.ContactSensorState.CONTACT_DETECTED
|
|
@@ -1549,6 +1674,10 @@ export class YotoPlayerAccessory {
|
|
|
1549
1674
|
*/
|
|
1550
1675
|
async getSleepTimerState () {
|
|
1551
1676
|
const playback = this.#deviceModel.playback
|
|
1677
|
+
this.#log.debug(
|
|
1678
|
+
LOG_PREFIX.ACCESSORY,
|
|
1679
|
+
`[${this.#device.name}] Get sleep timer -> ${playback.sleepTimerActive ?? false}`
|
|
1680
|
+
)
|
|
1552
1681
|
return playback.sleepTimerActive ?? false
|
|
1553
1682
|
}
|
|
1554
1683
|
|
|
@@ -1584,7 +1713,9 @@ export class YotoPlayerAccessory {
|
|
|
1584
1713
|
* @returns {Promise<CharacteristicValue>}
|
|
1585
1714
|
*/
|
|
1586
1715
|
async getBluetoothState () {
|
|
1587
|
-
|
|
1716
|
+
const enabled = this.#deviceModel.config.bluetoothEnabled ?? false
|
|
1717
|
+
this.#log.debug(LOG_PREFIX.ACCESSORY, `[${this.#device.name}] Get Bluetooth -> ${enabled}`)
|
|
1718
|
+
return enabled
|
|
1588
1719
|
}
|
|
1589
1720
|
|
|
1590
1721
|
/**
|
|
@@ -1594,7 +1725,14 @@ export class YotoPlayerAccessory {
|
|
|
1594
1725
|
async setBluetoothState (value) {
|
|
1595
1726
|
const enabled = Boolean(value)
|
|
1596
1727
|
this.#log.debug(LOG_PREFIX.ACCESSORY, `Setting Bluetooth: ${enabled ? 'ON' : 'OFF'}`)
|
|
1597
|
-
|
|
1728
|
+
try {
|
|
1729
|
+
await this.#deviceModel.updateConfig({ bluetoothEnabled: enabled })
|
|
1730
|
+
} catch (error) {
|
|
1731
|
+
this.#log.error(LOG_PREFIX.ACCESSORY, `[${this.#device.name}] Failed to set Bluetooth:`, error)
|
|
1732
|
+
throw new this.#platform.api.hap.HapStatusError(
|
|
1733
|
+
this.#platform.api.hap.HAPStatus.SERVICE_COMMUNICATION_FAILURE
|
|
1734
|
+
)
|
|
1735
|
+
}
|
|
1598
1736
|
}
|
|
1599
1737
|
|
|
1600
1738
|
// ==================== Card Control Switch Setter ====================
|
|
@@ -1609,6 +1747,10 @@ export class YotoPlayerAccessory {
|
|
|
1609
1747
|
async setCardControl (service, control, value) {
|
|
1610
1748
|
const { Characteristic } = this.#platform
|
|
1611
1749
|
const isOn = Boolean(value)
|
|
1750
|
+
this.#log.debug(
|
|
1751
|
+
LOG_PREFIX.ACCESSORY,
|
|
1752
|
+
`[${this.#device.name}] Set card control: ${control.label} (${control.cardId}) -> ${isOn}`
|
|
1753
|
+
)
|
|
1612
1754
|
|
|
1613
1755
|
if (!isOn) {
|
|
1614
1756
|
service.getCharacteristic(Characteristic.On).updateValue(false)
|
|
@@ -1683,7 +1825,14 @@ export class YotoPlayerAccessory {
|
|
|
1683
1825
|
LOG_PREFIX.ACCESSORY,
|
|
1684
1826
|
`[${this.#device.name}] Set day max volume limit raw=${value} normalizedPercent=${normalizedPercent} requestedSteps=${requestedSteps} -> steps=${limit} percent=${limitPercent}`
|
|
1685
1827
|
)
|
|
1686
|
-
|
|
1828
|
+
try {
|
|
1829
|
+
await this.#deviceModel.updateConfig({ maxVolumeLimit: limit })
|
|
1830
|
+
} catch (error) {
|
|
1831
|
+
this.#log.error(LOG_PREFIX.ACCESSORY, `[${this.#device.name}] Failed to set day max volume limit:`, error)
|
|
1832
|
+
throw new this.#platform.api.hap.HapStatusError(
|
|
1833
|
+
this.#platform.api.hap.HAPStatus.SERVICE_COMMUNICATION_FAILURE
|
|
1834
|
+
)
|
|
1835
|
+
}
|
|
1687
1836
|
}
|
|
1688
1837
|
|
|
1689
1838
|
/**
|
|
@@ -1722,7 +1871,14 @@ export class YotoPlayerAccessory {
|
|
|
1722
1871
|
LOG_PREFIX.ACCESSORY,
|
|
1723
1872
|
`[${this.#device.name}] Set night max volume limit raw=${value} normalizedPercent=${normalizedPercent} requestedSteps=${requestedSteps} -> steps=${limit} percent=${limitPercent}`
|
|
1724
1873
|
)
|
|
1725
|
-
|
|
1874
|
+
try {
|
|
1875
|
+
await this.#deviceModel.updateConfig({ nightMaxVolumeLimit: limit })
|
|
1876
|
+
} catch (error) {
|
|
1877
|
+
this.#log.error(LOG_PREFIX.ACCESSORY, `[${this.#device.name}] Failed to set night max volume limit:`, error)
|
|
1878
|
+
throw new this.#platform.api.hap.HapStatusError(
|
|
1879
|
+
this.#platform.api.hap.HAPStatus.SERVICE_COMMUNICATION_FAILURE
|
|
1880
|
+
)
|
|
1881
|
+
}
|
|
1726
1882
|
}
|
|
1727
1883
|
|
|
1728
1884
|
// ==================== Characteristic Update Methods ====================
|
|
@@ -1746,6 +1902,8 @@ export class YotoPlayerAccessory {
|
|
|
1746
1902
|
* @param {number} volumeSteps - Volume level (0-16)
|
|
1747
1903
|
*/
|
|
1748
1904
|
updateVolumeCharacteristic (volumeSteps) {
|
|
1905
|
+
if (!this.volumeService) return
|
|
1906
|
+
|
|
1749
1907
|
if (volumeSteps > 0) {
|
|
1750
1908
|
this.#lastNonZeroVolume = Math.round((volumeSteps / 16) * 100)
|
|
1751
1909
|
}
|
|
@@ -1757,7 +1915,6 @@ export class YotoPlayerAccessory {
|
|
|
1757
1915
|
LOG_PREFIX.ACCESSORY,
|
|
1758
1916
|
`[${this.#device.name}] Update volume characteristic rawSteps=${volumeSteps} percent=${percent}`
|
|
1759
1917
|
)
|
|
1760
|
-
if (!this.volumeService) return
|
|
1761
1918
|
|
|
1762
1919
|
const { Characteristic } = this.#platform
|
|
1763
1920
|
this.volumeService
|
|
@@ -72,13 +72,20 @@ export class YotoCardControlAccessory {
|
|
|
72
72
|
this.#accessory.addService(Service.AccessoryInformation)
|
|
73
73
|
|
|
74
74
|
const displayName = sanitizeName(this.#accessory.displayName)
|
|
75
|
+
const nameCharacteristic = service.getCharacteristic(Characteristic.Name)
|
|
76
|
+
const configuredCharacteristic = service.getCharacteristic(Characteristic.ConfiguredName)
|
|
77
|
+
const previousName = nameCharacteristic.value
|
|
78
|
+
const configuredName = configuredCharacteristic.value
|
|
75
79
|
|
|
76
80
|
service
|
|
77
81
|
.setCharacteristic(Characteristic.Manufacturer, DEFAULT_MANUFACTURER)
|
|
78
82
|
.setCharacteristic(Characteristic.Model, DEFAULT_MODEL)
|
|
79
83
|
.setCharacteristic(Characteristic.SerialNumber, this.#cardControl.id)
|
|
80
84
|
.setCharacteristic(Characteristic.Name, displayName)
|
|
81
|
-
|
|
85
|
+
|
|
86
|
+
if (typeof configuredName !== 'string' || configuredName === previousName) {
|
|
87
|
+
service.setCharacteristic(Characteristic.ConfiguredName, displayName)
|
|
88
|
+
}
|
|
82
89
|
|
|
83
90
|
this.#currentServices.add(service)
|
|
84
91
|
}
|
|
@@ -114,6 +121,10 @@ export class YotoCardControlAccessory {
|
|
|
114
121
|
async setCardControl (value) {
|
|
115
122
|
const { Characteristic } = this.#platform
|
|
116
123
|
const isOn = Boolean(value)
|
|
124
|
+
this.#log.debug(
|
|
125
|
+
LOG_PREFIX.ACCESSORY,
|
|
126
|
+
`Card control toggle requested: ${this.#cardControl.label} (${this.#cardControl.cardId}) -> ${isOn}`
|
|
127
|
+
)
|
|
117
128
|
|
|
118
129
|
if (!isOn) {
|
|
119
130
|
this.switchService?.getCharacteristic(Characteristic.On).updateValue(false)
|
package/lib/platform.js
CHANGED
|
@@ -127,13 +127,20 @@ export class YotoPlatform {
|
|
|
127
127
|
}
|
|
128
128
|
})
|
|
129
129
|
|
|
130
|
+
const formatError = (/** @type {unknown} */ error) => (
|
|
131
|
+
error instanceof Error ? (error.stack || error.message) : String(error)
|
|
132
|
+
)
|
|
133
|
+
|
|
130
134
|
// Listen to account-level events
|
|
131
135
|
this.yotoAccount.on('error', ({ error, context }) => {
|
|
136
|
+
const details = formatError(error)
|
|
132
137
|
if (context.deviceId) {
|
|
133
138
|
const label = this.formatDeviceLabel(context.deviceId)
|
|
134
|
-
log.error(`Device error [${label} ${context.operation} ${context.source}]:`,
|
|
139
|
+
log.error(`Device error [${label} ${context.operation} ${context.source}]:`, details)
|
|
140
|
+
log.debug('Device error context:', context)
|
|
135
141
|
} else {
|
|
136
|
-
log.error('Account error:',
|
|
142
|
+
log.error('Account error:', details)
|
|
143
|
+
log.debug('Account error context:', context)
|
|
137
144
|
}
|
|
138
145
|
})
|
|
139
146
|
|
|
@@ -163,7 +170,7 @@ export class YotoPlatform {
|
|
|
163
170
|
*/
|
|
164
171
|
configureAccessory (accessory) {
|
|
165
172
|
const { log, accessories, cardAccessories } = this
|
|
166
|
-
log.debug('Loading accessory from cache:', accessory.displayName)
|
|
173
|
+
log.debug('Loading accessory from cache:', accessory.displayName, accessory.UUID)
|
|
167
174
|
|
|
168
175
|
const context = accessory.context
|
|
169
176
|
const record = context && typeof context === 'object'
|
|
@@ -172,6 +179,7 @@ export class YotoPlatform {
|
|
|
172
179
|
const accessoryType = record && typeof record['type'] === 'string'
|
|
173
180
|
? record['type']
|
|
174
181
|
: undefined
|
|
182
|
+
log.debug('Cached accessory context type:', accessory.displayName, accessoryType ?? 'device')
|
|
175
183
|
|
|
176
184
|
if (accessoryType === 'card-control' || record?.['cardControl']) {
|
|
177
185
|
cardAccessories.set(accessory.UUID, /** @type {PlatformAccessory<YotoCardAccessoryContext>} */ (accessory))
|
|
@@ -193,6 +201,7 @@ export class YotoPlatform {
|
|
|
193
201
|
|
|
194
202
|
try {
|
|
195
203
|
this.log.debug('Starting Yoto account...')
|
|
204
|
+
this.log.debug('Playback accessory mode:', this.playbackAccessoryConfig.mode)
|
|
196
205
|
|
|
197
206
|
// Listen for devices being added
|
|
198
207
|
this.yotoAccount.on('deviceAdded', async ({ deviceId }) => {
|
|
@@ -205,6 +214,7 @@ export class YotoPlatform {
|
|
|
205
214
|
|
|
206
215
|
const device = deviceModel.device
|
|
207
216
|
this.log.info(`Device discovered: ${device.name} (${deviceId})`)
|
|
217
|
+
this.log.debug('Registering device from account discovery:', device.name, deviceId)
|
|
208
218
|
await this.registerDevice(device, deviceModel)
|
|
209
219
|
})
|
|
210
220
|
|
|
@@ -327,10 +337,15 @@ export class YotoPlatform {
|
|
|
327
337
|
await this.yotoAccount.start()
|
|
328
338
|
|
|
329
339
|
this.log.info(`✓ Yoto account started with ${this.yotoAccount.devices.size} device(s)`)
|
|
340
|
+
this.log.debug(
|
|
341
|
+
'Account devices:',
|
|
342
|
+
Array.from(this.yotoAccount.devices.keys()).join(', ') || 'none'
|
|
343
|
+
)
|
|
330
344
|
|
|
331
345
|
// Remove stale accessories after all devices are registered
|
|
332
346
|
this.removeStaleAccessories()
|
|
333
347
|
|
|
348
|
+
this.log.debug('Registering card control accessories (playOnAll).')
|
|
334
349
|
await this.registerCardControlAccessories()
|
|
335
350
|
} catch (error) {
|
|
336
351
|
this.log.error('Failed to start account:', error instanceof Error ? error.message : String(error))
|
|
@@ -394,28 +409,24 @@ export class YotoPlatform {
|
|
|
394
409
|
const uuid = this.api.hap.uuid.generate(device.deviceId)
|
|
395
410
|
const sanitizedDeviceName = sanitizeName(device.name)
|
|
396
411
|
const accessoryCategory = this.api.hap.Categories.SPEAKER
|
|
412
|
+
this.log.debug(
|
|
413
|
+
'Register device:',
|
|
414
|
+
`${device.name} (${device.deviceId})`,
|
|
415
|
+
`uuid=${uuid}`,
|
|
416
|
+
`category=${accessoryCategory}`
|
|
417
|
+
)
|
|
397
418
|
|
|
398
419
|
// Check if accessory already exists
|
|
399
420
|
const existingAccessory = this.accessories.get(uuid)
|
|
400
421
|
|
|
401
422
|
if (existingAccessory) {
|
|
402
423
|
// Accessory exists - update it
|
|
403
|
-
this.log.debug('Restoring existing accessory from cache:', device.name)
|
|
424
|
+
this.log.debug('Restoring existing accessory from cache:', device.name, existingAccessory.UUID)
|
|
404
425
|
|
|
405
426
|
// Update display name if it has changed
|
|
406
427
|
if (existingAccessory.displayName !== sanitizedDeviceName) {
|
|
428
|
+
this.log.debug('Updating accessory display name:', existingAccessory.displayName, '->', sanitizedDeviceName)
|
|
407
429
|
existingAccessory.updateDisplayName(sanitizedDeviceName)
|
|
408
|
-
const infoService = existingAccessory.getService(this.api.hap.Service.AccessoryInformation)
|
|
409
|
-
if (infoService) {
|
|
410
|
-
// Only update Name, preserve user's ConfiguredName customization
|
|
411
|
-
// ConfiguredName is intentionally NOT updated here because:
|
|
412
|
-
// - It allows users to rename accessories in the Home app
|
|
413
|
-
// - Their custom names should survive Homebridge restarts and Yoto device name changes
|
|
414
|
-
// - Name stays in sync with Yoto's device name for plugin identification
|
|
415
|
-
infoService
|
|
416
|
-
.setCharacteristic(this.api.hap.Characteristic.Name, sanitizedDeviceName)
|
|
417
|
-
.setCharacteristic(this.api.hap.Characteristic.ConfiguredName, sanitizedDeviceName)
|
|
418
|
-
}
|
|
419
430
|
}
|
|
420
431
|
|
|
421
432
|
// Update context with fresh device data
|
|
@@ -432,6 +443,7 @@ export class YotoPlatform {
|
|
|
432
443
|
|
|
433
444
|
// Update accessory information
|
|
434
445
|
this.api.updatePlatformAccessories([existingAccessory])
|
|
446
|
+
this.log.debug('Updated accessory cache entry:', existingAccessory.displayName, existingAccessory.UUID)
|
|
435
447
|
|
|
436
448
|
// Create handler for this accessory with device model
|
|
437
449
|
const handler = new YotoPlayerAccessory({
|
|
@@ -442,9 +454,11 @@ export class YotoPlatform {
|
|
|
442
454
|
|
|
443
455
|
// Track handler
|
|
444
456
|
this.accessoryHandlers.set(uuid, handler)
|
|
457
|
+
this.log.debug('Created accessory handler:', existingAccessory.displayName, uuid)
|
|
445
458
|
|
|
446
459
|
// Initialize accessory (setup services and event listeners)
|
|
447
460
|
await handler.setup()
|
|
461
|
+
this.log.debug('Accessory setup complete:', existingAccessory.displayName)
|
|
448
462
|
|
|
449
463
|
if (this.playbackAccessoryConfig.mode === 'external') {
|
|
450
464
|
await this.registerSpeakerAccessory(device, deviceModel)
|
|
@@ -453,21 +467,13 @@ export class YotoPlatform {
|
|
|
453
467
|
return { success: true }
|
|
454
468
|
} else {
|
|
455
469
|
// Create new accessory
|
|
456
|
-
this.log.debug('Adding new accessory:', device.name)
|
|
470
|
+
this.log.debug('Adding new accessory:', device.name, uuid)
|
|
457
471
|
|
|
458
472
|
// Create platform accessory
|
|
459
473
|
/** @type {PlatformAccessory<YotoAccessoryContext>} */
|
|
460
474
|
// eslint-disable-next-line new-cap
|
|
461
475
|
const accessory = new this.api.platformAccessory(sanitizedDeviceName, uuid, accessoryCategory)
|
|
462
476
|
|
|
463
|
-
// Set Name and ConfiguredName on AccessoryInformation service
|
|
464
|
-
const infoService = accessory.getService(this.api.hap.Service.AccessoryInformation)
|
|
465
|
-
if (infoService) {
|
|
466
|
-
infoService
|
|
467
|
-
.setCharacteristic(this.api.hap.Characteristic.Name, sanitizedDeviceName)
|
|
468
|
-
.setCharacteristic(this.api.hap.Characteristic.ConfiguredName, sanitizedDeviceName)
|
|
469
|
-
}
|
|
470
|
-
|
|
471
477
|
// Set accessory context
|
|
472
478
|
accessory.context = {
|
|
473
479
|
type: 'device',
|
|
@@ -483,13 +489,16 @@ export class YotoPlatform {
|
|
|
483
489
|
|
|
484
490
|
// Track handler
|
|
485
491
|
this.accessoryHandlers.set(uuid, handler)
|
|
492
|
+
this.log.debug('Created accessory handler:', device.name, uuid)
|
|
486
493
|
|
|
487
494
|
// Initialize accessory (setup services and event listeners)
|
|
488
495
|
await handler.setup()
|
|
496
|
+
this.log.debug('Accessory setup complete:', device.name)
|
|
489
497
|
|
|
490
498
|
// Register as a platform accessory (bridged).
|
|
491
499
|
this.log.debug(`Registering new accessory: ${device.name}`)
|
|
492
500
|
this.api.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [accessory])
|
|
501
|
+
this.log.debug('Registered platform accessory:', device.name, uuid)
|
|
493
502
|
|
|
494
503
|
if (this.playbackAccessoryConfig.mode === 'external') {
|
|
495
504
|
await this.registerSpeakerAccessory(device, deviceModel)
|
|
@@ -497,6 +506,7 @@ export class YotoPlatform {
|
|
|
497
506
|
|
|
498
507
|
// Add to our tracking map (cast to typed version)
|
|
499
508
|
this.accessories.set(uuid, accessory)
|
|
509
|
+
this.log.debug('Tracked new accessory:', device.name, uuid)
|
|
500
510
|
|
|
501
511
|
return { success: true }
|
|
502
512
|
}
|
|
@@ -512,11 +522,12 @@ export class YotoPlatform {
|
|
|
512
522
|
const uuid = this.getSpeakerAccessoryUuid(device.deviceId)
|
|
513
523
|
const speakerName = this.getSpeakerAccessoryName(device)
|
|
514
524
|
if (this.speakerAccessories.has(uuid)) {
|
|
515
|
-
this.log.debug('SmartSpeaker accessory already published:', speakerName)
|
|
525
|
+
this.log.debug('SmartSpeaker accessory already published:', speakerName, uuid)
|
|
516
526
|
return { success: true }
|
|
517
527
|
}
|
|
518
528
|
|
|
519
529
|
this.log.info('Adding new SmartSpeaker accessory:', speakerName)
|
|
530
|
+
this.log.debug('Creating SmartSpeaker accessory:', speakerName, uuid)
|
|
520
531
|
|
|
521
532
|
/** @type {PlatformAccessory<YotoAccessoryContext>} */
|
|
522
533
|
// eslint-disable-next-line new-cap
|
|
@@ -526,13 +537,6 @@ export class YotoPlatform {
|
|
|
526
537
|
this.api.hap.Categories.SPEAKER
|
|
527
538
|
)
|
|
528
539
|
|
|
529
|
-
const infoService = accessory.getService(this.api.hap.Service.AccessoryInformation)
|
|
530
|
-
if (infoService) {
|
|
531
|
-
infoService
|
|
532
|
-
.setCharacteristic(this.api.hap.Characteristic.Name, speakerName)
|
|
533
|
-
.setCharacteristic(this.api.hap.Characteristic.ConfiguredName, speakerName)
|
|
534
|
-
}
|
|
535
|
-
|
|
536
540
|
accessory.context = {
|
|
537
541
|
device,
|
|
538
542
|
}
|
|
@@ -544,11 +548,14 @@ export class YotoPlatform {
|
|
|
544
548
|
})
|
|
545
549
|
|
|
546
550
|
this.speakerAccessoryHandlers.set(uuid, handler)
|
|
551
|
+
this.log.debug('Created SmartSpeaker handler:', speakerName, uuid)
|
|
547
552
|
|
|
548
553
|
await handler.setup()
|
|
554
|
+
this.log.debug('SmartSpeaker setup complete:', speakerName)
|
|
549
555
|
|
|
550
556
|
this.log.info(`Publishing external SmartSpeaker accessory: ${speakerName}`)
|
|
551
557
|
this.api.publishExternalAccessories(PLUGIN_NAME, [accessory])
|
|
558
|
+
this.log.debug('Published external SmartSpeaker accessory:', speakerName, uuid)
|
|
552
559
|
|
|
553
560
|
this.speakerAccessories.set(uuid, accessory)
|
|
554
561
|
|
|
@@ -562,24 +569,21 @@ export class YotoPlatform {
|
|
|
562
569
|
async registerCardControlAccessories () {
|
|
563
570
|
const cardControls = getCardControlConfigs(this.config).filter(control => control.playOnAll)
|
|
564
571
|
const desiredUuids = new Set()
|
|
572
|
+
this.log.debug('Card control configs (playOnAll):', cardControls.length)
|
|
565
573
|
|
|
566
574
|
for (const control of cardControls) {
|
|
567
575
|
const uuid = this.getCardControlAccessoryUuid(control)
|
|
568
576
|
const accessoryName = this.getCardControlAccessoryName(control)
|
|
569
577
|
desiredUuids.add(uuid)
|
|
578
|
+
this.log.debug('Ensuring card control accessory:', accessoryName, uuid)
|
|
570
579
|
|
|
571
580
|
const existingAccessory = this.cardAccessories.get(uuid)
|
|
572
581
|
if (existingAccessory) {
|
|
573
|
-
this.log.debug('Restoring existing card control accessory from cache:', accessoryName)
|
|
582
|
+
this.log.debug('Restoring existing card control accessory from cache:', accessoryName, uuid)
|
|
574
583
|
|
|
575
584
|
if (existingAccessory.displayName !== accessoryName) {
|
|
585
|
+
this.log.debug('Updating card control display name:', existingAccessory.displayName, '->', accessoryName)
|
|
576
586
|
existingAccessory.updateDisplayName(accessoryName)
|
|
577
|
-
const infoService = existingAccessory.getService(this.api.hap.Service.AccessoryInformation)
|
|
578
|
-
if (infoService) {
|
|
579
|
-
infoService
|
|
580
|
-
.setCharacteristic(this.api.hap.Characteristic.Name, accessoryName)
|
|
581
|
-
.setCharacteristic(this.api.hap.Characteristic.ConfiguredName, accessoryName)
|
|
582
|
-
}
|
|
583
587
|
}
|
|
584
588
|
|
|
585
589
|
existingAccessory.context = {
|
|
@@ -588,6 +592,7 @@ export class YotoPlatform {
|
|
|
588
592
|
}
|
|
589
593
|
|
|
590
594
|
this.api.updatePlatformAccessories([existingAccessory])
|
|
595
|
+
this.log.debug('Updated card control cache entry:', existingAccessory.displayName, uuid)
|
|
591
596
|
|
|
592
597
|
const existingHandler = this.cardAccessoryHandlers.get(uuid)
|
|
593
598
|
if (existingHandler) {
|
|
@@ -605,10 +610,11 @@ export class YotoPlatform {
|
|
|
605
610
|
|
|
606
611
|
this.cardAccessoryHandlers.set(uuid, handler)
|
|
607
612
|
await handler.setup()
|
|
613
|
+
this.log.debug('Card control setup complete:', accessoryName)
|
|
608
614
|
continue
|
|
609
615
|
}
|
|
610
616
|
|
|
611
|
-
this.log.debug('Adding new card control accessory:', accessoryName)
|
|
617
|
+
this.log.debug('Adding new card control accessory:', accessoryName, uuid)
|
|
612
618
|
|
|
613
619
|
/** @type {PlatformAccessory<YotoCardAccessoryContext>} */
|
|
614
620
|
// eslint-disable-next-line new-cap
|
|
@@ -618,13 +624,6 @@ export class YotoPlatform {
|
|
|
618
624
|
this.api.hap.Categories.SWITCH
|
|
619
625
|
)
|
|
620
626
|
|
|
621
|
-
const infoService = accessory.getService(this.api.hap.Service.AccessoryInformation)
|
|
622
|
-
if (infoService) {
|
|
623
|
-
infoService
|
|
624
|
-
.setCharacteristic(this.api.hap.Characteristic.Name, accessoryName)
|
|
625
|
-
.setCharacteristic(this.api.hap.Characteristic.ConfiguredName, accessoryName)
|
|
626
|
-
}
|
|
627
|
-
|
|
628
627
|
accessory.context = {
|
|
629
628
|
type: 'card-control',
|
|
630
629
|
cardControl: control,
|
|
@@ -638,11 +637,14 @@ export class YotoPlatform {
|
|
|
638
637
|
|
|
639
638
|
this.cardAccessoryHandlers.set(uuid, handler)
|
|
640
639
|
await handler.setup()
|
|
640
|
+
this.log.debug('Card control setup complete:', accessoryName)
|
|
641
641
|
|
|
642
642
|
this.log.debug(`Registering card control accessory: ${accessoryName}`)
|
|
643
643
|
this.api.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [accessory])
|
|
644
|
+
this.log.debug('Registered card control accessory:', accessoryName, uuid)
|
|
644
645
|
|
|
645
646
|
this.cardAccessories.set(uuid, accessory)
|
|
647
|
+
this.log.debug('Tracked card control accessory:', accessoryName, uuid)
|
|
646
648
|
}
|
|
647
649
|
|
|
648
650
|
for (const [uuid, accessory] of this.cardAccessories) {
|
|
@@ -650,7 +652,7 @@ export class YotoPlatform {
|
|
|
650
652
|
continue
|
|
651
653
|
}
|
|
652
654
|
|
|
653
|
-
this.log.debug('Removing card control accessory from cache:', accessory.displayName)
|
|
655
|
+
this.log.debug('Removing card control accessory from cache:', accessory.displayName, uuid)
|
|
654
656
|
|
|
655
657
|
const handler = this.cardAccessoryHandlers.get(uuid)
|
|
656
658
|
if (handler) {
|
|
@@ -662,6 +664,7 @@ export class YotoPlatform {
|
|
|
662
664
|
|
|
663
665
|
this.api.unregisterPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [accessory])
|
|
664
666
|
this.cardAccessories.delete(uuid)
|
|
667
|
+
this.log.debug('Removed card control accessory:', accessory.displayName, uuid)
|
|
665
668
|
}
|
|
666
669
|
}
|
|
667
670
|
|
|
@@ -676,10 +679,16 @@ export class YotoPlatform {
|
|
|
676
679
|
// Get current device IDs from account
|
|
677
680
|
const currentDeviceIds = this.yotoAccount.getDeviceIds()
|
|
678
681
|
const currentUUIDs = currentDeviceIds.map(id => this.api.hap.uuid.generate(id))
|
|
682
|
+
this.log.debug(
|
|
683
|
+
'Evaluating stale accessories:',
|
|
684
|
+
`accountDevices=${currentDeviceIds.length}`,
|
|
685
|
+
`cachedAccessories=${this.accessories.size}`,
|
|
686
|
+
`externalSpeakers=${this.speakerAccessories.size}`
|
|
687
|
+
)
|
|
679
688
|
|
|
680
689
|
for (const [uuid, accessory] of this.accessories) {
|
|
681
690
|
if (!currentUUIDs.includes(uuid)) {
|
|
682
|
-
this.log.debug('Removing existing accessory from cache:', accessory.displayName)
|
|
691
|
+
this.log.debug('Removing existing accessory from cache:', accessory.displayName, uuid)
|
|
683
692
|
|
|
684
693
|
// Stop handler if it exists
|
|
685
694
|
const handler = this.accessoryHandlers.get(uuid)
|
|
@@ -692,16 +701,18 @@ export class YotoPlatform {
|
|
|
692
701
|
|
|
693
702
|
// Unregister from Homebridge
|
|
694
703
|
this.api.unregisterPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [accessory])
|
|
704
|
+
this.log.debug('Unregistered accessory from Homebridge:', accessory.displayName, uuid)
|
|
695
705
|
|
|
696
706
|
// Remove from our tracking map
|
|
697
707
|
this.accessories.delete(uuid)
|
|
708
|
+
this.log.debug('Removed accessory from tracking map:', accessory.displayName, uuid)
|
|
698
709
|
}
|
|
699
710
|
}
|
|
700
711
|
|
|
701
712
|
for (const [uuid, accessory] of this.speakerAccessories) {
|
|
702
713
|
const deviceId = accessory.context.device?.deviceId
|
|
703
714
|
if (!deviceId || !currentDeviceIds.includes(deviceId)) {
|
|
704
|
-
this.log.debug('Removing external SmartSpeaker accessory from runtime:', accessory.displayName)
|
|
715
|
+
this.log.debug('Removing external SmartSpeaker accessory from runtime:', accessory.displayName, uuid)
|
|
705
716
|
|
|
706
717
|
const handler = this.speakerAccessoryHandlers.get(uuid)
|
|
707
718
|
if (handler) {
|
|
@@ -712,6 +723,7 @@ export class YotoPlatform {
|
|
|
712
723
|
}
|
|
713
724
|
|
|
714
725
|
this.speakerAccessories.delete(uuid)
|
|
726
|
+
this.log.debug('Removed external SmartSpeaker accessory:', accessory.displayName, uuid)
|
|
715
727
|
}
|
|
716
728
|
}
|
|
717
729
|
}
|
|
@@ -721,6 +733,12 @@ export class YotoPlatform {
|
|
|
721
733
|
*/
|
|
722
734
|
async shutdown () {
|
|
723
735
|
this.log.debug('Shutting down Yoto platform...')
|
|
736
|
+
this.log.debug(
|
|
737
|
+
'Handlers to stop:',
|
|
738
|
+
`devices=${this.accessoryHandlers.size}`,
|
|
739
|
+
`speakers=${this.speakerAccessoryHandlers.size}`,
|
|
740
|
+
`cardControls=${this.cardAccessoryHandlers.size}`
|
|
741
|
+
)
|
|
724
742
|
|
|
725
743
|
// Stop all accessory handlers
|
|
726
744
|
const stopPromises = []
|
package/lib/speaker-accessory.js
CHANGED
|
@@ -78,6 +78,11 @@ export class YotoSpeakerAccessory {
|
|
|
78
78
|
const { Service, Characteristic } = this.#platform
|
|
79
79
|
const service = this.#accessory.getService(Service.AccessoryInformation) ||
|
|
80
80
|
this.#accessory.addService(Service.AccessoryInformation)
|
|
81
|
+
const displayName = sanitizeName(this.#accessory.displayName)
|
|
82
|
+
const nameCharacteristic = service.getCharacteristic(Characteristic.Name)
|
|
83
|
+
const configuredCharacteristic = service.getCharacteristic(Characteristic.ConfiguredName)
|
|
84
|
+
const previousName = nameCharacteristic.value
|
|
85
|
+
const configuredName = configuredCharacteristic.value
|
|
81
86
|
|
|
82
87
|
const hardwareRevision = [
|
|
83
88
|
this.#device.generation,
|
|
@@ -87,11 +92,16 @@ export class YotoSpeakerAccessory {
|
|
|
87
92
|
const model = this.#device.deviceFamily || this.#device.deviceType || DEFAULT_MODEL
|
|
88
93
|
|
|
89
94
|
service
|
|
95
|
+
.setCharacteristic(Characteristic.Name, displayName)
|
|
90
96
|
.setCharacteristic(Characteristic.Manufacturer, DEFAULT_MANUFACTURER)
|
|
91
97
|
.setCharacteristic(Characteristic.Model, model)
|
|
92
98
|
.setCharacteristic(Characteristic.SerialNumber, this.#device.deviceId)
|
|
93
99
|
.setCharacteristic(Characteristic.HardwareRevision, hardwareRevision)
|
|
94
100
|
|
|
101
|
+
if (typeof configuredName !== 'string' || configuredName === previousName) {
|
|
102
|
+
service.setCharacteristic(Characteristic.ConfiguredName, displayName)
|
|
103
|
+
}
|
|
104
|
+
|
|
95
105
|
if (this.#deviceModel.status.firmwareVersion) {
|
|
96
106
|
service.setCharacteristic(
|
|
97
107
|
Characteristic.FirmwareRevision,
|
|
@@ -247,7 +257,8 @@ export class YotoSpeakerAccessory {
|
|
|
247
257
|
})
|
|
248
258
|
|
|
249
259
|
this.#deviceModel.on('error', (error) => {
|
|
250
|
-
|
|
260
|
+
const details = error instanceof Error ? (error.stack || error.message) : String(error)
|
|
261
|
+
this.#log.error(`[${this.#device.name}] Speaker device error:`, details)
|
|
251
262
|
})
|
|
252
263
|
}
|
|
253
264
|
|
|
@@ -284,7 +295,12 @@ export class YotoSpeakerAccessory {
|
|
|
284
295
|
*/
|
|
285
296
|
async getCurrentMediaState () {
|
|
286
297
|
const playbackStatus = this.#deviceModel.playback.playbackStatus ?? null
|
|
287
|
-
|
|
298
|
+
const current = this.getMediaStateValues(playbackStatus).current
|
|
299
|
+
this.#log.debug(
|
|
300
|
+
LOG_PREFIX.ACCESSORY,
|
|
301
|
+
`[${this.#device.name}] Get current media state -> ${current} (${playbackStatus ?? 'unknown'})`
|
|
302
|
+
)
|
|
303
|
+
return current
|
|
288
304
|
}
|
|
289
305
|
|
|
290
306
|
/**
|
|
@@ -293,7 +309,12 @@ export class YotoSpeakerAccessory {
|
|
|
293
309
|
*/
|
|
294
310
|
async getTargetMediaState () {
|
|
295
311
|
const playbackStatus = this.#deviceModel.playback.playbackStatus ?? null
|
|
296
|
-
|
|
312
|
+
const target = this.getMediaStateValues(playbackStatus).target
|
|
313
|
+
this.#log.debug(
|
|
314
|
+
LOG_PREFIX.ACCESSORY,
|
|
315
|
+
`[${this.#device.name}] Get target media state -> ${target} (${playbackStatus ?? 'unknown'})`
|
|
316
|
+
)
|
|
317
|
+
return target
|
|
297
318
|
}
|
|
298
319
|
|
|
299
320
|
/**
|
|
@@ -336,7 +357,12 @@ export class YotoSpeakerAccessory {
|
|
|
336
357
|
const volumeSteps = this.#deviceModel.status.volume
|
|
337
358
|
const normalizedSteps = Number.isFinite(volumeSteps) ? volumeSteps : 0
|
|
338
359
|
const clampedSteps = Math.max(0, Math.min(normalizedSteps, 16))
|
|
339
|
-
|
|
360
|
+
const percent = Math.round((clampedSteps / 16) * 100)
|
|
361
|
+
this.#log.debug(
|
|
362
|
+
LOG_PREFIX.ACCESSORY,
|
|
363
|
+
`[${this.#device.name}] Get speaker volume -> ${percent} (rawSteps=${volumeSteps})`
|
|
364
|
+
)
|
|
365
|
+
return percent
|
|
340
366
|
}
|
|
341
367
|
|
|
342
368
|
/**
|
|
@@ -379,7 +405,9 @@ export class YotoSpeakerAccessory {
|
|
|
379
405
|
* @returns {Promise<CharacteristicValue>}
|
|
380
406
|
*/
|
|
381
407
|
async getMute () {
|
|
382
|
-
|
|
408
|
+
const isMuted = this.#deviceModel.status.volume === 0
|
|
409
|
+
this.#log.debug(LOG_PREFIX.ACCESSORY, `[${this.#device.name}] Get speaker mute -> ${isMuted}`)
|
|
410
|
+
return isMuted
|
|
383
411
|
}
|
|
384
412
|
|
|
385
413
|
/**
|
|
@@ -404,7 +432,9 @@ export class YotoSpeakerAccessory {
|
|
|
404
432
|
* @returns {Promise<CharacteristicValue>}
|
|
405
433
|
*/
|
|
406
434
|
async getStatusActive () {
|
|
407
|
-
|
|
435
|
+
const isOnline = this.#deviceModel.status.isOnline
|
|
436
|
+
this.#log.debug(LOG_PREFIX.ACCESSORY, `[${this.#device.name}] Get speaker status active -> ${isOnline}`)
|
|
437
|
+
return isOnline
|
|
408
438
|
}
|
|
409
439
|
|
|
410
440
|
/**
|
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.42",
|
|
5
5
|
"author": "Bret Comnes <bcomnes@gmail.com> (https://bret.io)",
|
|
6
6
|
"bugs": {
|
|
7
7
|
"url": "https://github.com/bcomnes/homebridge-yoto/issues"
|