homebridge-yoto 0.0.12 → 0.0.14

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/platform.js CHANGED
@@ -56,6 +56,10 @@ export class YotoPlatform {
56
56
  // Status polling interval
57
57
  this.statusPollInterval = null
58
58
 
59
+ // Cache of user's owned MYO card IDs
60
+ /** @type {Set<string>} */
61
+ this.ownedCardIds = new Set()
62
+
59
63
  // Initialize API clients
60
64
  this.auth = new YotoAuth(log, this.config.clientId)
61
65
  this.yotoApi = new YotoApi(log, this.auth)
@@ -108,6 +112,9 @@ export class YotoPlatform {
108
112
  throw error
109
113
  }
110
114
 
115
+ // Fetch user's owned cards for lookup optimization
116
+ await this.fetchOwnedCards()
117
+
111
118
  // Start platform-level status polling (every 60 seconds)
112
119
  this.startStatusPolling()
113
120
  } catch (error) {
@@ -173,6 +180,40 @@ export class YotoPlatform {
173
180
  }
174
181
 
175
182
  /**
183
+ * Fetch user's owned MYO cards and cache their IDs
184
+ * @returns {Promise<void>}
185
+ */
186
+ async fetchOwnedCards () {
187
+ try {
188
+ this.log.debug(LOG_PREFIX.PLATFORM, 'Fetching user\'s owned cards...')
189
+ const myContent = await this.yotoApi.getMyContent()
190
+
191
+ // Cache card IDs
192
+ if (myContent.cards && Array.isArray(myContent.cards)) {
193
+ this.ownedCardIds.clear()
194
+ for (const card of myContent.cards) {
195
+ if (card.cardId) {
196
+ this.ownedCardIds.add(card.cardId)
197
+ }
198
+ }
199
+ this.log.info(LOG_PREFIX.PLATFORM, `✓ Cached ${this.ownedCardIds.size} owned card(s)`)
200
+ }
201
+ } catch (error) {
202
+ this.log.warn(LOG_PREFIX.PLATFORM, 'Failed to fetch owned cards, card details may be limited:', error)
203
+ }
204
+ }
205
+
206
+ /**
207
+ * Check if a card is owned by the user
208
+ * @param {string} cardId - Card ID to check
209
+ * @returns {boolean}
210
+ */
211
+ isCardOwned (cardId) {
212
+ return this.ownedCardIds.has(cardId)
213
+ }
214
+
215
+ /**
216
+ * Handle token refresh - update config
176
217
  * Perform device authorization flow
177
218
  * @returns {Promise<void>}
178
219
  */
@@ -785,10 +785,10 @@ export class YotoPlayerAccessory {
785
785
  * @returns {Promise<CharacteristicValue>}
786
786
  */
787
787
  async getVolume () {
788
- if (!this.currentStatus) {
788
+ if (!this.currentStatus || this.currentStatus.userVolume === undefined) {
789
789
  return 50
790
790
  }
791
- return this.currentStatus.userVolume
791
+ return Number(this.currentStatus.userVolume) || 50
792
792
  }
793
793
 
794
794
  /**
@@ -813,10 +813,10 @@ export class YotoPlayerAccessory {
813
813
  * @returns {Promise<CharacteristicValue>}
814
814
  */
815
815
  async getMute () {
816
- if (!this.currentStatus) {
816
+ if (!this.currentStatus || this.currentStatus.userVolume === undefined) {
817
817
  return false
818
818
  }
819
- return this.currentStatus.userVolume === 0
819
+ return Number(this.currentStatus.userVolume) === 0
820
820
  }
821
821
 
822
822
  /**
@@ -849,10 +849,10 @@ export class YotoPlayerAccessory {
849
849
  * @returns {Promise<CharacteristicValue>}
850
850
  */
851
851
  async getBatteryLevel () {
852
- if (!this.currentStatus) {
852
+ if (!this.currentStatus || this.currentStatus.batteryLevel === undefined) {
853
853
  return 100
854
854
  }
855
- return this.currentStatus.batteryLevel
855
+ return Number(this.currentStatus.batteryLevel) || 100
856
856
  }
857
857
 
858
858
  /**
@@ -1527,10 +1527,27 @@ export class YotoPlayerAccessory {
1527
1527
  }
1528
1528
 
1529
1529
  this.activeContentCardId = cardId
1530
+
1531
+ // Handle no card inserted
1532
+ if (!cardId || cardId === 'none' || cardId === 'null') {
1533
+ this.log.info(LOG_PREFIX.ACCESSORY, `[${this.device.name}] No card inserted`)
1534
+ this.activeContentInfo = null
1535
+ this.accessory.context.activeContentInfo = null
1536
+ return
1537
+ }
1538
+
1539
+ // Check if we own this card before attempting to fetch details
1540
+ if (!this.platform.isCardOwned(cardId)) {
1541
+ this.log.info(LOG_PREFIX.ACCESSORY, `[${this.device.name}] Playing card ${cardId} (store content - details not available)`)
1542
+ this.activeContentInfo = null
1543
+ this.accessory.context.activeContentInfo = null
1544
+ return
1545
+ }
1546
+
1530
1547
  this.log.info(LOG_PREFIX.ACCESSORY, `[${this.device.name}] Active card changed: ${cardId}`)
1531
1548
 
1532
1549
  try {
1533
- // Fetch card details
1550
+ // Fetch card details for owned cards
1534
1551
  const content = await this.platform.yotoApi.getContent(cardId)
1535
1552
  this.activeContentInfo = content
1536
1553
 
@@ -1555,7 +1572,20 @@ export class YotoPlayerAccessory {
1555
1572
  // Store in context for persistence
1556
1573
  this.accessory.context.activeContentInfo = this.activeContentInfo
1557
1574
  } catch (error) {
1558
- this.log.error(LOG_PREFIX.ACCESSORY, `[${this.device.name}] Failed to fetch content details:`, error)
1575
+ // Handle 403 Forbidden (store-bought cards user doesn't own)
1576
+ if (error instanceof Error && error.message.includes('403')) {
1577
+ this.log.info(LOG_PREFIX.ACCESSORY, `[${this.device.name}] Playing card ${cardId} (store content - details not available)`)
1578
+ this.activeContentInfo = null
1579
+ this.accessory.context.activeContentInfo = null
1580
+ } else if (error instanceof Error && error.message.includes('404')) {
1581
+ // Card not found - might be deleted or invalid
1582
+ this.log.warn(LOG_PREFIX.ACCESSORY, `[${this.device.name}] Card ${cardId} not found`)
1583
+ this.activeContentInfo = null
1584
+ this.accessory.context.activeContentInfo = null
1585
+ } else {
1586
+ // Other errors
1587
+ this.log.error(LOG_PREFIX.ACCESSORY, `[${this.device.name}] Failed to fetch content details:`, error)
1588
+ }
1559
1589
  }
1560
1590
  }
1561
1591
  }
package/lib/yotoApi.js CHANGED
@@ -114,7 +114,6 @@ export class YotoApi {
114
114
 
115
115
  if (!response.ok) {
116
116
  const errorText = await response.text()
117
- this.log.error(LOG_PREFIX.API, `Request failed: ${response.status} ${errorText}`)
118
117
 
119
118
  // Handle 401 by attempting token refresh once
120
119
  if (response.status === 401 && !options._retried) {
@@ -124,6 +123,13 @@ export class YotoApi {
124
123
  return this.request(endpoint, { ...options, _retried: true })
125
124
  }
126
125
 
126
+ // Reduce noise for expected errors (403/404 on content endpoints)
127
+ if ((response.status === 403 || response.status === 404) && endpoint.startsWith('/content/')) {
128
+ this.log.debug(LOG_PREFIX.API, `Request failed: ${response.status} ${errorText}`)
129
+ } else {
130
+ this.log.error(LOG_PREFIX.API, `Request failed: ${response.status} ${errorText}`)
131
+ }
132
+
127
133
  throw new Error(`API request failed: ${response.status} ${errorText}`)
128
134
  }
129
135
 
@@ -134,7 +140,10 @@ export class YotoApi {
134
140
 
135
141
  return await response.json()
136
142
  } catch (error) {
137
- this.log.error(LOG_PREFIX.API, `${ERROR_MESSAGES.API_ERROR}:`, error)
143
+ // Only log non-API errors (network issues, etc.)
144
+ if (!(error instanceof Error && error.message.startsWith('API request failed'))) {
145
+ this.log.error(LOG_PREFIX.API, `${ERROR_MESSAGES.API_ERROR}:`, error)
146
+ }
138
147
  throw error
139
148
  }
140
149
  }
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.12",
4
+ "version": "0.0.14",
5
5
  "author": "Bret Comnes <bcomnes@gmail.com> (https://bret.io)",
6
6
  "bugs": {
7
7
  "url": "https://github.com/bcomnes/homebridge-yoto/issues"