homebridge-yoto 0.0.37 → 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 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-16 steps)
96
- /** @type {number} */ #lastNonZeroVolume = 8
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'
@@ -325,7 +325,7 @@ export class YotoPlayerAccessory {
325
325
  .getCharacteristic(Characteristic.Brightness)
326
326
  .setProps({
327
327
  minValue: 0,
328
- maxValue: 16,
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: 16, minStep: 1 })
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: 16, minStep: 1 })
625
+ .setProps({ minValue: 0, maxValue: 100, minStep: 1 })
626
626
  .onGet(this.getNightMaxVolume.bind(this))
627
627
  .onSet(this.setNightMaxVolume.bind(this))
628
628
 
@@ -917,16 +917,23 @@ export class YotoPlayerAccessory {
917
917
  }
918
918
 
919
919
  /**
920
- * Get volume level (0-16 steps) from live status
920
+ * Get volume level as percentage (mapped from 0-16 steps)
921
921
  * @returns {Promise<CharacteristicValue>}
922
922
  */
923
923
  async getVolume () {
924
- this.#log.debug(LOG_PREFIX.ACCESSORY, `[${this.#device.name}] Get volume`)
925
- return this.#deviceModel.status.volume
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
926
933
  }
927
934
 
928
935
  /**
929
- * Set volume level (0-16 steps)
936
+ * Set volume level as percentage (mapped to 0-16 steps)
930
937
  * @param {CharacteristicValue} value
931
938
  * @returns {Promise<void>}
932
939
  */
@@ -934,25 +941,28 @@ export class YotoPlayerAccessory {
934
941
  const deviceModel = this.#deviceModel
935
942
  this.#log.debug(LOG_PREFIX.ACCESSORY, `[${this.#device.name}] Set volume:`, value)
936
943
 
937
- const requestedSteps = typeof value === 'number' ? value : Number(value)
938
- if (!Number.isFinite(requestedSteps)) {
944
+ const requestedPercent = typeof value === 'number' ? value : Number(value)
945
+ if (!Number.isFinite(requestedPercent)) {
939
946
  throw new this.#platform.api.hap.HapStatusError(
940
947
  this.#platform.api.hap.HAPStatus.INVALID_VALUE_IN_REQUEST
941
948
  )
942
949
  }
943
950
 
951
+ const normalizedPercent = Math.max(0, Math.min(Math.round(requestedPercent), 100))
952
+ const requestedSteps = Math.round((normalizedPercent / 100) * 16)
944
953
  const maxVolumeSteps = Number.isFinite(deviceModel.status.maxVolume)
945
954
  ? deviceModel.status.maxVolume
946
955
  : 16
947
956
  const steps = Math.max(0, Math.min(Math.round(requestedSteps), maxVolumeSteps))
957
+ const resultPercent = Math.round((steps / 16) * 100)
948
958
  this.#log.debug(
949
959
  LOG_PREFIX.ACCESSORY,
950
- `[${this.#device.name}] Set volume raw=${value} steps=${requestedSteps} -> ${steps} (max ${maxVolumeSteps}/16)`
960
+ `[${this.#device.name}] Set volume raw=${value} normalizedPercent=${normalizedPercent} requestedSteps=${requestedSteps} -> steps=${steps} percent=${resultPercent} (maxSteps=${maxVolumeSteps})`
951
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(steps)
981
+ .updateValue(clampedPercent)
971
982
  }
972
983
  }
973
984
  } catch (error) {
@@ -1555,67 +1566,79 @@ 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
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)
1563
1577
  this.#log.debug(
1564
1578
  LOG_PREFIX.ACCESSORY,
1565
- `[${this.#device.name}] Get day max volume limit: ${limit}`
1579
+ `[${this.#device.name}] Get day max volume limit rawSteps=${limit} steps=${clampedSteps} percent=${percent}`
1566
1580
  )
1567
- return Number.isFinite(limit) ? limit : 16
1581
+ return percent
1568
1582
  }
1569
1583
 
1570
1584
  /**
1571
- * Set day max volume limit
1585
+ * Set day max volume limit as percentage (mapped to 0-16 steps)
1572
1586
  * @param {CharacteristicValue} value
1573
1587
  */
1574
1588
  async setDayMaxVolume (value) {
1575
- const requested = typeof value === 'number' ? value : Number(value)
1576
- if (!Number.isFinite(requested)) {
1589
+ const requestedPercent = typeof value === 'number' ? value : Number(value)
1590
+ if (!Number.isFinite(requestedPercent)) {
1577
1591
  throw new this.#platform.api.hap.HapStatusError(
1578
1592
  this.#platform.api.hap.HAPStatus.INVALID_VALUE_IN_REQUEST
1579
1593
  )
1580
1594
  }
1581
1595
 
1582
- const limit = Math.max(0, Math.min(Math.round(requested), 16))
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)
1583
1600
  this.#log.debug(
1584
1601
  LOG_PREFIX.ACCESSORY,
1585
- `[${this.#device.name}] Set day max volume limit raw=${value} requested=${requested} -> ${limit}/16`
1602
+ `[${this.#device.name}] Set day max volume limit raw=${value} normalizedPercent=${normalizedPercent} requestedSteps=${requestedSteps} -> steps=${limit} percent=${limitPercent}`
1586
1603
  )
1587
1604
  await this.#deviceModel.updateConfig({ maxVolumeLimit: limit })
1588
1605
  }
1589
1606
 
1590
1607
  /**
1591
- * Get night max volume limit
1608
+ * Get night max volume limit as percentage (mapped from 0-16 steps)
1592
1609
  * @returns {Promise<CharacteristicValue>}
1593
1610
  */
1594
1611
  async getNightMaxVolume () {
1595
1612
  const limit = this.#deviceModel.config.nightMaxVolumeLimit
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)
1596
1616
  this.#log.debug(
1597
1617
  LOG_PREFIX.ACCESSORY,
1598
- `[${this.#device.name}] Get night max volume limit: ${limit}`
1618
+ `[${this.#device.name}] Get night max volume limit rawSteps=${limit} steps=${clampedSteps} percent=${percent}`
1599
1619
  )
1600
- return Number.isFinite(limit) ? limit : 10
1620
+ return percent
1601
1621
  }
1602
1622
 
1603
1623
  /**
1604
- * Set night max volume limit
1624
+ * Set night max volume limit as percentage (mapped to 0-16 steps)
1605
1625
  * @param {CharacteristicValue} value
1606
1626
  */
1607
1627
  async setNightMaxVolume (value) {
1608
- const requested = typeof value === 'number' ? value : Number(value)
1609
- if (!Number.isFinite(requested)) {
1628
+ const requestedPercent = typeof value === 'number' ? value : Number(value)
1629
+ if (!Number.isFinite(requestedPercent)) {
1610
1630
  throw new this.#platform.api.hap.HapStatusError(
1611
1631
  this.#platform.api.hap.HAPStatus.INVALID_VALUE_IN_REQUEST
1612
1632
  )
1613
1633
  }
1614
1634
 
1615
- const limit = Math.max(0, Math.min(Math.round(requested), 16))
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)
1616
1639
  this.#log.debug(
1617
1640
  LOG_PREFIX.ACCESSORY,
1618
- `[${this.#device.name}] Set night max volume limit raw=${value} requested=${requested} -> ${limit}/16`
1641
+ `[${this.#device.name}] Set night max volume limit raw=${value} normalizedPercent=${normalizedPercent} requestedSteps=${requestedSteps} -> steps=${limit} percent=${limitPercent}`
1619
1642
  )
1620
1643
  await this.#deviceModel.updateConfig({ nightMaxVolumeLimit: limit })
1621
1644
  }
@@ -1645,18 +1668,19 @@ export class YotoPlayerAccessory {
1645
1668
 
1646
1669
  const { Characteristic } = this.#platform
1647
1670
  if (volumeSteps > 0) {
1648
- this.#lastNonZeroVolume = volumeSteps
1671
+ this.#lastNonZeroVolume = Math.round((volumeSteps / 16) * 100)
1649
1672
  }
1650
1673
 
1651
1674
  const normalizedVolume = Number.isFinite(volumeSteps) ? volumeSteps : 0
1652
1675
  const clampedVolume = Math.max(0, Math.min(normalizedVolume, 16))
1676
+ const percent = Math.round((clampedVolume / 16) * 100)
1653
1677
  this.#log.debug(
1654
1678
  LOG_PREFIX.ACCESSORY,
1655
- `[${this.#device.name}] Update volume characteristic raw=${volumeSteps} -> ${clampedVolume}`
1679
+ `[${this.#device.name}] Update volume characteristic rawSteps=${volumeSteps} steps=${clampedVolume} percent=${percent}`
1656
1680
  )
1657
1681
  this.volumeService
1658
1682
  .getCharacteristic(Characteristic.Brightness)
1659
- .updateValue(clampedVolume)
1683
+ .updateValue(percent)
1660
1684
  }
1661
1685
 
1662
1686
  /**
@@ -1920,25 +1944,27 @@ export class YotoPlayerAccessory {
1920
1944
  if (this.dayMaxVolumeService) {
1921
1945
  const limit = Number.isFinite(config.maxVolumeLimit) ? config.maxVolumeLimit : 16
1922
1946
  const clampedLimit = Math.max(0, Math.min(limit, 16))
1947
+ const percent = Math.round((clampedLimit / 16) * 100)
1923
1948
  this.#log.debug(
1924
1949
  LOG_PREFIX.ACCESSORY,
1925
- `[${this.#device.name}] Update day max volume characteristic raw=${limit} -> ${clampedLimit}`
1950
+ `[${this.#device.name}] Update day max volume characteristic rawSteps=${limit} steps=${clampedLimit} percent=${percent}`
1926
1951
  )
1927
1952
  this.dayMaxVolumeService
1928
1953
  .getCharacteristic(Characteristic.Brightness)
1929
- .updateValue(clampedLimit)
1954
+ .updateValue(percent)
1930
1955
  }
1931
1956
 
1932
1957
  if (this.nightMaxVolumeService) {
1933
1958
  const limit = Number.isFinite(config.nightMaxVolumeLimit) ? config.nightMaxVolumeLimit : 10
1934
1959
  const clampedLimit = Math.max(0, Math.min(limit, 16))
1960
+ const percent = Math.round((clampedLimit / 16) * 100)
1935
1961
  this.#log.debug(
1936
1962
  LOG_PREFIX.ACCESSORY,
1937
- `[${this.#device.name}] Update night max volume characteristic raw=${limit} -> ${clampedLimit}`
1963
+ `[${this.#device.name}] Update night max volume characteristic rawSteps=${limit} steps=${clampedLimit} percent=${percent}`
1938
1964
  )
1939
1965
  this.nightMaxVolumeService
1940
1966
  .getCharacteristic(Characteristic.Brightness)
1941
- .updateValue(clampedLimit)
1967
+ .updateValue(percent)
1942
1968
  }
1943
1969
  }
1944
1970
 
package/lib/platform.js CHANGED
@@ -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 [${deviceId} ${source}]: ${fields}`)
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 [${deviceId}]: ${fields}`)
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 [${deviceId}]: ${fields}`)
221
+ this.log.debug(`Playback update [${label}]: ${fields}`)
204
222
  })
205
223
 
206
224
  this.yotoAccount.on('mqttConnect', ({ deviceId }) => {
207
- const deviceName = this.yotoAccount?.getDevice(deviceId)?.device?.name
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: ${deviceId}${reason}`)
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: ${deviceId}${reason}`)
239
+ this.log.debug(`MQTT closed: ${label}${reason}`)
221
240
  })
222
241
 
223
242
  this.yotoAccount.on('mqttReconnect', ({ deviceId }) => {
224
- this.log.debug(`MQTT reconnecting: ${deviceId}`)
243
+ const label = getDeviceLabel(deviceId)
244
+ this.log.debug(`MQTT reconnecting: ${label}`)
225
245
  })
226
246
 
227
247
  this.yotoAccount.on('mqttOffline', ({ deviceId }) => {
228
- this.log.debug(`MQTT offline: ${deviceId}`)
248
+ const label = getDeviceLabel(deviceId)
249
+ this.log.debug(`MQTT offline: ${label}`)
229
250
  })
230
251
 
231
252
  this.yotoAccount.on('mqttEnd', ({ deviceId }) => {
232
- this.log.debug(`MQTT ended: ${deviceId}`)
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
- this.log.debug(`MQTT status [${deviceId}]: ${topic}`)
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
- this.log.debug(`MQTT events [${deviceId}]: ${topic}`)
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
- this.log.debug(`MQTT legacy status [${deviceId}]: ${topic}`)
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
- this.log.debug(`MQTT response [${deviceId}]: ${topic}`)
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
- this.log.debug(`MQTT unknown [${deviceId}]: ${topic}`)
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)
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.37",
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"