homebridge-yoto 0.0.36 → 0.0.38
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/lib/accessory.js +104 -69
- package/lib/platform.js +46 -18
- package/lib/sync-service-names.js +10 -7
- package/package.json +1 -1
package/lib/accessory.js
CHANGED
|
@@ -92,8 +92,8 @@ export class YotoPlayerAccessory {
|
|
|
92
92
|
/** @type {Service | undefined} */ bluetoothService
|
|
93
93
|
/** @type {Service | undefined} */ dayMaxVolumeService
|
|
94
94
|
/** @type {Service | undefined} */ nightMaxVolumeService
|
|
95
|
-
// Volume state for mute/unmute (0-
|
|
96
|
-
/** @type {number} */ #lastNonZeroVolume =
|
|
95
|
+
// Volume state for mute/unmute (0-100 percent)
|
|
96
|
+
/** @type {number} */ #lastNonZeroVolume = 50
|
|
97
97
|
// Nightlight color state for restore-on-ON
|
|
98
98
|
/** @type {string} */ #lastDayColor = '0xffffff'
|
|
99
99
|
/** @type {string} */ #lastNightColor = '0xffffff'
|
|
@@ -129,15 +129,15 @@ export class YotoPlayerAccessory {
|
|
|
129
129
|
: {}
|
|
130
130
|
|
|
131
131
|
return {
|
|
132
|
-
playback: getBooleanSetting(serviceConfig
|
|
133
|
-
volume: getBooleanSetting(serviceConfig
|
|
134
|
-
temperature: getBooleanSetting(serviceConfig
|
|
135
|
-
nightlight: getBooleanSetting(serviceConfig
|
|
136
|
-
cardSlot: getBooleanSetting(serviceConfig
|
|
137
|
-
nightMode: getBooleanSetting(serviceConfig
|
|
138
|
-
sleepTimer: getBooleanSetting(serviceConfig
|
|
139
|
-
bluetooth: getBooleanSetting(serviceConfig
|
|
140
|
-
volumeLimits: getBooleanSetting(serviceConfig
|
|
132
|
+
playback: getBooleanSetting(serviceConfig['playback'], getServiceDefault('playback')),
|
|
133
|
+
volume: getBooleanSetting(serviceConfig['volume'], getServiceDefault('volume')),
|
|
134
|
+
temperature: getBooleanSetting(serviceConfig['temperature'], getServiceDefault('temperature')),
|
|
135
|
+
nightlight: getBooleanSetting(serviceConfig['nightlight'], getServiceDefault('nightlight')),
|
|
136
|
+
cardSlot: getBooleanSetting(serviceConfig['cardSlot'], getServiceDefault('cardSlot')),
|
|
137
|
+
nightMode: getBooleanSetting(serviceConfig['nightMode'], getServiceDefault('nightMode')),
|
|
138
|
+
sleepTimer: getBooleanSetting(serviceConfig['sleepTimer'], getServiceDefault('sleepTimer')),
|
|
139
|
+
bluetooth: getBooleanSetting(serviceConfig['bluetooth'], getServiceDefault('bluetooth')),
|
|
140
|
+
volumeLimits: getBooleanSetting(serviceConfig['volumeLimits'], getServiceDefault('volumeLimits')),
|
|
141
141
|
}
|
|
142
142
|
}
|
|
143
143
|
|
|
@@ -325,7 +325,7 @@ export class YotoPlayerAccessory {
|
|
|
325
325
|
.getCharacteristic(Characteristic.Brightness)
|
|
326
326
|
.setProps({
|
|
327
327
|
minValue: 0,
|
|
328
|
-
maxValue:
|
|
328
|
+
maxValue: 100,
|
|
329
329
|
minStep: 1,
|
|
330
330
|
})
|
|
331
331
|
.onGet(this.getVolume.bind(this))
|
|
@@ -598,7 +598,7 @@ export class YotoPlayerAccessory {
|
|
|
598
598
|
|
|
599
599
|
dayService
|
|
600
600
|
.getCharacteristic(Characteristic.Brightness)
|
|
601
|
-
.setProps({ minValue: 0, maxValue:
|
|
601
|
+
.setProps({ minValue: 0, maxValue: 100, minStep: 1 })
|
|
602
602
|
.onGet(this.getDayMaxVolume.bind(this))
|
|
603
603
|
.onSet(this.setDayMaxVolume.bind(this))
|
|
604
604
|
|
|
@@ -622,7 +622,7 @@ export class YotoPlayerAccessory {
|
|
|
622
622
|
|
|
623
623
|
nightService
|
|
624
624
|
.getCharacteristic(Characteristic.Brightness)
|
|
625
|
-
.setProps({ minValue: 0, maxValue:
|
|
625
|
+
.setProps({ minValue: 0, maxValue: 100, minStep: 1 })
|
|
626
626
|
.onGet(this.getNightMaxVolume.bind(this))
|
|
627
627
|
.onSet(this.setNightMaxVolume.bind(this))
|
|
628
628
|
|
|
@@ -663,10 +663,6 @@ export class YotoPlayerAccessory {
|
|
|
663
663
|
this.updateFirmwareVersionCharacteristic(status.firmwareVersion)
|
|
664
664
|
break
|
|
665
665
|
|
|
666
|
-
case 'maxVolume':
|
|
667
|
-
this.updateVolumeLimitProps(status.maxVolume)
|
|
668
|
-
break
|
|
669
|
-
|
|
670
666
|
case 'temperatureCelsius':
|
|
671
667
|
if (this.#deviceModel.capabilities.hasTemperatureSensor && status.temperatureCelsius !== null) {
|
|
672
668
|
this.updateTemperatureCharacteristic(status.temperatureCelsius)
|
|
@@ -687,8 +683,6 @@ export class YotoPlayerAccessory {
|
|
|
687
683
|
if (this.#deviceModel.capabilities.hasColoredNightlight) {
|
|
688
684
|
this.updateNightlightStatusCharacteristics()
|
|
689
685
|
}
|
|
690
|
-
// Day/night mode affects which volume limit is active
|
|
691
|
-
this.updateVolumeLimitProps(status.maxVolume)
|
|
692
686
|
break
|
|
693
687
|
|
|
694
688
|
case 'cardInsertionState':
|
|
@@ -697,6 +691,7 @@ export class YotoPlayerAccessory {
|
|
|
697
691
|
|
|
698
692
|
// Available but not yet mapped to characteristics
|
|
699
693
|
case 'activeCardId':
|
|
694
|
+
case 'maxVolume':
|
|
700
695
|
case 'powerSource':
|
|
701
696
|
case 'wifiStrength':
|
|
702
697
|
case 'freeDiskSpaceBytes':
|
|
@@ -873,12 +868,12 @@ export class YotoPlayerAccessory {
|
|
|
873
868
|
})
|
|
874
869
|
|
|
875
870
|
// Lifecycle events
|
|
876
|
-
this.#deviceModel.on('online', ({ reason }) => {
|
|
871
|
+
this.#deviceModel.on('online', ({ reason: _reason }) => {
|
|
877
872
|
// Platform logs online/offline events to avoid duplicate output.
|
|
878
873
|
this.updateOnlineStatusCharacteristic(true)
|
|
879
874
|
})
|
|
880
875
|
|
|
881
|
-
this.#deviceModel.on('offline', ({ reason }) => {
|
|
876
|
+
this.#deviceModel.on('offline', ({ reason: _reason }) => {
|
|
882
877
|
// Platform logs online/offline events to avoid duplicate output.
|
|
883
878
|
this.updateOnlineStatusCharacteristic(false)
|
|
884
879
|
})
|
|
@@ -922,15 +917,23 @@ export class YotoPlayerAccessory {
|
|
|
922
917
|
}
|
|
923
918
|
|
|
924
919
|
/**
|
|
925
|
-
* Get volume level (0-16 steps)
|
|
920
|
+
* Get volume level as percentage (mapped from 0-16 steps)
|
|
926
921
|
* @returns {Promise<CharacteristicValue>}
|
|
927
922
|
*/
|
|
928
923
|
async getVolume () {
|
|
929
|
-
|
|
924
|
+
const volumeSteps = this.#deviceModel.status.volume
|
|
925
|
+
const normalizedSteps = Number.isFinite(volumeSteps) ? volumeSteps : 0
|
|
926
|
+
const clampedSteps = Math.max(0, Math.min(normalizedSteps, 16))
|
|
927
|
+
const percent = Math.round((clampedSteps / 16) * 100)
|
|
928
|
+
this.#log.debug(
|
|
929
|
+
LOG_PREFIX.ACCESSORY,
|
|
930
|
+
`[${this.#device.name}] Get volume rawSteps=${volumeSteps} steps=${clampedSteps} percent=${percent}`
|
|
931
|
+
)
|
|
932
|
+
return percent
|
|
930
933
|
}
|
|
931
934
|
|
|
932
935
|
/**
|
|
933
|
-
* Set volume level (0-16 steps)
|
|
936
|
+
* Set volume level as percentage (mapped to 0-16 steps)
|
|
934
937
|
* @param {CharacteristicValue} value
|
|
935
938
|
* @returns {Promise<void>}
|
|
936
939
|
*/
|
|
@@ -938,21 +941,28 @@ export class YotoPlayerAccessory {
|
|
|
938
941
|
const deviceModel = this.#deviceModel
|
|
939
942
|
this.#log.debug(LOG_PREFIX.ACCESSORY, `[${this.#device.name}] Set volume:`, value)
|
|
940
943
|
|
|
941
|
-
const
|
|
942
|
-
if (!Number.isFinite(
|
|
944
|
+
const requestedPercent = typeof value === 'number' ? value : Number(value)
|
|
945
|
+
if (!Number.isFinite(requestedPercent)) {
|
|
943
946
|
throw new this.#platform.api.hap.HapStatusError(
|
|
944
947
|
this.#platform.api.hap.HAPStatus.INVALID_VALUE_IN_REQUEST
|
|
945
948
|
)
|
|
946
949
|
}
|
|
947
950
|
|
|
951
|
+
const normalizedPercent = Math.max(0, Math.min(Math.round(requestedPercent), 100))
|
|
952
|
+
const requestedSteps = Math.round((normalizedPercent / 100) * 16)
|
|
948
953
|
const maxVolumeSteps = Number.isFinite(deviceModel.status.maxVolume)
|
|
949
954
|
? deviceModel.status.maxVolume
|
|
950
955
|
: 16
|
|
951
956
|
const steps = Math.max(0, Math.min(Math.round(requestedSteps), maxVolumeSteps))
|
|
957
|
+
const resultPercent = Math.round((steps / 16) * 100)
|
|
958
|
+
this.#log.debug(
|
|
959
|
+
LOG_PREFIX.ACCESSORY,
|
|
960
|
+
`[${this.#device.name}] Set volume raw=${value} normalizedPercent=${normalizedPercent} requestedSteps=${requestedSteps} -> steps=${steps} percent=${resultPercent} (maxSteps=${maxVolumeSteps})`
|
|
961
|
+
)
|
|
952
962
|
|
|
953
963
|
// Track last non-zero volume for unmute
|
|
954
964
|
if (steps > 0) {
|
|
955
|
-
this.#lastNonZeroVolume = steps
|
|
965
|
+
this.#lastNonZeroVolume = Math.round((steps / 16) * 100)
|
|
956
966
|
}
|
|
957
967
|
|
|
958
968
|
try {
|
|
@@ -964,10 +974,11 @@ export class YotoPlayerAccessory {
|
|
|
964
974
|
.getCharacteristic(Characteristic.On)
|
|
965
975
|
.updateValue(steps > 0)
|
|
966
976
|
|
|
967
|
-
if (steps !== requestedSteps) {
|
|
977
|
+
if (steps !== requestedSteps || normalizedPercent !== requestedPercent) {
|
|
978
|
+
const clampedPercent = Math.round((steps / 16) * 100)
|
|
968
979
|
this.volumeService
|
|
969
980
|
.getCharacteristic(Characteristic.Brightness)
|
|
970
|
-
.updateValue(
|
|
981
|
+
.updateValue(clampedPercent)
|
|
971
982
|
}
|
|
972
983
|
}
|
|
973
984
|
} catch (error) {
|
|
@@ -1555,54 +1566,80 @@ export class YotoPlayerAccessory {
|
|
|
1555
1566
|
// ==================== Volume Limit Lightbulb Getters/Setters ====================
|
|
1556
1567
|
|
|
1557
1568
|
/**
|
|
1558
|
-
* Get day max volume limit
|
|
1569
|
+
* Get day max volume limit as percentage (mapped from 0-16 steps)
|
|
1559
1570
|
* @returns {Promise<CharacteristicValue>}
|
|
1560
1571
|
*/
|
|
1561
1572
|
async getDayMaxVolume () {
|
|
1562
1573
|
const limit = this.#deviceModel.config.maxVolumeLimit
|
|
1563
|
-
|
|
1574
|
+
const steps = Number.isFinite(limit) ? limit : 16
|
|
1575
|
+
const clampedSteps = Math.max(0, Math.min(steps, 16))
|
|
1576
|
+
const percent = Math.round((clampedSteps / 16) * 100)
|
|
1577
|
+
this.#log.debug(
|
|
1578
|
+
LOG_PREFIX.ACCESSORY,
|
|
1579
|
+
`[${this.#device.name}] Get day max volume limit rawSteps=${limit} steps=${clampedSteps} percent=${percent}`
|
|
1580
|
+
)
|
|
1581
|
+
return percent
|
|
1564
1582
|
}
|
|
1565
1583
|
|
|
1566
1584
|
/**
|
|
1567
|
-
* Set day max volume limit
|
|
1585
|
+
* Set day max volume limit as percentage (mapped to 0-16 steps)
|
|
1568
1586
|
* @param {CharacteristicValue} value
|
|
1569
1587
|
*/
|
|
1570
1588
|
async setDayMaxVolume (value) {
|
|
1571
|
-
const
|
|
1572
|
-
if (!Number.isFinite(
|
|
1589
|
+
const requestedPercent = typeof value === 'number' ? value : Number(value)
|
|
1590
|
+
if (!Number.isFinite(requestedPercent)) {
|
|
1573
1591
|
throw new this.#platform.api.hap.HapStatusError(
|
|
1574
1592
|
this.#platform.api.hap.HAPStatus.INVALID_VALUE_IN_REQUEST
|
|
1575
1593
|
)
|
|
1576
1594
|
}
|
|
1577
1595
|
|
|
1578
|
-
const
|
|
1579
|
-
|
|
1596
|
+
const normalizedPercent = Math.max(0, Math.min(Math.round(requestedPercent), 100))
|
|
1597
|
+
const requestedSteps = Math.round((normalizedPercent / 100) * 16)
|
|
1598
|
+
const limit = Math.max(0, Math.min(Math.round(requestedSteps), 16))
|
|
1599
|
+
const limitPercent = Math.round((limit / 16) * 100)
|
|
1600
|
+
this.#log.debug(
|
|
1601
|
+
LOG_PREFIX.ACCESSORY,
|
|
1602
|
+
`[${this.#device.name}] Set day max volume limit raw=${value} normalizedPercent=${normalizedPercent} requestedSteps=${requestedSteps} -> steps=${limit} percent=${limitPercent}`
|
|
1603
|
+
)
|
|
1580
1604
|
await this.#deviceModel.updateConfig({ maxVolumeLimit: limit })
|
|
1581
1605
|
}
|
|
1582
1606
|
|
|
1583
1607
|
/**
|
|
1584
|
-
* Get night max volume limit
|
|
1608
|
+
* Get night max volume limit as percentage (mapped from 0-16 steps)
|
|
1585
1609
|
* @returns {Promise<CharacteristicValue>}
|
|
1586
1610
|
*/
|
|
1587
1611
|
async getNightMaxVolume () {
|
|
1588
1612
|
const limit = this.#deviceModel.config.nightMaxVolumeLimit
|
|
1589
|
-
|
|
1613
|
+
const steps = Number.isFinite(limit) ? limit : 10
|
|
1614
|
+
const clampedSteps = Math.max(0, Math.min(steps, 16))
|
|
1615
|
+
const percent = Math.round((clampedSteps / 16) * 100)
|
|
1616
|
+
this.#log.debug(
|
|
1617
|
+
LOG_PREFIX.ACCESSORY,
|
|
1618
|
+
`[${this.#device.name}] Get night max volume limit rawSteps=${limit} steps=${clampedSteps} percent=${percent}`
|
|
1619
|
+
)
|
|
1620
|
+
return percent
|
|
1590
1621
|
}
|
|
1591
1622
|
|
|
1592
1623
|
/**
|
|
1593
|
-
* Set night max volume limit
|
|
1624
|
+
* Set night max volume limit as percentage (mapped to 0-16 steps)
|
|
1594
1625
|
* @param {CharacteristicValue} value
|
|
1595
1626
|
*/
|
|
1596
1627
|
async setNightMaxVolume (value) {
|
|
1597
|
-
const
|
|
1598
|
-
if (!Number.isFinite(
|
|
1628
|
+
const requestedPercent = typeof value === 'number' ? value : Number(value)
|
|
1629
|
+
if (!Number.isFinite(requestedPercent)) {
|
|
1599
1630
|
throw new this.#platform.api.hap.HapStatusError(
|
|
1600
1631
|
this.#platform.api.hap.HAPStatus.INVALID_VALUE_IN_REQUEST
|
|
1601
1632
|
)
|
|
1602
1633
|
}
|
|
1603
1634
|
|
|
1604
|
-
const
|
|
1605
|
-
|
|
1635
|
+
const normalizedPercent = Math.max(0, Math.min(Math.round(requestedPercent), 100))
|
|
1636
|
+
const requestedSteps = Math.round((normalizedPercent / 100) * 16)
|
|
1637
|
+
const limit = Math.max(0, Math.min(Math.round(requestedSteps), 16))
|
|
1638
|
+
const limitPercent = Math.round((limit / 16) * 100)
|
|
1639
|
+
this.#log.debug(
|
|
1640
|
+
LOG_PREFIX.ACCESSORY,
|
|
1641
|
+
`[${this.#device.name}] Set night max volume limit raw=${value} normalizedPercent=${normalizedPercent} requestedSteps=${requestedSteps} -> steps=${limit} percent=${limitPercent}`
|
|
1642
|
+
)
|
|
1606
1643
|
await this.#deviceModel.updateConfig({ nightMaxVolumeLimit: limit })
|
|
1607
1644
|
}
|
|
1608
1645
|
|
|
@@ -1631,33 +1668,19 @@ export class YotoPlayerAccessory {
|
|
|
1631
1668
|
|
|
1632
1669
|
const { Characteristic } = this.#platform
|
|
1633
1670
|
if (volumeSteps > 0) {
|
|
1634
|
-
this.#lastNonZeroVolume = volumeSteps
|
|
1671
|
+
this.#lastNonZeroVolume = Math.round((volumeSteps / 16) * 100)
|
|
1635
1672
|
}
|
|
1636
1673
|
|
|
1674
|
+
const normalizedVolume = Number.isFinite(volumeSteps) ? volumeSteps : 0
|
|
1675
|
+
const clampedVolume = Math.max(0, Math.min(normalizedVolume, 16))
|
|
1676
|
+
const percent = Math.round((clampedVolume / 16) * 100)
|
|
1677
|
+
this.#log.debug(
|
|
1678
|
+
LOG_PREFIX.ACCESSORY,
|
|
1679
|
+
`[${this.#device.name}] Update volume characteristic rawSteps=${volumeSteps} steps=${clampedVolume} percent=${percent}`
|
|
1680
|
+
)
|
|
1637
1681
|
this.volumeService
|
|
1638
1682
|
.getCharacteristic(Characteristic.Brightness)
|
|
1639
|
-
.updateValue(
|
|
1640
|
-
}
|
|
1641
|
-
|
|
1642
|
-
/**
|
|
1643
|
-
* Update volume limit props - adjusts max value based on day/night mode
|
|
1644
|
-
* @param {number} maxVolume - Maximum volume limit (0-16)
|
|
1645
|
-
*/
|
|
1646
|
-
updateVolumeLimitProps (maxVolume) {
|
|
1647
|
-
if (!this.volumeService) return
|
|
1648
|
-
|
|
1649
|
-
const { Characteristic } = this.#platform
|
|
1650
|
-
const maxVolumeSteps = Number.isFinite(maxVolume) ? maxVolume : 16
|
|
1651
|
-
const clampedMaxVolume = Math.max(0, Math.min(maxVolumeSteps, 16))
|
|
1652
|
-
this.volumeService
|
|
1653
|
-
.getCharacteristic(Characteristic.Brightness)
|
|
1654
|
-
.setProps({
|
|
1655
|
-
minValue: 0,
|
|
1656
|
-
maxValue: clampedMaxVolume,
|
|
1657
|
-
minStep: 1,
|
|
1658
|
-
})
|
|
1659
|
-
|
|
1660
|
-
this.#log.debug(`[${this.#device.name}] Volume max is ${clampedMaxVolume}/16`)
|
|
1683
|
+
.updateValue(percent)
|
|
1661
1684
|
}
|
|
1662
1685
|
|
|
1663
1686
|
/**
|
|
@@ -1920,16 +1943,28 @@ export class YotoPlayerAccessory {
|
|
|
1920
1943
|
|
|
1921
1944
|
if (this.dayMaxVolumeService) {
|
|
1922
1945
|
const limit = Number.isFinite(config.maxVolumeLimit) ? config.maxVolumeLimit : 16
|
|
1946
|
+
const clampedLimit = Math.max(0, Math.min(limit, 16))
|
|
1947
|
+
const percent = Math.round((clampedLimit / 16) * 100)
|
|
1948
|
+
this.#log.debug(
|
|
1949
|
+
LOG_PREFIX.ACCESSORY,
|
|
1950
|
+
`[${this.#device.name}] Update day max volume characteristic rawSteps=${limit} steps=${clampedLimit} percent=${percent}`
|
|
1951
|
+
)
|
|
1923
1952
|
this.dayMaxVolumeService
|
|
1924
1953
|
.getCharacteristic(Characteristic.Brightness)
|
|
1925
|
-
.updateValue(
|
|
1954
|
+
.updateValue(percent)
|
|
1926
1955
|
}
|
|
1927
1956
|
|
|
1928
1957
|
if (this.nightMaxVolumeService) {
|
|
1929
1958
|
const limit = Number.isFinite(config.nightMaxVolumeLimit) ? config.nightMaxVolumeLimit : 10
|
|
1959
|
+
const clampedLimit = Math.max(0, Math.min(limit, 16))
|
|
1960
|
+
const percent = Math.round((clampedLimit / 16) * 100)
|
|
1961
|
+
this.#log.debug(
|
|
1962
|
+
LOG_PREFIX.ACCESSORY,
|
|
1963
|
+
`[${this.#device.name}] Update night max volume characteristic rawSteps=${limit} steps=${clampedLimit} percent=${percent}`
|
|
1964
|
+
)
|
|
1930
1965
|
this.nightMaxVolumeService
|
|
1931
1966
|
.getCharacteristic(Characteristic.Brightness)
|
|
1932
|
-
.updateValue(
|
|
1967
|
+
.updateValue(percent)
|
|
1933
1968
|
}
|
|
1934
1969
|
}
|
|
1935
1970
|
|
package/lib/platform.js
CHANGED
|
@@ -70,7 +70,7 @@ export class YotoPlatform {
|
|
|
70
70
|
return
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
-
log.
|
|
73
|
+
log.debug('Authentication tokens found, initializing Yoto account...')
|
|
74
74
|
|
|
75
75
|
const { updateHomebridgeConfig, sessionId } = this
|
|
76
76
|
|
|
@@ -158,7 +158,7 @@ export class YotoPlatform {
|
|
|
158
158
|
}
|
|
159
159
|
|
|
160
160
|
try {
|
|
161
|
-
this.log.
|
|
161
|
+
this.log.debug('Starting Yoto account...')
|
|
162
162
|
|
|
163
163
|
// Listen for devices being added
|
|
164
164
|
this.yotoAccount.on('deviceAdded', async ({ deviceId }) => {
|
|
@@ -188,68 +188,96 @@ export class YotoPlatform {
|
|
|
188
188
|
this.log.info(`Device offline: ${deviceId}${reason}`)
|
|
189
189
|
})
|
|
190
190
|
|
|
191
|
+
const getDeviceLabel = (deviceId) => {
|
|
192
|
+
const deviceName = this.yotoAccount?.getDevice(deviceId)?.device?.name
|
|
193
|
+
return deviceName || deviceId
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const formatLegacyStatusFields = (message) => {
|
|
197
|
+
const status = message?.status
|
|
198
|
+
if (!status || typeof status !== 'object') return ''
|
|
199
|
+
const fields = Object.keys(status)
|
|
200
|
+
if (!fields.length) return ''
|
|
201
|
+
const preview = fields.slice(0, 8).join(', ')
|
|
202
|
+
const suffix = fields.length > 8 ? `, +${fields.length - 8} more` : ''
|
|
203
|
+
return ` fields: ${preview}${suffix}`
|
|
204
|
+
}
|
|
205
|
+
|
|
191
206
|
this.yotoAccount.on('statusUpdate', ({ deviceId, source, changedFields }) => {
|
|
207
|
+
const label = getDeviceLabel(deviceId)
|
|
192
208
|
const fields = Array.from(changedFields).join(', ')
|
|
193
|
-
this.log.debug(`Status update [${
|
|
209
|
+
this.log.debug(`Status update [${label} ${source}]: ${fields}`)
|
|
194
210
|
})
|
|
195
211
|
|
|
196
212
|
this.yotoAccount.on('configUpdate', ({ deviceId, changedFields }) => {
|
|
213
|
+
const label = getDeviceLabel(deviceId)
|
|
197
214
|
const fields = Array.from(changedFields).join(', ')
|
|
198
|
-
this.log.debug(`Config update [${
|
|
215
|
+
this.log.debug(`Config update [${label}]: ${fields}`)
|
|
199
216
|
})
|
|
200
217
|
|
|
201
218
|
this.yotoAccount.on('playbackUpdate', ({ deviceId, changedFields }) => {
|
|
219
|
+
const label = getDeviceLabel(deviceId)
|
|
202
220
|
const fields = Array.from(changedFields).join(', ')
|
|
203
|
-
this.log.debug(`Playback update [${
|
|
221
|
+
this.log.debug(`Playback update [${label}]: ${fields}`)
|
|
204
222
|
})
|
|
205
223
|
|
|
206
224
|
this.yotoAccount.on('mqttConnect', ({ deviceId }) => {
|
|
207
|
-
const
|
|
208
|
-
const label = deviceName ? `${deviceName} (${deviceId})` : deviceId
|
|
225
|
+
const label = getDeviceLabel(deviceId)
|
|
209
226
|
this.log.debug(`MQTT connected: ${label}`)
|
|
210
227
|
})
|
|
211
228
|
|
|
212
229
|
this.yotoAccount.on('mqttDisconnect', ({ deviceId, metadata }) => {
|
|
230
|
+
const label = getDeviceLabel(deviceId)
|
|
213
231
|
const reasonCode = metadata?.packet?.reasonCode
|
|
214
232
|
const reason = typeof reasonCode === 'number' ? ` (code ${reasonCode})` : ''
|
|
215
|
-
this.log.warn(`MQTT disconnected: ${
|
|
233
|
+
this.log.warn(`MQTT disconnected: ${label}${reason}`)
|
|
216
234
|
})
|
|
217
235
|
|
|
218
236
|
this.yotoAccount.on('mqttClose', ({ deviceId, metadata }) => {
|
|
237
|
+
const label = getDeviceLabel(deviceId)
|
|
219
238
|
const reason = metadata?.reason ? ` (${metadata.reason})` : ''
|
|
220
|
-
this.log.debug(`MQTT closed: ${
|
|
239
|
+
this.log.debug(`MQTT closed: ${label}${reason}`)
|
|
221
240
|
})
|
|
222
241
|
|
|
223
242
|
this.yotoAccount.on('mqttReconnect', ({ deviceId }) => {
|
|
224
|
-
|
|
243
|
+
const label = getDeviceLabel(deviceId)
|
|
244
|
+
this.log.debug(`MQTT reconnecting: ${label}`)
|
|
225
245
|
})
|
|
226
246
|
|
|
227
247
|
this.yotoAccount.on('mqttOffline', ({ deviceId }) => {
|
|
228
|
-
|
|
248
|
+
const label = getDeviceLabel(deviceId)
|
|
249
|
+
this.log.debug(`MQTT offline: ${label}`)
|
|
229
250
|
})
|
|
230
251
|
|
|
231
252
|
this.yotoAccount.on('mqttEnd', ({ deviceId }) => {
|
|
232
|
-
|
|
253
|
+
const label = getDeviceLabel(deviceId)
|
|
254
|
+
this.log.debug(`MQTT ended: ${label}`)
|
|
233
255
|
})
|
|
234
256
|
|
|
235
257
|
this.yotoAccount.on('mqttStatus', ({ deviceId, topic }) => {
|
|
236
|
-
|
|
258
|
+
const label = getDeviceLabel(deviceId)
|
|
259
|
+
this.log.debug(`MQTT status [${label}]: ${topic}`)
|
|
237
260
|
})
|
|
238
261
|
|
|
239
262
|
this.yotoAccount.on('mqttEvents', ({ deviceId, topic }) => {
|
|
240
|
-
|
|
263
|
+
const label = getDeviceLabel(deviceId)
|
|
264
|
+
this.log.debug(`MQTT events [${label}]: ${topic}`)
|
|
241
265
|
})
|
|
242
266
|
|
|
243
|
-
this.yotoAccount.on('mqttStatusLegacy', ({ deviceId, topic }) => {
|
|
244
|
-
|
|
267
|
+
this.yotoAccount.on('mqttStatusLegacy', ({ deviceId, topic, message }) => {
|
|
268
|
+
const label = getDeviceLabel(deviceId)
|
|
269
|
+
const fields = formatLegacyStatusFields(message)
|
|
270
|
+
this.log.debug(`MQTT legacy status [${label}]: ${topic}${fields}`)
|
|
245
271
|
})
|
|
246
272
|
|
|
247
273
|
this.yotoAccount.on('mqttResponse', ({ deviceId, topic }) => {
|
|
248
|
-
|
|
274
|
+
const label = getDeviceLabel(deviceId)
|
|
275
|
+
this.log.debug(`MQTT response [${label}]: ${topic}`)
|
|
249
276
|
})
|
|
250
277
|
|
|
251
278
|
this.yotoAccount.on('mqttUnknown', ({ deviceId, topic }) => {
|
|
252
|
-
|
|
279
|
+
const label = getDeviceLabel(deviceId)
|
|
280
|
+
this.log.debug(`MQTT unknown [${label}]: ${topic}`)
|
|
253
281
|
})
|
|
254
282
|
|
|
255
283
|
// Start the account (discovers devices, creates device models, starts MQTT)
|
|
@@ -22,13 +22,16 @@ export function syncServiceNames ({
|
|
|
22
22
|
|
|
23
23
|
service.updateCharacteristic(Characteristic.Name, sanitizedName)
|
|
24
24
|
|
|
25
|
-
//
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
25
|
+
// Add ConfiguredName when missing so we avoid HAP warnings on update.
|
|
26
|
+
const configuredNameUuid = Characteristic.ConfiguredName.UUID
|
|
27
|
+
const hasConfiguredNameCharacteristic = service.characteristics
|
|
28
|
+
.some((characteristic) => characteristic.UUID === configuredNameUuid)
|
|
29
|
+
const hasConfiguredNameOptional = service.optionalCharacteristics
|
|
30
|
+
.some((characteristic) => characteristic.UUID === configuredNameUuid)
|
|
31
|
+
|
|
32
|
+
if (!hasConfiguredNameCharacteristic && !hasConfiguredNameOptional) {
|
|
33
|
+
service.addOptionalCharacteristic(Characteristic.ConfiguredName)
|
|
34
|
+
}
|
|
32
35
|
|
|
33
36
|
service.updateCharacteristic(Characteristic.ConfiguredName, sanitizedName)
|
|
34
37
|
}
|
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.38",
|
|
5
5
|
"author": "Bret Comnes <bcomnes@gmail.com> (https://bret.io)",
|
|
6
6
|
"bugs": {
|
|
7
7
|
"url": "https://github.com/bcomnes/homebridge-yoto/issues"
|