homebridge-yoto 0.0.28 → 0.0.31

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/types.js DELETED
@@ -1,253 +0,0 @@
1
- /**
2
- * @fileoverview Type definitions for Yoto API and Homebridge integration
3
- */
4
-
5
- /**
6
- * @typedef {Object} YotoDevice
7
- * @property {string} deviceId - Unique identifier for the device
8
- * @property {string} name - Device name
9
- * @property {string} description - Device description
10
- * @property {boolean} online - Whether device is currently online
11
- * @property {string} releaseChannel - Release channel (e.g., "stable", "beta")
12
- * @property {string} deviceType - Type of device (e.g., "player")
13
- * @property {string} deviceFamily - Device family (e.g., "yoto")
14
- * @property {string} deviceGroup - Device group classification
15
- */
16
-
17
- /**
18
- * YotoDeviceStatus - ACTUAL structure from MQTT /device/{id}/data/status
19
- * Note: This differs from the documented API schema
20
- * @typedef {Object} YotoDeviceStatus
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
65
- * @typedef {Object} YotoPlaybackEvents
66
- * @property {string} repeatAll - Repeat all setting ("true" or "false")
67
- * @property {string} streaming - Streaming active ("true" or "false")
68
- * @property {string} volume - Current volume level (0-100 as string)
69
- * @property {string} volumeMax - Maximum volume level (0-100 as string)
70
- * @property {string} playbackWait - Playback wait state ("true" or "false")
71
- * @property {string} sleepTimerActive - Sleep timer active ("true" or "false")
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)
75
- * @property {string} cardId - Active card ID
76
- * @property {string} source - Playback source (e.g., "card", "remote", "MQTT")
77
- * @property {string} cardUpdatedAt - Card last updated timestamp (ISO8601)
78
- * @property {string} chapterTitle - Current chapter title
79
- * @property {string} chapterKey - Current chapter key
80
- * @property {string} trackTitle - Current track title
81
- * @property {string} trackKey - Current track key
82
- * @property {string} playbackStatus - Playback status (e.g., "playing", "paused", "stopped")
83
- * @property {string} sleepTimerSeconds - Remaining sleep timer seconds (as string)
84
- */
85
-
86
- /**
87
- * @typedef {Object} YotoDeviceConfigSettings
88
- * @property {any[]} alarms - Alarm configurations
89
- * @property {string} ambientColour - Ambient LED color (hex format)
90
- * @property {string} bluetoothEnabled - Bluetooth enabled state
91
- * @property {boolean} btHeadphonesEnabled - Bluetooth headphones enabled
92
- * @property {string} clockFace - Clock face style
93
- * @property {string} dayDisplayBrightness - Day display brightness
94
- * @property {string} dayTime - Day mode start time
95
- * @property {string} dayYotoDaily - Day mode Yoto Daily content
96
- * @property {string} dayYotoRadio - Day mode Yoto Radio content
97
- * @property {string} displayDimBrightness - Display dim brightness level
98
- * @property {string} displayDimTimeout - Display dim timeout in seconds
99
- * @property {boolean} headphonesVolumeLimited - Headphones volume limited
100
- * @property {string} hourFormat - Hour format ("12" or "24")
101
- * @property {string} locale - Locale setting
102
- * @property {string} maxVolumeLimit - Max volume limit (0-16)
103
- * @property {string} nightAmbientColour - Night ambient LED color
104
- * @property {string} nightDisplayBrightness - Night display brightness
105
- * @property {string} nightMaxVolumeLimit - Night max volume limit (0-16)
106
- * @property {string} nightTime - Night mode start time
107
- * @property {string} nightYotoDaily - Night mode Yoto Daily content
108
- * @property {string} nightYotoRadio - Night mode Yoto Radio content
109
- * @property {boolean} repeatAll - Repeat all tracks enabled
110
- * @property {string} shutdownTimeout - Auto-shutdown timeout in seconds
111
- * @property {string} volumeLevel - Volume level preset
112
- */
113
-
114
- /**
115
- * @typedef {Object} YotoDeviceConfig
116
- * @property {string} name - Device name
117
- * @property {YotoDeviceConfigSettings} config - Device configuration settings
118
- */
119
-
120
- /**
121
- * @typedef {Object} YotoCardContent
122
- * @property {Object} [card] - Card information
123
- * @property {string} [card.cardId] - Card unique identifier
124
- * @property {string} [card.title] - Card title
125
- * @property {string} [card.slug] - Card slug
126
- * @property {string} [card.userId] - Owner user ID
127
- * @property {string} [card.createdAt] - Creation timestamp
128
- * @property {string} [card.updatedAt] - Update timestamp
129
- * @property {boolean} [card.deleted] - Whether card is deleted
130
- * @property {Object} [card.metadata] - Additional metadata
131
- * @property {string} [card.metadata.author] - Card author
132
- * @property {string} [card.metadata.category] - Content category
133
- * @property {string} [card.metadata.description] - Card description
134
- * @property {Object} [card.metadata.cover] - Cover image data
135
- * @property {string} [card.metadata.cover.imageL] - Large cover image URL
136
- * @property {Object} [card.metadata.media] - Media information
137
- * @property {number} [card.metadata.media.duration] - Total duration in seconds
138
- * @property {number} [card.metadata.media.fileSize] - File size in bytes
139
- * @property {YotoChapter[]} [card.chapters] - Card chapters
140
- */
141
-
142
- /**
143
- * @typedef {Object} YotoChapter
144
- * @property {string} key - Chapter key
145
- * @property {string} title - Chapter title
146
- * @property {string | null} availableFrom - Availability date (ISO8601)
147
- * @property {YotoTrack[]} tracks - Chapter tracks
148
- */
149
-
150
- /**
151
- * @typedef {Object} YotoTrack
152
- * @property {string} key - Track key
153
- * @property {string} title - Track title
154
- * @property {number} duration - Track duration in seconds
155
- */
156
-
157
- /**
158
- * @typedef {Object} YotoApiDevicesResponse
159
- * @property {YotoDevice[]} devices - Array of devices
160
- */
161
-
162
- /**
163
- * @typedef {Object} YotoApiTokenResponse
164
- * @property {string} access_token - Access token
165
- * @property {string} token_type - Token type (typically "Bearer")
166
- * @property {number} expires_in - Token lifetime in seconds
167
- * @property {string} [refresh_token] - Refresh token
168
- * @property {string} [scope] - Granted scopes
169
- * @property {string} [id_token] - ID token JWT
170
- */
171
-
172
- /**
173
- * @typedef {Object} YotoApiDeviceCodeResponse
174
- * @property {string} device_code - Device verification code
175
- * @property {string} user_code - User-facing code to enter
176
- * @property {string} verification_uri - URL for user to visit
177
- * @property {string} verification_uri_complete - Complete verification URL with code
178
- * @property {number} expires_in - Code lifetime in seconds
179
- * @property {number} interval - Minimum polling interval in seconds
180
- */
181
-
182
- /**
183
- * @typedef {Object} YotoAccessoryContext
184
- * @property {YotoDevice} device - Device information
185
- * @property {YotoDeviceStatus | null} lastStatus - Last known device status
186
- * @property {YotoPlaybackEvents | null} lastEvents - Last known playback events
187
- * @property {number} lastUpdate - Timestamp of last update
188
- * @property {YotoCardContent | null} [activeContentInfo] - Current active content information
189
- */
190
-
191
- /**
192
- * @typedef {Object} YotoPlatformConfig
193
- * @property {string} [platform] - Platform name (should be "Yoto")
194
- * @property {string} [name] - Platform instance name
195
- * @property {string} [clientId] - OAuth client ID
196
- * @property {string} [accessToken] - Stored access token
197
- * @property {string} [refreshToken] - Stored refresh token
198
- * @property {number} [tokenExpiresAt] - Token expiration timestamp
199
- * @property {string} [mqttBroker] - MQTT broker URL
200
- * @property {number} [statusTimeoutSeconds] - Seconds before marking device offline
201
- * @property {boolean} [exposeTemperature] - Expose temperature sensor
202
- * @property {boolean} [exposeBattery] - Expose battery service
203
- * @property {boolean} [exposeAdvancedControls] - Expose advanced control switches
204
- * @property {boolean} [exposeConnectionStatus] - Expose connection status sensor
205
- * @property {boolean} [exposeCardDetection] - Expose card detection sensor
206
- * @property {boolean} [exposeDisplayBrightness] - Expose display brightness control
207
- * @property {boolean} [exposeSleepTimer] - Expose sleep timer control
208
- * @property {boolean} [exposeVolumeLimits] - Expose volume limit controls
209
- * @property {boolean} [exposeAmbientLight] - Expose ambient light color control
210
- * @property {boolean} [exposeActiveContent] - Track and display active content information
211
- * @property {boolean} [updateAccessoryName] - Update accessory display name with current content
212
- * @property {string} [volumeControlType] - Volume control service type
213
- * @property {boolean} [debug] - Enable debug logging
214
- */
215
-
216
- /**
217
- * @typedef {Object} MqttCommandResponse
218
- * @property {Object} status - Response status
219
- * @property {string} req_body - Stringified request body
220
- */
221
-
222
- /**
223
- * MQTT command payloads
224
- */
225
-
226
- /**
227
- * @typedef {Object} MqttVolumeCommand
228
- * @property {number} volume - Volume level (0-100)
229
- */
230
-
231
- /**
232
- * @typedef {Object} MqttAmbientCommand
233
- * @property {number} r - Red intensity (0-255)
234
- * @property {number} g - Green intensity (0-255)
235
- * @property {number} b - Blue intensity (0-255)
236
- */
237
-
238
- /**
239
- * @typedef {Object} MqttSleepTimerCommand
240
- * @property {number} seconds - Sleep timer duration (0 to disable)
241
- */
242
-
243
- /**
244
- * @typedef {Object} MqttCardStartCommand
245
- * @property {string} uri - Card URI (e.g., "https://yoto.io/{cardId}")
246
- * @property {string} [chapterKey] - Chapter to start from
247
- * @property {string} [trackKey] - Track to start from
248
- * @property {number} [secondsIn] - Playback start offset in seconds
249
- * @property {number} [cutOff] - Playback stop offset in seconds
250
- * @property {boolean} [anyButtonStop] - Whether any button stops playback
251
- */
252
-
253
- export {}
package/lib/yotoApi.js DELETED
@@ -1,270 +0,0 @@
1
- /**
2
- * @fileoverview REST API client for Yoto API
3
- */
4
-
5
- /** @import { Logger } from 'homebridge' */
6
- /** @import { YotoApiDevicesResponse, YotoDevice, YotoDeviceConfig, YotoCardContent } from './types.js' */
7
- /** @import { YotoAuth } from './auth.js' */
8
-
9
- import {
10
- YOTO_API_BASE_URL,
11
- ERROR_MESSAGES,
12
- LOG_PREFIX
13
- } from './constants.js'
14
-
15
- /**
16
- * Yoto REST API client
17
- */
18
- export class YotoApi {
19
- /**
20
- * @param {Logger} log - Homebridge logger
21
- * @param {YotoAuth} auth - Authentication handler
22
- */
23
- constructor (log, auth) {
24
- this.log = log
25
- this.auth = auth
26
- this.accessToken = null
27
- this.refreshToken = null
28
- this.tokenExpiresAt = 0
29
- }
30
-
31
- /**
32
- * Set authentication tokens
33
- * @param {string} accessToken - Access token
34
- * @param {string} refreshToken - Refresh token
35
- * @param {number} expiresAt - Token expiration timestamp
36
- */
37
- setTokens (accessToken, refreshToken, expiresAt) {
38
- this.accessToken = accessToken
39
- this.refreshToken = refreshToken
40
- this.tokenExpiresAt = expiresAt
41
- }
42
-
43
- /**
44
- * Check if we have valid authentication
45
- * @returns {boolean}
46
- */
47
- hasAuth () {
48
- return !!this.accessToken
49
- }
50
-
51
- /**
52
- * Ensure we have a valid access token, refreshing if necessary
53
- * @returns {Promise<void>}
54
- */
55
- async ensureValidToken () {
56
- if (!this.accessToken) {
57
- throw new Error(ERROR_MESSAGES.NO_AUTH)
58
- }
59
-
60
- // Check if token needs refresh
61
- if (this.auth.isTokenExpired(this.tokenExpiresAt)) {
62
- this.log.debug(LOG_PREFIX.API, ERROR_MESSAGES.TOKEN_EXPIRED)
63
-
64
- if (!this.refreshToken) {
65
- throw new Error(ERROR_MESSAGES.TOKEN_REFRESH_FAILED)
66
- }
67
-
68
- try {
69
- const tokenResponse = await this.auth.refreshAccessToken(this.refreshToken)
70
- this.accessToken = tokenResponse.access_token
71
- this.tokenExpiresAt = this.auth.calculateExpiresAt(tokenResponse.expires_in)
72
-
73
- // Update refresh token if a new one was provided
74
- if (tokenResponse.refresh_token) {
75
- this.refreshToken = tokenResponse.refresh_token
76
- }
77
-
78
- // Notify platform to save updated tokens
79
- if (this.onTokenRefreshCallback) {
80
- this.onTokenRefreshCallback(this.accessToken, this.refreshToken, this.tokenExpiresAt)
81
- }
82
- } catch (error) {
83
- this.log.error(LOG_PREFIX.API, 'Token refresh failed:', error)
84
- // Throw specific error so platform can detect and restart auth flow
85
- throw new Error('TOKEN_REFRESH_FAILED')
86
- }
87
- }
88
- }
89
-
90
- /**
91
- * Make an authenticated API request
92
- * @param {string} endpoint - API endpoint path
93
- * @param {RequestInit & { _retried?: boolean }} [options] - Fetch options
94
- * @returns {Promise<any>}
95
- */
96
- async request (endpoint, options = {}) {
97
- await this.ensureValidToken()
98
-
99
- const url = `${YOTO_API_BASE_URL}${endpoint}`
100
-
101
- const headers = {
102
- Authorization: `Bearer ${this.accessToken}`,
103
- 'Content-Type': 'application/json',
104
- ...options.headers
105
- }
106
-
107
- try {
108
- this.log.debug(LOG_PREFIX.API, `${options.method || 'GET'} ${endpoint}`)
109
-
110
- const response = await fetch(url, {
111
- ...options,
112
- headers
113
- })
114
-
115
- if (!response.ok) {
116
- const errorText = await response.text()
117
-
118
- // Handle 401 by attempting token refresh once
119
- if (response.status === 401 && !options._retried) {
120
- this.log.warn(LOG_PREFIX.API, 'Received 401, forcing token refresh...')
121
- this.tokenExpiresAt = 0 // Force refresh
122
- await this.ensureValidToken()
123
- return this.request(endpoint, { ...options, _retried: true })
124
- }
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
-
133
- throw new Error(`API request failed: ${response.status} ${errorText}`)
134
- }
135
-
136
- // Return empty object for 204 No Content
137
- if (response.status === 204) {
138
- return {}
139
- }
140
-
141
- return await response.json()
142
- } catch (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
- }
147
- throw error
148
- }
149
- }
150
-
151
- /**
152
- * Get all devices associated with the authenticated user
153
- * @returns {Promise<YotoDevice[]>}
154
- */
155
- async getDevices () {
156
- this.log.debug(LOG_PREFIX.API, 'Fetching devices...')
157
-
158
- const response = /** @type {YotoApiDevicesResponse} */ (await this.request('/device-v2/devices/mine'))
159
-
160
- this.log.info(LOG_PREFIX.API, `Found ${response.devices.length} device(s)`)
161
- return response.devices
162
- }
163
-
164
- /**
165
- * Get device configuration
166
- * @param {string} deviceId - Device ID
167
- * @returns {Promise<YotoDeviceConfig>}
168
- */
169
- async getDeviceConfig (deviceId) {
170
- this.log.debug(LOG_PREFIX.API, `Fetching config for device ${deviceId}`)
171
-
172
- const config = /** @type {YotoDeviceConfig} */ (await this.request(`/device-v2/${deviceId}/config`))
173
- return config
174
- }
175
-
176
- /**
177
- * Update device configuration
178
- * @param {string} deviceId - Device ID
179
- * @param {YotoDeviceConfig} config - Updated configuration
180
- * @returns {Promise<YotoDeviceConfig>}
181
- */
182
- async updateDeviceConfig (deviceId, config) {
183
- this.log.debug(LOG_PREFIX.API, `Updating config for device ${deviceId}`)
184
-
185
- const updatedConfig = /** @type {YotoDeviceConfig} */ (await this.request(`/device-v2/${deviceId}/config`, {
186
- method: 'PUT',
187
- body: JSON.stringify(config)
188
- }))
189
-
190
- return updatedConfig
191
- }
192
-
193
- /**
194
- * Get content/card details
195
- * @param {string} cardId - Card ID
196
- * @param {Object} [options] - Query options
197
- * @param {string} [options.timezone] - Timezone for chapter availability
198
- * @param {boolean} [options.playable] - Return playable signed URLs
199
- * @returns {Promise<YotoCardContent>}
200
- */
201
- async getContent (cardId, options = {}) {
202
- this.log.debug(LOG_PREFIX.API, `Fetching content for card ${cardId}`)
203
-
204
- const queryParams = new URLSearchParams()
205
- if (options.timezone) {
206
- queryParams.append('timezone', options.timezone)
207
- }
208
- if (options.playable) {
209
- queryParams.append('playable', 'true')
210
- }
211
-
212
- const queryString = queryParams.toString()
213
- const endpoint = `/content/${cardId}${queryString ? `?${queryString}` : ''}`
214
-
215
- const content = /** @type {YotoCardContent} */ (await this.request(endpoint))
216
- return content
217
- }
218
-
219
- /**
220
- * Get user's MYO (Make Your Own) cards
221
- * @param {Object} [options] - Query options
222
- * @param {boolean} [options.showdeleted] - Show deleted cards
223
- * @returns {Promise<any>}
224
- */
225
- async getMyContent (options = {}) {
226
- this.log.debug(LOG_PREFIX.API, 'Fetching user content...')
227
-
228
- const queryParams = new URLSearchParams()
229
- if (options.showdeleted !== undefined) {
230
- queryParams.append('showdeleted', String(options.showdeleted))
231
- }
232
-
233
- const queryString = queryParams.toString()
234
- const endpoint = `/content/mine${queryString ? `?${queryString}` : ''}`
235
-
236
- const content = await this.request(endpoint)
237
- return content
238
- }
239
-
240
- /**
241
- * Get family library groups
242
- * @returns {Promise<any>}
243
- */
244
- async getLibraryGroups () {
245
- this.log.debug(LOG_PREFIX.API, 'Fetching library groups...')
246
-
247
- const groups = await this.request('/card/family/library/groups')
248
- return groups
249
- }
250
-
251
- /**
252
- * Get specific library group
253
- * @param {string} groupId - Group ID
254
- * @returns {Promise<any>}
255
- */
256
- async getLibraryGroup (groupId) {
257
- this.log.debug(LOG_PREFIX.API, `Fetching library group ${groupId}`)
258
-
259
- const group = await this.request(`/card/family/library/groups/${groupId}`)
260
- return group
261
- }
262
-
263
- /**
264
- * Set callback for token refresh events
265
- * @param {(accessToken: string, refreshToken: string, expiresAt: number) => void} callback
266
- */
267
- setTokenRefreshCallback (callback) {
268
- this.onTokenRefreshCallback = callback
269
- }
270
- }