homebridge-yoto 0.0.40 → 0.0.41
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 +125 -42
- package/lib/platform.js +9 -2
- package/lib/speaker-accessory.js +2 -1
- 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) {
|
|
@@ -917,7 +921,8 @@ export class YotoPlayerAccessory {
|
|
|
917
921
|
})
|
|
918
922
|
|
|
919
923
|
this.#deviceModel.on('error', (error) => {
|
|
920
|
-
|
|
924
|
+
const details = error instanceof Error ? (error.stack || error.message) : String(error)
|
|
925
|
+
this.#log.error(`[${this.#device.name}] Device error:`, details)
|
|
921
926
|
})
|
|
922
927
|
}
|
|
923
928
|
|
|
@@ -1186,19 +1191,26 @@ export class YotoPlayerAccessory {
|
|
|
1186
1191
|
* @param {CharacteristicValue} value
|
|
1187
1192
|
*/
|
|
1188
1193
|
async setDayNightlightOn (value) {
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
this
|
|
1194
|
+
try {
|
|
1195
|
+
if (value) {
|
|
1196
|
+
// Turn ON - restore previous color or default to white
|
|
1197
|
+
const colorToSet = this.#lastDayColor || '0xffffff'
|
|
1198
|
+
this.#log.debug(LOG_PREFIX.ACCESSORY, `Turning day nightlight ON with color: ${colorToSet}`)
|
|
1199
|
+
await this.#deviceModel.updateConfig({ ambientColour: colorToSet })
|
|
1200
|
+
} else {
|
|
1201
|
+
// Turn OFF - save current color and set to black
|
|
1202
|
+
const currentColor = this.#deviceModel.config.ambientColour
|
|
1203
|
+
if (!this.isColorOff(currentColor)) {
|
|
1204
|
+
this.#lastDayColor = currentColor
|
|
1205
|
+
}
|
|
1206
|
+
this.#log.debug(LOG_PREFIX.ACCESSORY, 'Turning day nightlight OFF')
|
|
1207
|
+
await this.#deviceModel.updateConfig({ ambientColour: '0x000000' })
|
|
1199
1208
|
}
|
|
1200
|
-
|
|
1201
|
-
|
|
1209
|
+
} catch (error) {
|
|
1210
|
+
this.#log.error(LOG_PREFIX.ACCESSORY, `[${this.#device.name}] Failed to set day nightlight:`, error)
|
|
1211
|
+
throw new this.#platform.api.hap.HapStatusError(
|
|
1212
|
+
this.#platform.api.hap.HAPStatus.SERVICE_COMMUNICATION_FAILURE
|
|
1213
|
+
)
|
|
1202
1214
|
}
|
|
1203
1215
|
}
|
|
1204
1216
|
|
|
@@ -1228,10 +1240,17 @@ export class YotoPlayerAccessory {
|
|
|
1228
1240
|
|
|
1229
1241
|
const brightnessValue = Math.max(0, Math.min(Math.round(rawBrightness), 100))
|
|
1230
1242
|
this.#log.debug(LOG_PREFIX.ACCESSORY, `Setting day display brightness: ${brightnessValue}`)
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1243
|
+
try {
|
|
1244
|
+
await this.#deviceModel.updateConfig({
|
|
1245
|
+
dayDisplayBrightness: brightnessValue,
|
|
1246
|
+
dayDisplayBrightnessAuto: false
|
|
1247
|
+
})
|
|
1248
|
+
} catch (error) {
|
|
1249
|
+
this.#log.error(LOG_PREFIX.ACCESSORY, `[${this.#device.name}] Failed to set day brightness:`, error)
|
|
1250
|
+
throw new this.#platform.api.hap.HapStatusError(
|
|
1251
|
+
this.#platform.api.hap.HAPStatus.SERVICE_COMMUNICATION_FAILURE
|
|
1252
|
+
)
|
|
1253
|
+
}
|
|
1235
1254
|
}
|
|
1236
1255
|
|
|
1237
1256
|
/**
|
|
@@ -1275,7 +1294,14 @@ export class YotoPlayerAccessory {
|
|
|
1275
1294
|
const formattedColor = this.formatHexColor(newHex)
|
|
1276
1295
|
|
|
1277
1296
|
this.#log.debug(LOG_PREFIX.ACCESSORY, `Setting day nightlight hue: ${hue}° → ${formattedColor}`)
|
|
1278
|
-
|
|
1297
|
+
try {
|
|
1298
|
+
await this.#deviceModel.updateConfig({ ambientColour: formattedColor })
|
|
1299
|
+
} catch (error) {
|
|
1300
|
+
this.#log.error(LOG_PREFIX.ACCESSORY, `[${this.#device.name}] Failed to set day nightlight hue:`, error)
|
|
1301
|
+
throw new this.#platform.api.hap.HapStatusError(
|
|
1302
|
+
this.#platform.api.hap.HAPStatus.SERVICE_COMMUNICATION_FAILURE
|
|
1303
|
+
)
|
|
1304
|
+
}
|
|
1279
1305
|
}
|
|
1280
1306
|
|
|
1281
1307
|
/**
|
|
@@ -1319,7 +1345,14 @@ export class YotoPlayerAccessory {
|
|
|
1319
1345
|
const formattedColor = this.formatHexColor(newHex)
|
|
1320
1346
|
|
|
1321
1347
|
this.#log.debug(LOG_PREFIX.ACCESSORY, `Setting day nightlight saturation: ${saturation}% → ${formattedColor}`)
|
|
1322
|
-
|
|
1348
|
+
try {
|
|
1349
|
+
await this.#deviceModel.updateConfig({ ambientColour: formattedColor })
|
|
1350
|
+
} catch (error) {
|
|
1351
|
+
this.#log.error(LOG_PREFIX.ACCESSORY, `[${this.#device.name}] Failed to set day nightlight saturation:`, error)
|
|
1352
|
+
throw new this.#platform.api.hap.HapStatusError(
|
|
1353
|
+
this.#platform.api.hap.HAPStatus.SERVICE_COMMUNICATION_FAILURE
|
|
1354
|
+
)
|
|
1355
|
+
}
|
|
1323
1356
|
}
|
|
1324
1357
|
|
|
1325
1358
|
// ---------- Night Nightlight Handlers ----------
|
|
@@ -1338,19 +1371,26 @@ export class YotoPlayerAccessory {
|
|
|
1338
1371
|
* @param {CharacteristicValue} value
|
|
1339
1372
|
*/
|
|
1340
1373
|
async setNightNightlightOn (value) {
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
this
|
|
1374
|
+
try {
|
|
1375
|
+
if (value) {
|
|
1376
|
+
// Turn ON - restore previous color or default to white
|
|
1377
|
+
const colorToSet = this.#lastNightColor || '0xffffff'
|
|
1378
|
+
this.#log.debug(LOG_PREFIX.ACCESSORY, `Turning night nightlight ON with color: ${colorToSet}`)
|
|
1379
|
+
await this.#deviceModel.updateConfig({ nightAmbientColour: colorToSet })
|
|
1380
|
+
} else {
|
|
1381
|
+
// Turn OFF - save current color and set to black
|
|
1382
|
+
const currentColor = this.#deviceModel.config.nightAmbientColour
|
|
1383
|
+
if (!this.isColorOff(currentColor)) {
|
|
1384
|
+
this.#lastNightColor = currentColor
|
|
1385
|
+
}
|
|
1386
|
+
this.#log.debug(LOG_PREFIX.ACCESSORY, 'Turning night nightlight OFF')
|
|
1387
|
+
await this.#deviceModel.updateConfig({ nightAmbientColour: '0x000000' })
|
|
1351
1388
|
}
|
|
1352
|
-
|
|
1353
|
-
|
|
1389
|
+
} catch (error) {
|
|
1390
|
+
this.#log.error(LOG_PREFIX.ACCESSORY, `[${this.#device.name}] Failed to set night nightlight:`, error)
|
|
1391
|
+
throw new this.#platform.api.hap.HapStatusError(
|
|
1392
|
+
this.#platform.api.hap.HAPStatus.SERVICE_COMMUNICATION_FAILURE
|
|
1393
|
+
)
|
|
1354
1394
|
}
|
|
1355
1395
|
}
|
|
1356
1396
|
|
|
@@ -1380,10 +1420,17 @@ export class YotoPlayerAccessory {
|
|
|
1380
1420
|
|
|
1381
1421
|
const brightnessValue = Math.max(0, Math.min(Math.round(rawBrightness), 100))
|
|
1382
1422
|
this.#log.debug(LOG_PREFIX.ACCESSORY, `Setting night display brightness: ${brightnessValue}`)
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1423
|
+
try {
|
|
1424
|
+
await this.#deviceModel.updateConfig({
|
|
1425
|
+
nightDisplayBrightness: brightnessValue,
|
|
1426
|
+
nightDisplayBrightnessAuto: false
|
|
1427
|
+
})
|
|
1428
|
+
} catch (error) {
|
|
1429
|
+
this.#log.error(LOG_PREFIX.ACCESSORY, `[${this.#device.name}] Failed to set night brightness:`, error)
|
|
1430
|
+
throw new this.#platform.api.hap.HapStatusError(
|
|
1431
|
+
this.#platform.api.hap.HAPStatus.SERVICE_COMMUNICATION_FAILURE
|
|
1432
|
+
)
|
|
1433
|
+
}
|
|
1387
1434
|
}
|
|
1388
1435
|
|
|
1389
1436
|
/**
|
|
@@ -1427,7 +1474,14 @@ export class YotoPlayerAccessory {
|
|
|
1427
1474
|
const formattedColor = this.formatHexColor(newHex)
|
|
1428
1475
|
|
|
1429
1476
|
this.#log.debug(LOG_PREFIX.ACCESSORY, `Setting night nightlight hue: ${hue}° → ${formattedColor}`)
|
|
1430
|
-
|
|
1477
|
+
try {
|
|
1478
|
+
await this.#deviceModel.updateConfig({ nightAmbientColour: formattedColor })
|
|
1479
|
+
} catch (error) {
|
|
1480
|
+
this.#log.error(LOG_PREFIX.ACCESSORY, `[${this.#device.name}] Failed to set night nightlight hue:`, error)
|
|
1481
|
+
throw new this.#platform.api.hap.HapStatusError(
|
|
1482
|
+
this.#platform.api.hap.HAPStatus.SERVICE_COMMUNICATION_FAILURE
|
|
1483
|
+
)
|
|
1484
|
+
}
|
|
1431
1485
|
}
|
|
1432
1486
|
|
|
1433
1487
|
/**
|
|
@@ -1471,7 +1525,14 @@ export class YotoPlayerAccessory {
|
|
|
1471
1525
|
const formattedColor = this.formatHexColor(newHex)
|
|
1472
1526
|
|
|
1473
1527
|
this.#log.debug(LOG_PREFIX.ACCESSORY, `Setting night nightlight saturation: ${saturation}% → ${formattedColor}`)
|
|
1474
|
-
|
|
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 saturation:`, error)
|
|
1532
|
+
throw new this.#platform.api.hap.HapStatusError(
|
|
1533
|
+
this.#platform.api.hap.HAPStatus.SERVICE_COMMUNICATION_FAILURE
|
|
1534
|
+
)
|
|
1535
|
+
}
|
|
1475
1536
|
}
|
|
1476
1537
|
|
|
1477
1538
|
// ==================== Nightlight Status ContactSensor Getters ====================
|
|
@@ -1594,7 +1655,14 @@ export class YotoPlayerAccessory {
|
|
|
1594
1655
|
async setBluetoothState (value) {
|
|
1595
1656
|
const enabled = Boolean(value)
|
|
1596
1657
|
this.#log.debug(LOG_PREFIX.ACCESSORY, `Setting Bluetooth: ${enabled ? 'ON' : 'OFF'}`)
|
|
1597
|
-
|
|
1658
|
+
try {
|
|
1659
|
+
await this.#deviceModel.updateConfig({ bluetoothEnabled: enabled })
|
|
1660
|
+
} catch (error) {
|
|
1661
|
+
this.#log.error(LOG_PREFIX.ACCESSORY, `[${this.#device.name}] Failed to set Bluetooth:`, error)
|
|
1662
|
+
throw new this.#platform.api.hap.HapStatusError(
|
|
1663
|
+
this.#platform.api.hap.HAPStatus.SERVICE_COMMUNICATION_FAILURE
|
|
1664
|
+
)
|
|
1665
|
+
}
|
|
1598
1666
|
}
|
|
1599
1667
|
|
|
1600
1668
|
// ==================== Card Control Switch Setter ====================
|
|
@@ -1683,7 +1751,14 @@ export class YotoPlayerAccessory {
|
|
|
1683
1751
|
LOG_PREFIX.ACCESSORY,
|
|
1684
1752
|
`[${this.#device.name}] Set day max volume limit raw=${value} normalizedPercent=${normalizedPercent} requestedSteps=${requestedSteps} -> steps=${limit} percent=${limitPercent}`
|
|
1685
1753
|
)
|
|
1686
|
-
|
|
1754
|
+
try {
|
|
1755
|
+
await this.#deviceModel.updateConfig({ maxVolumeLimit: limit })
|
|
1756
|
+
} catch (error) {
|
|
1757
|
+
this.#log.error(LOG_PREFIX.ACCESSORY, `[${this.#device.name}] Failed to set day max volume limit:`, error)
|
|
1758
|
+
throw new this.#platform.api.hap.HapStatusError(
|
|
1759
|
+
this.#platform.api.hap.HAPStatus.SERVICE_COMMUNICATION_FAILURE
|
|
1760
|
+
)
|
|
1761
|
+
}
|
|
1687
1762
|
}
|
|
1688
1763
|
|
|
1689
1764
|
/**
|
|
@@ -1722,7 +1797,14 @@ export class YotoPlayerAccessory {
|
|
|
1722
1797
|
LOG_PREFIX.ACCESSORY,
|
|
1723
1798
|
`[${this.#device.name}] Set night max volume limit raw=${value} normalizedPercent=${normalizedPercent} requestedSteps=${requestedSteps} -> steps=${limit} percent=${limitPercent}`
|
|
1724
1799
|
)
|
|
1725
|
-
|
|
1800
|
+
try {
|
|
1801
|
+
await this.#deviceModel.updateConfig({ nightMaxVolumeLimit: limit })
|
|
1802
|
+
} catch (error) {
|
|
1803
|
+
this.#log.error(LOG_PREFIX.ACCESSORY, `[${this.#device.name}] Failed to set night max volume limit:`, error)
|
|
1804
|
+
throw new this.#platform.api.hap.HapStatusError(
|
|
1805
|
+
this.#platform.api.hap.HAPStatus.SERVICE_COMMUNICATION_FAILURE
|
|
1806
|
+
)
|
|
1807
|
+
}
|
|
1726
1808
|
}
|
|
1727
1809
|
|
|
1728
1810
|
// ==================== Characteristic Update Methods ====================
|
|
@@ -1746,6 +1828,8 @@ export class YotoPlayerAccessory {
|
|
|
1746
1828
|
* @param {number} volumeSteps - Volume level (0-16)
|
|
1747
1829
|
*/
|
|
1748
1830
|
updateVolumeCharacteristic (volumeSteps) {
|
|
1831
|
+
if (!this.volumeService) return
|
|
1832
|
+
|
|
1749
1833
|
if (volumeSteps > 0) {
|
|
1750
1834
|
this.#lastNonZeroVolume = Math.round((volumeSteps / 16) * 100)
|
|
1751
1835
|
}
|
|
@@ -1757,7 +1841,6 @@ export class YotoPlayerAccessory {
|
|
|
1757
1841
|
LOG_PREFIX.ACCESSORY,
|
|
1758
1842
|
`[${this.#device.name}] Update volume characteristic rawSteps=${volumeSteps} percent=${percent}`
|
|
1759
1843
|
)
|
|
1760
|
-
if (!this.volumeService) return
|
|
1761
1844
|
|
|
1762
1845
|
const { Characteristic } = this.#platform
|
|
1763
1846
|
this.volumeService
|
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
|
|
package/lib/speaker-accessory.js
CHANGED
|
@@ -247,7 +247,8 @@ export class YotoSpeakerAccessory {
|
|
|
247
247
|
})
|
|
248
248
|
|
|
249
249
|
this.#deviceModel.on('error', (error) => {
|
|
250
|
-
|
|
250
|
+
const details = error instanceof Error ? (error.stack || error.message) : String(error)
|
|
251
|
+
this.#log.error(`[${this.#device.name}] Speaker device error:`, details)
|
|
251
252
|
})
|
|
252
253
|
}
|
|
253
254
|
|
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.41",
|
|
5
5
|
"author": "Bret Comnes <bcomnes@gmail.com> (https://bret.io)",
|
|
6
6
|
"bugs": {
|
|
7
7
|
"url": "https://github.com/bcomnes/homebridge-yoto/issues"
|