homebridge-yoto 0.0.15 → 0.0.17

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.
@@ -511,13 +511,17 @@ export class YotoPlayerAccessory {
511
511
 
512
512
  /**
513
513
  * Handle status update from MQTT
514
- * @param {YotoDeviceStatus} status - Status data
514
+ * @param {YotoDeviceStatus | {status: YotoDeviceStatus}} statusMessage - Status data or wrapped status
515
515
  */
516
- handleStatusUpdate (status) {
517
- this.log.debug(LOG_PREFIX.ACCESSORY, `[${this.device.name}] Received status update:`, JSON.stringify(status, null, 2))
518
- this.log.debug(LOG_PREFIX.ACCESSORY, `[${this.device.name}] Status - batteryLevel:`, status.batteryLevel)
519
- this.log.debug(LOG_PREFIX.ACCESSORY, `[${this.device.name}] Status - userVolume:`, status.userVolume)
520
- this.log.debug(LOG_PREFIX.ACCESSORY, `[${this.device.name}] Status - volume:`, status.volume)
516
+ handleStatusUpdate (statusMessage) {
517
+ this.log.info(LOG_PREFIX.ACCESSORY, `[${this.device.name}] Raw MQTT status message:`, JSON.stringify(statusMessage, null, 2))
518
+
519
+ // Unwrap status if it's nested under a 'status' property
520
+ const status = /** @type {YotoDeviceStatus} */ ('status' in statusMessage ? statusMessage.status : statusMessage)
521
+
522
+ this.log.info(LOG_PREFIX.ACCESSORY, `[${this.device.name}] Unwrapped status - batteryLevel:`, status.batteryLevel)
523
+ this.log.info(LOG_PREFIX.ACCESSORY, `[${this.device.name}] Unwrapped status - userVolume:`, status.userVolume)
524
+ this.log.info(LOG_PREFIX.ACCESSORY, `[${this.device.name}] Unwrapped status - volume:`, status.volume)
521
525
 
522
526
  this.currentStatus = status
523
527
  this.lastUpdateTime = Date.now()
@@ -529,10 +533,15 @@ export class YotoPlayerAccessory {
529
533
 
530
534
  /**
531
535
  * Handle playback events update from MQTT
532
- * @param {YotoPlaybackEvents} events - Playback events
536
+ * @param {YotoPlaybackEvents | {events: YotoPlaybackEvents}} eventsMessage - Playback events or wrapped events
533
537
  */
534
- handleEventsUpdate (events) {
535
- this.log.debug(LOG_PREFIX.ACCESSORY, `[${this.device.name}] Events update received`)
538
+ handleEventsUpdate (eventsMessage) {
539
+ this.log.info(LOG_PREFIX.ACCESSORY, `[${this.device.name}] Raw MQTT events message:`, JSON.stringify(eventsMessage, null, 2))
540
+
541
+ // Unwrap events if it's nested under an 'events' property
542
+ const events = /** @type {YotoPlaybackEvents} */ ('events' in eventsMessage ? eventsMessage.events : eventsMessage)
543
+
544
+ this.log.info(LOG_PREFIX.ACCESSORY, `[${this.device.name}] Unwrapped events - cardId:`, events.cardId)
536
545
  this.currentEvents = events
537
546
  this.lastUpdateTime = Date.now()
538
547
  this.accessory.context.lastEvents = events
@@ -551,7 +560,7 @@ export class YotoPlayerAccessory {
551
560
  * @param {import('./types.js').MqttCommandResponse} response - Command response
552
561
  */
553
562
  handleCommandResponse (response) {
554
- this.log.debug(LOG_PREFIX.ACCESSORY, `[${this.device.name}] Command response:`, response)
563
+ this.log.info(LOG_PREFIX.ACCESSORY, `[${this.device.name}] Command response:`, response)
555
564
  }
556
565
 
557
566
  /**
@@ -562,16 +571,17 @@ export class YotoPlayerAccessory {
562
571
  return
563
572
  }
564
573
 
565
- // Update volume
566
- if (this.speakerService) {
574
+ // Update volume from events, not status
575
+ if (this.speakerService && this.currentEvents?.volume !== undefined) {
576
+ const volume = Number(this.currentEvents.volume) || 0
567
577
  this.speakerService.updateCharacteristic(
568
578
  this.platform.Characteristic.Volume,
569
- this.currentStatus.userVolume
579
+ volume
570
580
  )
571
581
  }
572
582
 
573
583
  // Update battery
574
- if (this.batteryService) {
584
+ if (this.batteryService && this.currentStatus.batteryLevel !== undefined) {
575
585
  this.batteryService.updateCharacteristic(
576
586
  this.platform.Characteristic.BatteryLevel,
577
587
  this.currentStatus.batteryLevel
@@ -603,7 +613,7 @@ export class YotoPlayerAccessory {
603
613
  }
604
614
 
605
615
  // Update display brightness
606
- if (this.displayService) {
616
+ if (this.displayService && this.currentStatus.dnowBrightness !== undefined) {
607
617
  const isOn = this.currentStatus.dnowBrightness > 0
608
618
  this.displayService.updateCharacteristic(
609
619
  this.platform.Characteristic.On,
@@ -619,7 +629,7 @@ export class YotoPlayerAccessory {
619
629
  }
620
630
 
621
631
  // Update advanced control switches
622
- if (this.bluetoothSwitch) {
632
+ if (this.bluetoothSwitch && this.currentStatus.bluetoothHp !== undefined) {
623
633
  const bluetoothEnabled = this.currentStatus.bluetoothHp
624
634
  this.bluetoothSwitch.updateCharacteristic(
625
635
  this.platform.Characteristic.On,
@@ -627,7 +637,7 @@ export class YotoPlayerAccessory {
627
637
  )
628
638
  }
629
639
 
630
- if (this.btHeadphonesSwitch) {
640
+ if (this.btHeadphonesSwitch && this.currentStatus.bluetoothHp !== undefined) {
631
641
  const btHeadphonesEnabled = this.currentStatus.bluetoothHp
632
642
  this.btHeadphonesSwitch.updateCharacteristic(
633
643
  this.platform.Characteristic.On,
@@ -789,15 +799,16 @@ export class YotoPlayerAccessory {
789
799
  * @returns {Promise<CharacteristicValue>}
790
800
  */
791
801
  async getVolume () {
792
- this.log.debug(LOG_PREFIX.ACCESSORY, `[${this.device.name}] getVolume - currentStatus:`, this.currentStatus)
793
- this.log.debug(LOG_PREFIX.ACCESSORY, `[${this.device.name}] getVolume - userVolume:`, this.currentStatus?.userVolume)
802
+ this.log.info(LOG_PREFIX.ACCESSORY, `[${this.device.name}] getVolume - currentEvents:`, this.currentEvents)
803
+ this.log.info(LOG_PREFIX.ACCESSORY, `[${this.device.name}] getVolume - events.volume:`, this.currentEvents?.volume)
794
804
 
795
- if (!this.currentStatus || this.currentStatus.userVolume === undefined) {
796
- this.log.debug(LOG_PREFIX.ACCESSORY, `[${this.device.name}] getVolume - returning default: 50`)
805
+ // Volume comes from events, not status
806
+ if (!this.currentEvents || this.currentEvents.volume === undefined) {
807
+ this.log.info(LOG_PREFIX.ACCESSORY, `[${this.device.name}] getVolume - no volume in events, returning default: 50`)
797
808
  return 50
798
809
  }
799
- const volume = Number(this.currentStatus.userVolume) || 50
800
- this.log.debug(LOG_PREFIX.ACCESSORY, `[${this.device.name}] getVolume - returning:`, volume)
810
+ const volume = Number(this.currentEvents.volume) || 50
811
+ this.log.info(LOG_PREFIX.ACCESSORY, `[${this.device.name}] getVolume - returning volume:`, volume)
801
812
  return volume
802
813
  }
803
814
 
@@ -823,10 +834,11 @@ export class YotoPlayerAccessory {
823
834
  * @returns {Promise<CharacteristicValue>}
824
835
  */
825
836
  async getMute () {
826
- if (!this.currentStatus || this.currentStatus.userVolume === undefined) {
837
+ // Volume comes from events, not status
838
+ if (!this.currentEvents || this.currentEvents.volume === undefined) {
827
839
  return false
828
840
  }
829
- return Number(this.currentStatus.userVolume) === 0
841
+ return Number(this.currentEvents.volume) === 0
830
842
  }
831
843
 
832
844
  /**
@@ -842,7 +854,7 @@ export class YotoPlayerAccessory {
842
854
  await this.mqtt.setVolume(this.device.deviceId, 0)
843
855
  } else {
844
856
  // Unmute - restore to a reasonable volume if currently 0
845
- const currentVolume = this.currentStatus?.userVolume || 0
857
+ const currentVolume = this.currentEvents?.volume ? Number(this.currentEvents.volume) : 0
846
858
  const targetVolume = currentVolume === 0 ? 50 : currentVolume
847
859
  await this.mqtt.setVolume(this.device.deviceId, targetVolume)
848
860
  }
@@ -859,15 +871,15 @@ export class YotoPlayerAccessory {
859
871
  * @returns {Promise<CharacteristicValue>}
860
872
  */
861
873
  async getBatteryLevel () {
862
- this.log.debug(LOG_PREFIX.ACCESSORY, `[${this.device.name}] getBatteryLevel - currentStatus:`, this.currentStatus)
863
- this.log.debug(LOG_PREFIX.ACCESSORY, `[${this.device.name}] getBatteryLevel - batteryLevel:`, this.currentStatus?.batteryLevel)
874
+ this.log.info(LOG_PREFIX.ACCESSORY, `[${this.device.name}] getBatteryLevel - currentStatus:`, this.currentStatus)
875
+ this.log.info(LOG_PREFIX.ACCESSORY, `[${this.device.name}] getBatteryLevel - batteryLevel:`, this.currentStatus?.batteryLevel)
864
876
 
865
877
  if (!this.currentStatus || this.currentStatus.batteryLevel === undefined) {
866
- this.log.debug(LOG_PREFIX.ACCESSORY, `[${this.device.name}] getBatteryLevel - returning default: 100`)
878
+ this.log.info(LOG_PREFIX.ACCESSORY, `[${this.device.name}] getBatteryLevel - returning default: 100`)
867
879
  return 100
868
880
  }
869
881
  const battery = Number(this.currentStatus.batteryLevel) || 100
870
- this.log.debug(LOG_PREFIX.ACCESSORY, `[${this.device.name}] getBatteryLevel - returning:`, battery)
882
+ this.log.info(LOG_PREFIX.ACCESSORY, `[${this.device.name}] getBatteryLevel - returning:`, battery)
871
883
  return battery
872
884
  }
873
885
 
@@ -951,7 +963,7 @@ export class YotoPlayerAccessory {
951
963
  * @returns {Promise<CharacteristicValue>}
952
964
  */
953
965
  async getDisplayOn () {
954
- if (!this.currentStatus) {
966
+ if (!this.currentStatus || this.currentStatus.dnowBrightness === undefined) {
955
967
  return true
956
968
  }
957
969
  return this.currentStatus.dnowBrightness > 0
@@ -992,12 +1004,16 @@ export class YotoPlayerAccessory {
992
1004
  * @returns {Promise<CharacteristicValue>}
993
1005
  */
994
1006
  async getDisplayBrightness () {
995
- if (!this.currentStatus) {
1007
+ if (!this.currentStatus || this.currentStatus.dnowBrightness === undefined) {
996
1008
  return 100
997
1009
  }
998
1010
 
999
1011
  // Use current brightness value (0-100)
1000
- return Math.min(100, Math.max(0, this.currentStatus.dnowBrightness))
1012
+ const brightness = Number(this.currentStatus.dnowBrightness)
1013
+ if (isNaN(brightness)) {
1014
+ return 100
1015
+ }
1016
+ return Math.min(100, Math.max(0, brightness))
1001
1017
  }
1002
1018
 
1003
1019
  /**
@@ -1031,7 +1047,7 @@ export class YotoPlayerAccessory {
1031
1047
  * @returns {Promise<CharacteristicValue>}
1032
1048
  */
1033
1049
  async getBluetoothEnabled () {
1034
- if (!this.currentStatus) {
1050
+ if (!this.currentStatus || this.currentStatus.bluetoothHp === undefined) {
1035
1051
  return false
1036
1052
  }
1037
1053
  return this.currentStatus.bluetoothHp
@@ -1091,7 +1107,7 @@ export class YotoPlayerAccessory {
1091
1107
  * @returns {Promise<CharacteristicValue>}
1092
1108
  */
1093
1109
  async getBtHeadphonesEnabled () {
1094
- if (!this.currentStatus) {
1110
+ if (!this.currentStatus || this.currentStatus.bluetoothHp === undefined) {
1095
1111
  return false
1096
1112
  }
1097
1113
  return this.currentStatus.bluetoothHp
@@ -1220,6 +1236,9 @@ export class YotoPlayerAccessory {
1220
1236
  try {
1221
1237
  const config = await this.platform.yotoApi.getDeviceConfig(this.device.deviceId)
1222
1238
  const limit = parseInt(config.config.maxVolumeLimit || '16')
1239
+ if (isNaN(limit)) {
1240
+ return 100
1241
+ }
1223
1242
  // Map 0-16 to 0-100
1224
1243
  return Math.round((limit / 16) * 100)
1225
1244
  } catch (error) {
@@ -1275,6 +1294,9 @@ export class YotoPlayerAccessory {
1275
1294
  try {
1276
1295
  const config = await this.platform.yotoApi.getDeviceConfig(this.device.deviceId)
1277
1296
  const limit = parseInt(config.config.nightMaxVolumeLimit || '16')
1297
+ if (isNaN(limit)) {
1298
+ return 100
1299
+ }
1278
1300
  // Map 0-16 to 0-100
1279
1301
  return Math.round((limit / 16) * 100)
1280
1302
  } catch (error) {
@@ -1352,6 +1374,9 @@ export class YotoPlayerAccessory {
1352
1374
  const config = await this.platform.yotoApi.getDeviceConfig(this.device.deviceId)
1353
1375
  const hex = config.config.ambientColour || '#000000'
1354
1376
  const { h } = this.hexToHsv(hex)
1377
+ if (isNaN(h)) {
1378
+ return 0
1379
+ }
1355
1380
  return h
1356
1381
  } catch (error) {
1357
1382
  this.log.error(LOG_PREFIX.ACCESSORY, `[${this.device.name}] Failed to get ambient light hue:`, error)
@@ -1379,6 +1404,9 @@ export class YotoPlayerAccessory {
1379
1404
  const config = await this.platform.yotoApi.getDeviceConfig(this.device.deviceId)
1380
1405
  const hex = config.config.ambientColour || '#000000'
1381
1406
  const { s } = this.hexToHsv(hex)
1407
+ if (isNaN(s)) {
1408
+ return 0
1409
+ }
1382
1410
  return s
1383
1411
  } catch (error) {
1384
1412
  this.log.error(LOG_PREFIX.ACCESSORY, `[${this.device.name}] Failed to get ambient light saturation:`, error)
@@ -1404,11 +1432,15 @@ export class YotoPlayerAccessory {
1404
1432
  async getAmbientLightBrightness () {
1405
1433
  try {
1406
1434
  const config = await this.platform.yotoApi.getDeviceConfig(this.device.deviceId)
1407
- const hex = config.config.ambientColour || '#000000'
1435
+ const hex = config.config.ambientColour
1408
1436
  const { v } = this.hexToHsv(hex)
1409
- return v
1437
+ const brightness = Math.round(v)
1438
+ if (isNaN(brightness)) {
1439
+ return 100
1440
+ }
1441
+ return brightness
1410
1442
  } catch (error) {
1411
- this.log.error(LOG_PREFIX.ACCESSORY, `[${this.device.name}] Failed to get ambient light brightness:`, error)
1443
+ this.log.error(LOG_PREFIX.ACCESSORY, `[${this.device.name}] Failed to get ambient brightness:`, error)
1412
1444
  return 100
1413
1445
  }
1414
1446
  }
package/lib/types.js CHANGED
@@ -15,43 +15,63 @@
15
15
  */
16
16
 
17
17
  /**
18
+ * YotoDeviceStatus - ACTUAL structure from MQTT /device/{id}/data/status
19
+ * Note: This differs from the documented API schema
18
20
  * @typedef {Object} YotoDeviceStatus
19
- * @property {number} statusVersion - Status data version
20
- * @property {string} fwVersion - Firmware version
21
- * @property {string} productType - Product type identifier
22
- * @property {number} batteryLevel - Battery level (0-100)
23
- * @property {number} als - Ambient light sensor reading
24
- * @property {number} freeDisk - Free disk space in bytes
25
- * @property {number} shutdownTimeout - Auto-shutdown timeout in seconds
26
- * @property {number} dbatTimeout - Display battery timeout
27
- * @property {number} charging - Charging state (0=not charging, 1=charging)
28
- * @property {string | null} activeCard - Currently active card ID or null
29
- * @property {number} cardInserted - Card insertion state (0=none, 1=physical, 2=remote)
30
- * @property {number} playingStatus - Playing status code
31
- * @property {boolean} headphones - Whether headphones are connected
32
- * @property {number} dnowBrightness - Current display brightness
33
- * @property {number} dayBright - Day mode brightness setting
34
- * @property {number} nightBright - Night mode brightness setting
35
- * @property {boolean} bluetoothHp - Bluetooth headphones enabled
36
- * @property {number} volume - System volume level
37
- * @property {number} userVolume - User volume level (0-100)
38
- * @property {'12' | '24'} timeFormat - Time format preference
39
- * @property {string} nightlightMode - Nightlight mode setting
40
- * @property {string} temp - Temperature reading
41
- * @property {number} day - Day mode (0=night, 1=day, -1=unknown)
42
- */
43
-
44
- /**
21
+ * @property {number} battery - Raw battery voltage (e.g., 3693)
22
+ * @property {string} [powerCaps] - Power capability flags (e.g., '0x02')
23
+ * @property {number} batteryLevel - Battery level percentage (0-100)
24
+ * @property {number} batteryTemp - Battery temperature
25
+ * @property {string} batteryData - Battery data string (e.g., '0:0:0')
26
+ * @property {number} batteryLevelRaw - Raw battery level value
27
+ * @property {number} free - Free memory
28
+ * @property {number} freeDMA - Free DMA memory
29
+ * @property {number} free32 - Free 32-bit memory
30
+ * @property {number} upTime - Device uptime in seconds
31
+ * @property {number} utcTime - Current UTC time (Unix timestamp)
32
+ * @property {number} aliveTime - Time device has been alive
33
+ * @property {number} accelTemp - Accelerometer temperature in Celsius
34
+ * @property {number} [qiOtp] - Qi charging related field
35
+ * @property {number} [errorsLogged] - Number of errors logged
36
+ * @property {string} nightlightMode - Nightlight mode (e.g., 'off')
37
+ * @property {string} temp - Temperature data string (e.g., '1014:23:318')
38
+ *
39
+ * Note: Volume information comes from events, not status!
40
+ * The following documented fields may exist but haven't been observed in v3 players:
41
+ * @property {number} [statusVersion] - Status data version
42
+ * @property {string} [fwVersion] - Firmware version
43
+ * @property {string} [productType] - Product type identifier
44
+ * @property {number} [als] - Ambient light sensor reading
45
+ * @property {number} [freeDisk] - Free disk space in bytes
46
+ * @property {number} [shutdownTimeout] - Auto-shutdown timeout in seconds
47
+ * @property {number} [dbatTimeout] - Display battery timeout
48
+ * @property {number} [charging] - Charging state (0=not charging, 1=charging)
49
+ * @property {string | null} [activeCard] - Currently active card ID or null
50
+ * @property {number} [cardInserted] - Card insertion state (0=none, 1=physical, 2=remote)
51
+ * @property {number} [playingStatus] - Playing status code
52
+ * @property {boolean} [headphones] - Whether headphones are connected
53
+ * @property {number} [dnowBrightness] - Current display brightness
54
+ * @property {number} [dayBright] - Day mode brightness setting
55
+ * @property {number} [nightBright] - Night mode brightness setting
56
+ * @property {boolean} [bluetoothHp] - Bluetooth headphones enabled
57
+ * @property {number} [volume] - System volume level (may be in events instead)
58
+ * @property {number} [userVolume] - User volume level 0-100 (may be in events instead)
59
+ * @property {'12' | '24'} [timeFormat] - Time format preference
60
+ * @property {number} [day] - Day mode (0=night, 1=day, -1=unknown)
61
+ */
62
+
63
+ /**
64
+ * YotoPlaybackEvents - From MQTT /device/{id}/data/events
45
65
  * @typedef {Object} YotoPlaybackEvents
46
66
  * @property {string} repeatAll - Repeat all setting ("true" or "false")
47
67
  * @property {string} streaming - Streaming active ("true" or "false")
48
- * @property {string} volume - Current volume level
49
- * @property {string} volumeMax - Maximum volume level
68
+ * @property {string} volume - Current volume level (0-100 as string)
69
+ * @property {string} volumeMax - Maximum volume level (0-100 as string)
50
70
  * @property {string} playbackWait - Playback wait state ("true" or "false")
51
71
  * @property {string} sleepTimerActive - Sleep timer active ("true" or "false")
52
- * @property {string} eventUtc - Event timestamp (Unix timestamp)
53
- * @property {string} trackLength - Track duration in seconds
54
- * @property {string} position - Current playback position in seconds
72
+ * @property {string} eventUtc - Event timestamp (Unix timestamp as string)
73
+ * @property {string} trackLength - Track duration in seconds (as string)
74
+ * @property {string} position - Current playback position in seconds (as string)
55
75
  * @property {string} cardId - Active card ID
56
76
  * @property {string} source - Playback source (e.g., "card", "remote", "MQTT")
57
77
  * @property {string} cardUpdatedAt - Card last updated timestamp (ISO8601)
@@ -59,8 +79,8 @@
59
79
  * @property {string} chapterKey - Current chapter key
60
80
  * @property {string} trackTitle - Current track title
61
81
  * @property {string} trackKey - Current track key
62
- * @property {string} playbackStatus - Playback status ("playing", "paused", "stopped")
63
- * @property {string} sleepTimerSeconds - Sleep timer remaining seconds
82
+ * @property {string} playbackStatus - Playback status (e.g., "playing", "paused", "stopped")
83
+ * @property {string} sleepTimerSeconds - Remaining sleep timer seconds (as string)
64
84
  */
65
85
 
66
86
  /**
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.15",
4
+ "version": "0.0.17",
5
5
  "author": "Bret Comnes <bcomnes@gmail.com> (https://bret.io)",
6
6
  "bugs": {
7
7
  "url": "https://github.com/bcomnes/homebridge-yoto/issues"