homebridge-yoto 0.0.27 → 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/AGENTS.md +42 -157
- package/CHANGELOG.md +13 -5
- package/NOTES.md +87 -0
- package/PLAN.md +320 -504
- package/README.md +18 -314
- package/config.schema.cjs +3 -0
- package/config.schema.json +19 -155
- package/homebridge-ui/server.js +264 -0
- package/index.js +1 -1
- package/index.test.js +1 -1
- package/lib/accessory.js +1870 -0
- package/lib/constants.js +8 -149
- package/lib/platform.js +303 -364
- package/lib/sanitize-name.js +49 -0
- package/lib/settings.js +16 -0
- package/lib/sync-service-names.js +34 -0
- package/logo.png +0 -0
- package/package.json +17 -22
- package/pnpm-workspace.yaml +4 -0
- package/declaration.tsconfig.json +0 -15
- package/lib/auth.js +0 -237
- package/lib/playerAccessory.js +0 -1724
- package/lib/types.js +0 -253
- package/lib/yotoApi.js +0 -270
- package/lib/yotoMqtt.js +0 -570
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
|
-
}
|