homebridge-yoto 0.0.31 → 0.0.32
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/homebridge-ui/public/client.js +428 -0
- package/homebridge-ui/public/index.html +138 -0
- package/package.json +12 -1
- package/.github/dependabot.yml +0 -18
- package/.github/funding.yml +0 -4
- package/.github/workflows/release.yml +0 -41
- package/.github/workflows/tests.yml +0 -37
- package/AGENTS.md +0 -138
- package/CHANGELOG.md +0 -16
- package/CONTRIBUTING.md +0 -34
- package/NOTES.md +0 -87
- package/PLAN.md +0 -425
- package/eslint.config.js +0 -7
- package/index.test.js +0 -7
- package/logo.png +0 -0
- package/pnpm-workspace.yaml +0 -4
- package/tsconfig.json +0 -14
|
@@ -0,0 +1,428 @@
|
|
|
1
|
+
/// <reference lib="dom" />
|
|
2
|
+
/* eslint-env browser */
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @fileoverview Client-side UI logic for Yoto Homebridge plugin OAuth authentication
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/** @import {IHomebridgePluginUi} from '@homebridge/plugin-ui-utils/ui.interface' */
|
|
9
|
+
/** @import { AuthConfigResponse, AuthStartResponse, AuthPollResponse, AuthPollSlowDownResponse } from '../server.js' */
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @global
|
|
13
|
+
* @type {IHomebridgePluginUi}
|
|
14
|
+
*/
|
|
15
|
+
const homebridge = window.homebridge
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* @typedef {Object} YotoConfig
|
|
19
|
+
* @property {string} [clientId] - OAuth client ID
|
|
20
|
+
* @property {string} [refreshToken] - Stored refresh token
|
|
21
|
+
* @property {string} [accessToken] - Stored access token
|
|
22
|
+
* @property {number} [tokenExpiresAt] - Token expiration timestamp
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
// State variables
|
|
26
|
+
/** @type {ReturnType<typeof setInterval> | null} */
|
|
27
|
+
let pollingInterval = null
|
|
28
|
+
/** @type {ReturnType<typeof setInterval> | null} */
|
|
29
|
+
let countdownInterval = null
|
|
30
|
+
/** @type {string | null} */
|
|
31
|
+
let deviceCode = null
|
|
32
|
+
/** @type {string | null} */
|
|
33
|
+
let clientId = null
|
|
34
|
+
let pollIntervalSeconds = 5
|
|
35
|
+
/** @type {YotoConfig[]} */
|
|
36
|
+
let pluginConfig = []
|
|
37
|
+
/** @type {string | null} */
|
|
38
|
+
let defaultClientId = null
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Initialize UI when ready
|
|
42
|
+
*/
|
|
43
|
+
async function initializeUI () {
|
|
44
|
+
// Button click handlers
|
|
45
|
+
const startAuthBtn = document.getElementById('startAuthButton')
|
|
46
|
+
const openUrlBtn = document.getElementById('openUrlButton')
|
|
47
|
+
const retryBtn = document.getElementById('retryButton')
|
|
48
|
+
const logoutBtn = document.getElementById('logoutButton')
|
|
49
|
+
|
|
50
|
+
if (startAuthBtn) startAuthBtn.addEventListener('click', startDeviceFlow)
|
|
51
|
+
if (openUrlBtn) openUrlBtn.addEventListener('click', openVerificationUrl)
|
|
52
|
+
if (retryBtn) retryBtn.addEventListener('click', retryAuth)
|
|
53
|
+
if (logoutBtn) logoutBtn.addEventListener('click', logout)
|
|
54
|
+
|
|
55
|
+
// Show schema-based config form below custom UI
|
|
56
|
+
homebridge.showSchemaForm()
|
|
57
|
+
|
|
58
|
+
// Load auth config and check authentication status
|
|
59
|
+
await loadAuthConfig()
|
|
60
|
+
await checkAuthStatus()
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Initialize on ready
|
|
64
|
+
homebridge.addEventListener('ready', initializeUI)
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Show a specific UI section and hide all others
|
|
68
|
+
* @param {string} sectionToShow - ID of section to show
|
|
69
|
+
* @param {Object} [options] - Optional parameters
|
|
70
|
+
* @param {string} [options.errorMessage] - Error message to display (for errorSection)
|
|
71
|
+
*/
|
|
72
|
+
function showSection (sectionToShow, options = {}) {
|
|
73
|
+
const sections = [
|
|
74
|
+
'statusMessage',
|
|
75
|
+
'authRequired',
|
|
76
|
+
'deviceCodeSection',
|
|
77
|
+
'authSuccess',
|
|
78
|
+
'errorSection'
|
|
79
|
+
]
|
|
80
|
+
|
|
81
|
+
for (const sectionId of sections) {
|
|
82
|
+
const el = document.getElementById(sectionId)
|
|
83
|
+
if (el) {
|
|
84
|
+
el.style.display = sectionId === sectionToShow ? 'block' : 'none'
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Set error message if provided
|
|
89
|
+
if (options.errorMessage) {
|
|
90
|
+
const errorMessageEl = document.getElementById('errorMessage')
|
|
91
|
+
if (errorMessageEl) errorMessageEl.textContent = options.errorMessage
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Show authentication required section
|
|
97
|
+
*/
|
|
98
|
+
function showAuthRequired () {
|
|
99
|
+
showSection('authRequired')
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Show authentication success
|
|
104
|
+
*/
|
|
105
|
+
function showAuthSuccess () {
|
|
106
|
+
showSection('authSuccess')
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Show error message
|
|
111
|
+
* @param {string} message - Error message to display
|
|
112
|
+
*/
|
|
113
|
+
function showError (message) {
|
|
114
|
+
showSection('errorSection', { errorMessage: message })
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Load authentication configuration from server
|
|
119
|
+
* @returns {Promise<void>}
|
|
120
|
+
*/
|
|
121
|
+
async function loadAuthConfig () {
|
|
122
|
+
try {
|
|
123
|
+
// Load plugin config first
|
|
124
|
+
pluginConfig = await homebridge.getPluginConfig()
|
|
125
|
+
if (!pluginConfig.length) {
|
|
126
|
+
pluginConfig.push({})
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** @type {AuthConfigResponse} */
|
|
130
|
+
const config = await homebridge.request('/auth/config')
|
|
131
|
+
defaultClientId = config.defaultClientId
|
|
132
|
+
|
|
133
|
+
// Populate the client ID input field
|
|
134
|
+
const clientIdInput = /** @type {HTMLInputElement | null} */ (document.getElementById('clientIdInput'))
|
|
135
|
+
const defaultClientIdDisplay = document.getElementById('defaultClientIdDisplay')
|
|
136
|
+
|
|
137
|
+
if (clientIdInput) {
|
|
138
|
+
// Use configured value or default
|
|
139
|
+
const currentConfig = pluginConfig[0] || {}
|
|
140
|
+
clientIdInput.value = currentConfig.clientId || defaultClientId
|
|
141
|
+
clientIdInput.placeholder = defaultClientId
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (defaultClientIdDisplay) {
|
|
145
|
+
defaultClientIdDisplay.textContent = defaultClientId
|
|
146
|
+
}
|
|
147
|
+
} catch (error) {
|
|
148
|
+
console.error('Failed to load auth config:', error)
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Start OAuth device flow
|
|
154
|
+
* @returns {Promise<void>}
|
|
155
|
+
*/
|
|
156
|
+
async function startDeviceFlow () {
|
|
157
|
+
try {
|
|
158
|
+
homebridge.showSpinner()
|
|
159
|
+
|
|
160
|
+
// Get client ID from input field (which may have been edited by user)
|
|
161
|
+
const clientIdInput = /** @type {HTMLInputElement | null} */ (document.getElementById('clientIdInput'))
|
|
162
|
+
const clientIdToUse = clientIdInput?.value || defaultClientId || undefined
|
|
163
|
+
|
|
164
|
+
// Save the client ID to config if it's different from what's stored
|
|
165
|
+
const config = pluginConfig[0] || {}
|
|
166
|
+
if (clientIdToUse && clientIdToUse !== config.clientId) {
|
|
167
|
+
if (!pluginConfig[0]) pluginConfig[0] = {}
|
|
168
|
+
pluginConfig[0].clientId = clientIdToUse
|
|
169
|
+
await homebridge.updatePluginConfig(pluginConfig)
|
|
170
|
+
await homebridge.savePluginConfig()
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** @type {AuthStartResponse} */
|
|
174
|
+
const response = await homebridge.request('/auth/start', {
|
|
175
|
+
clientId: clientIdToUse
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
// Store device code and client ID for polling
|
|
179
|
+
deviceCode = response.device_code
|
|
180
|
+
clientId = response.client_id
|
|
181
|
+
pollIntervalSeconds = response.interval
|
|
182
|
+
|
|
183
|
+
// Show device code section
|
|
184
|
+
showSection('deviceCodeSection')
|
|
185
|
+
|
|
186
|
+
// Set verification URL (complete with code)
|
|
187
|
+
const verificationUrlCompleteEl = /** @type {HTMLInputElement | null} */ (document.getElementById('verificationUrlComplete'))
|
|
188
|
+
if (verificationUrlCompleteEl) verificationUrlCompleteEl.value = response.verification_uri_complete
|
|
189
|
+
|
|
190
|
+
// Set user code
|
|
191
|
+
const userCodeEl = /** @type {HTMLInputElement | null} */ (document.getElementById('userCode'))
|
|
192
|
+
if (userCodeEl) userCodeEl.value = response.user_code
|
|
193
|
+
|
|
194
|
+
// Start countdown
|
|
195
|
+
startCountdown(response.expires_in)
|
|
196
|
+
|
|
197
|
+
// Start polling for token
|
|
198
|
+
startPolling()
|
|
199
|
+
|
|
200
|
+
homebridge.hideSpinner()
|
|
201
|
+
} catch (error) {
|
|
202
|
+
homebridge.hideSpinner()
|
|
203
|
+
|
|
204
|
+
// Debug: Log the raw error
|
|
205
|
+
console.error('Start auth error (raw):', error)
|
|
206
|
+
console.error('Start auth error (type):', typeof error)
|
|
207
|
+
console.error('Start auth error (keys):', error && typeof error === 'object' ? Object.keys(error) : 'N/A')
|
|
208
|
+
|
|
209
|
+
// Extract error message from various error formats
|
|
210
|
+
let errorMessage = 'Failed to start authentication'
|
|
211
|
+
if (error && typeof error === 'object') {
|
|
212
|
+
if ('message' in error && error.message) {
|
|
213
|
+
errorMessage = String(error.message)
|
|
214
|
+
} else if ('error' in error && error.error) {
|
|
215
|
+
errorMessage = String(error.error)
|
|
216
|
+
} else {
|
|
217
|
+
errorMessage = JSON.stringify(error)
|
|
218
|
+
}
|
|
219
|
+
} else if (error) {
|
|
220
|
+
errorMessage = String(error)
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
console.error('Start auth error (extracted message):', errorMessage)
|
|
224
|
+
|
|
225
|
+
homebridge.toast.error('Failed to start authentication', errorMessage)
|
|
226
|
+
showError(errorMessage)
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Start countdown timer
|
|
232
|
+
* @param {number} expiresIn - Seconds until expiration
|
|
233
|
+
*/
|
|
234
|
+
function startCountdown (expiresIn) {
|
|
235
|
+
let remaining = expiresIn
|
|
236
|
+
const totalTime = expiresIn
|
|
237
|
+
|
|
238
|
+
countdownInterval = setInterval(() => {
|
|
239
|
+
remaining--
|
|
240
|
+
|
|
241
|
+
const minutes = Math.floor(remaining / 60)
|
|
242
|
+
const seconds = remaining % 60
|
|
243
|
+
const percentage = (remaining / totalTime) * 100
|
|
244
|
+
|
|
245
|
+
const countdownTextEl = document.getElementById('countdownText')
|
|
246
|
+
const countdownBarEl = /** @type {HTMLElement | null} */ (document.getElementById('countdownBar'))
|
|
247
|
+
|
|
248
|
+
if (countdownTextEl) countdownTextEl.textContent = `${minutes}:${seconds.toString().padStart(2, '0')}`
|
|
249
|
+
if (countdownBarEl) countdownBarEl.style.width = percentage + '%'
|
|
250
|
+
|
|
251
|
+
if (percentage < 30) {
|
|
252
|
+
const barEl = document.getElementById('countdownBar')
|
|
253
|
+
if (barEl) barEl.className = 'progress-bar bg-danger'
|
|
254
|
+
} else if (percentage < 60) {
|
|
255
|
+
const barEl = document.getElementById('countdownBar')
|
|
256
|
+
if (barEl) barEl.className = 'progress-bar bg-warning'
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (remaining <= 0) {
|
|
260
|
+
if (countdownInterval) clearInterval(countdownInterval)
|
|
261
|
+
if (pollingInterval) clearInterval(pollingInterval)
|
|
262
|
+
showError('The authorization code has expired. Please try again.')
|
|
263
|
+
}
|
|
264
|
+
}, 1000)
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Start polling for token
|
|
269
|
+
*/
|
|
270
|
+
function startPolling () {
|
|
271
|
+
pollingInterval = setInterval(async () => {
|
|
272
|
+
try {
|
|
273
|
+
/** @type {AuthPollResponse} */
|
|
274
|
+
const result = await homebridge.request('/auth/poll', {
|
|
275
|
+
deviceCode: deviceCode || '',
|
|
276
|
+
clientId: clientId || ''
|
|
277
|
+
})
|
|
278
|
+
|
|
279
|
+
// Type guard: check if success response
|
|
280
|
+
if ('success' in result && result.success) {
|
|
281
|
+
// Success! Update config with tokens
|
|
282
|
+
if (pollingInterval) clearInterval(pollingInterval)
|
|
283
|
+
if (countdownInterval) clearInterval(countdownInterval)
|
|
284
|
+
|
|
285
|
+
// Update plugin config with new tokens
|
|
286
|
+
if (!pluginConfig[0]) pluginConfig[0] = {}
|
|
287
|
+
// Type narrowing: result.success is true, so we have AuthPollSuccessResponse
|
|
288
|
+
pluginConfig[0].refreshToken = result.refreshToken
|
|
289
|
+
pluginConfig[0].accessToken = result.accessToken
|
|
290
|
+
pluginConfig[0].tokenExpiresAt = result.tokenExpiresAt
|
|
291
|
+
|
|
292
|
+
// Ensure clientId is set (use the one we got from the flow)
|
|
293
|
+
if (!pluginConfig[0].clientId && clientId) {
|
|
294
|
+
pluginConfig[0].clientId = clientId
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Save to Homebridge config
|
|
298
|
+
await homebridge.updatePluginConfig(pluginConfig)
|
|
299
|
+
await homebridge.savePluginConfig()
|
|
300
|
+
|
|
301
|
+
// Refresh the schema form to show updated token fields
|
|
302
|
+
homebridge.hideSchemaForm()
|
|
303
|
+
setTimeout(() => {
|
|
304
|
+
homebridge.showSchemaForm()
|
|
305
|
+
}, 100)
|
|
306
|
+
|
|
307
|
+
homebridge.toast.success('Authentication successful!')
|
|
308
|
+
homebridge.toast.info('Please restart the plugin for changes to take effect', 'Restart Required')
|
|
309
|
+
showAuthSuccess()
|
|
310
|
+
} else if ('slow_down' in result && result.slow_down) {
|
|
311
|
+
// Type guard: check if slow_down response
|
|
312
|
+
// Use the interval from the server response, or increase by 1.5x as fallback
|
|
313
|
+
if (pollingInterval) clearInterval(pollingInterval)
|
|
314
|
+
const slowDownResult = /** @type {AuthPollSlowDownResponse} */ (result)
|
|
315
|
+
pollIntervalSeconds = slowDownResult.interval || (pollIntervalSeconds * 1.5)
|
|
316
|
+
console.log('[Client] Slowing down polling to', pollIntervalSeconds, 'seconds')
|
|
317
|
+
startPolling()
|
|
318
|
+
}
|
|
319
|
+
// If pending (has 'pending' property), just continue polling
|
|
320
|
+
} catch (error) {
|
|
321
|
+
// Stop polling on error
|
|
322
|
+
if (pollingInterval) clearInterval(pollingInterval)
|
|
323
|
+
if (countdownInterval) clearInterval(countdownInterval)
|
|
324
|
+
|
|
325
|
+
// Debug: Log the raw error
|
|
326
|
+
console.error('Poll error (raw):', error)
|
|
327
|
+
console.error('Poll error (type):', typeof error)
|
|
328
|
+
console.error('Poll error (keys):', error && typeof error === 'object' ? Object.keys(error) : 'N/A')
|
|
329
|
+
|
|
330
|
+
// Extract error message from various error formats
|
|
331
|
+
let errorMessage = 'Authentication failed'
|
|
332
|
+
if (error && typeof error === 'object') {
|
|
333
|
+
if ('message' in error && error.message) {
|
|
334
|
+
errorMessage = String(error.message)
|
|
335
|
+
} else if ('error' in error && error.error) {
|
|
336
|
+
errorMessage = String(error.error)
|
|
337
|
+
} else {
|
|
338
|
+
errorMessage = JSON.stringify(error)
|
|
339
|
+
}
|
|
340
|
+
} else if (error) {
|
|
341
|
+
errorMessage = String(error)
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
console.error('Poll error (extracted message):', errorMessage)
|
|
345
|
+
|
|
346
|
+
homebridge.toast.error('Authentication failed', errorMessage)
|
|
347
|
+
showError(errorMessage)
|
|
348
|
+
}
|
|
349
|
+
}, pollIntervalSeconds * 1000)
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Open verification URL in new window
|
|
354
|
+
*/
|
|
355
|
+
function openVerificationUrl () {
|
|
356
|
+
const urlEl = /** @type {HTMLInputElement | null} */ (document.getElementById('verificationUrlComplete'))
|
|
357
|
+
if (urlEl && urlEl.value) {
|
|
358
|
+
window.open(urlEl.value, '_blank')
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Retry authentication
|
|
364
|
+
*/
|
|
365
|
+
function retryAuth () {
|
|
366
|
+
showAuthRequired()
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Logout - clear tokens and restart auth flow
|
|
371
|
+
*/
|
|
372
|
+
async function logout () {
|
|
373
|
+
try {
|
|
374
|
+
homebridge.showSpinner()
|
|
375
|
+
|
|
376
|
+
// Clear tokens from config
|
|
377
|
+
if (pluginConfig[0]) {
|
|
378
|
+
delete pluginConfig[0].refreshToken
|
|
379
|
+
delete pluginConfig[0].accessToken
|
|
380
|
+
delete pluginConfig[0].tokenExpiresAt
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// Save cleared config
|
|
384
|
+
await homebridge.updatePluginConfig(pluginConfig)
|
|
385
|
+
await homebridge.savePluginConfig()
|
|
386
|
+
|
|
387
|
+
homebridge.hideSpinner()
|
|
388
|
+
homebridge.toast.success('Logged out successfully')
|
|
389
|
+
|
|
390
|
+
// Show auth required screen
|
|
391
|
+
showAuthRequired()
|
|
392
|
+
} catch (error) {
|
|
393
|
+
homebridge.hideSpinner()
|
|
394
|
+
const errorMessage = error && typeof error === 'object' && 'message' in error
|
|
395
|
+
? String(error.message)
|
|
396
|
+
: String(error)
|
|
397
|
+
homebridge.toast.error('Logout failed', errorMessage)
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* Check initial authentication status
|
|
403
|
+
* @returns {Promise<void>}
|
|
404
|
+
*/
|
|
405
|
+
async function checkAuthStatus () {
|
|
406
|
+
try {
|
|
407
|
+
// pluginConfig is already loaded by loadAuthConfig
|
|
408
|
+
const config = pluginConfig[0]
|
|
409
|
+
|
|
410
|
+
// Check if we have tokens configured (don't validate them, just check they exist)
|
|
411
|
+
const hasRefreshToken = !!config?.refreshToken
|
|
412
|
+
const hasAccessToken = !!config?.accessToken
|
|
413
|
+
|
|
414
|
+
if (hasRefreshToken && hasAccessToken) {
|
|
415
|
+
showAuthSuccess()
|
|
416
|
+
} else {
|
|
417
|
+
showAuthRequired()
|
|
418
|
+
// Populate client ID field if we're showing auth required
|
|
419
|
+
const clientIdInput = /** @type {HTMLInputElement | null} */ (document.getElementById('clientIdInput'))
|
|
420
|
+
if (clientIdInput && defaultClientId) {
|
|
421
|
+
clientIdInput.value = config?.clientId || defaultClientId
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
} catch (error) {
|
|
425
|
+
console.error('Failed to check auth status:', error)
|
|
426
|
+
showAuthRequired()
|
|
427
|
+
}
|
|
428
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
<!-- Load client script as module -->
|
|
2
|
+
<script type="module" src="client.js"></script>
|
|
3
|
+
|
|
4
|
+
<div class="card card-body">
|
|
5
|
+
<!-- Authentication Status -->
|
|
6
|
+
<div id="authStatus">
|
|
7
|
+
<h4>Authentication Status</h4>
|
|
8
|
+
<div id="statusMessage" class="alert alert-info">
|
|
9
|
+
Checking authentication status...
|
|
10
|
+
</div>
|
|
11
|
+
</div>
|
|
12
|
+
|
|
13
|
+
<!-- Authentication Required Section -->
|
|
14
|
+
<div id="authRequired" style="display: none">
|
|
15
|
+
<div class="alert alert-warning">
|
|
16
|
+
<h5>Yoto Authentication Required</h5>
|
|
17
|
+
<p>
|
|
18
|
+
To connect your Yoto devices to HomeKit, please authorize this plugin:
|
|
19
|
+
</p>
|
|
20
|
+
|
|
21
|
+
<ol>
|
|
22
|
+
<li>Click the button below to start authentication</li>
|
|
23
|
+
<li>Visit the Yoto website when prompted</li>
|
|
24
|
+
<li>Enter the code shown</li>
|
|
25
|
+
<li>Wait for confirmation</li>
|
|
26
|
+
</ol>
|
|
27
|
+
</div>
|
|
28
|
+
|
|
29
|
+
<!-- Advanced Settings -->
|
|
30
|
+
<details class="card mb-3">
|
|
31
|
+
<summary class="card-header" style="cursor: pointer">
|
|
32
|
+
<strong>Advanced Settings</strong>
|
|
33
|
+
<small class="text-muted">(optional)</small>
|
|
34
|
+
</summary>
|
|
35
|
+
<div class="card-body">
|
|
36
|
+
<div class="form-group">
|
|
37
|
+
<label for="clientIdInput">OAuth Client ID</label>
|
|
38
|
+
<input
|
|
39
|
+
type="text"
|
|
40
|
+
class="form-control"
|
|
41
|
+
id="clientIdInput"
|
|
42
|
+
placeholder="Loading..."
|
|
43
|
+
/>
|
|
44
|
+
<small class="grey-text help-block small" id="clientIdHelp">
|
|
45
|
+
Default: <code id="defaultClientIdDisplay">Loading...</code>. Only
|
|
46
|
+
change this if you have your own Yoto OAuth application.
|
|
47
|
+
</small>
|
|
48
|
+
</div>
|
|
49
|
+
</div>
|
|
50
|
+
</details>
|
|
51
|
+
|
|
52
|
+
<div class="text-center">
|
|
53
|
+
<button id="startAuthButton" type="button" class="btn btn-primary btn-lg">
|
|
54
|
+
Start Authentication
|
|
55
|
+
</button>
|
|
56
|
+
</div>
|
|
57
|
+
</div>
|
|
58
|
+
|
|
59
|
+
<!-- Device Code Display -->
|
|
60
|
+
<div id="deviceCodeSection" style="display: none">
|
|
61
|
+
<div class="alert alert-info">
|
|
62
|
+
<h5>Complete Authentication</h5>
|
|
63
|
+
|
|
64
|
+
<div class="form-group">
|
|
65
|
+
<label>Click to authorize (code included in URL)</label>
|
|
66
|
+
<div class="input-group">
|
|
67
|
+
<input
|
|
68
|
+
type="text"
|
|
69
|
+
class="form-control"
|
|
70
|
+
id="verificationUrlComplete"
|
|
71
|
+
readonly
|
|
72
|
+
/>
|
|
73
|
+
<div class="input-group-append">
|
|
74
|
+
<button class="btn btn-primary" type="button" id="openUrlButton">
|
|
75
|
+
Open & Authorize
|
|
76
|
+
</button>
|
|
77
|
+
</div>
|
|
78
|
+
</div>
|
|
79
|
+
</div>
|
|
80
|
+
|
|
81
|
+
<div class="form-group">
|
|
82
|
+
<label>Or manually enter this code</label>
|
|
83
|
+
<input
|
|
84
|
+
type="text"
|
|
85
|
+
class="form-control form-control-lg text-center"
|
|
86
|
+
id="userCode"
|
|
87
|
+
readonly
|
|
88
|
+
style="font-family: monospace; font-weight: bold"
|
|
89
|
+
/>
|
|
90
|
+
</div>
|
|
91
|
+
|
|
92
|
+
<div class="form-group">
|
|
93
|
+
<label>Time remaining</label>
|
|
94
|
+
<div class="progress">
|
|
95
|
+
<div
|
|
96
|
+
id="countdownBar"
|
|
97
|
+
class="progress-bar"
|
|
98
|
+
role="progressbar"
|
|
99
|
+
style="width: 100%"
|
|
100
|
+
>
|
|
101
|
+
<span id="countdownText">15:00</span>
|
|
102
|
+
</div>
|
|
103
|
+
</div>
|
|
104
|
+
</div>
|
|
105
|
+
|
|
106
|
+
<p class="text-center">
|
|
107
|
+
<small>Waiting for authorization...</small>
|
|
108
|
+
</p>
|
|
109
|
+
</div>
|
|
110
|
+
</div>
|
|
111
|
+
|
|
112
|
+
<!-- Success Message -->
|
|
113
|
+
<div id="authSuccess" style="display: none">
|
|
114
|
+
<div class="alert alert-success">
|
|
115
|
+
<h5>✓ Authentication Successful!</h5>
|
|
116
|
+
<p>
|
|
117
|
+
Your Yoto account is connected. The plugin will now discover your
|
|
118
|
+
devices.
|
|
119
|
+
</p>
|
|
120
|
+
</div>
|
|
121
|
+
<div class="text-center">
|
|
122
|
+
<button id="logoutButton" type="button" class="btn btn-outline-danger">
|
|
123
|
+
Logout
|
|
124
|
+
</button>
|
|
125
|
+
</div>
|
|
126
|
+
</div>
|
|
127
|
+
|
|
128
|
+
<!-- Error Display -->
|
|
129
|
+
<div id="errorSection" style="display: none">
|
|
130
|
+
<div class="alert alert-danger">
|
|
131
|
+
<h5>Authentication Error</h5>
|
|
132
|
+
<p id="errorMessage"></p>
|
|
133
|
+
<button id="retryButton" type="button" class="btn btn-danger">
|
|
134
|
+
Try Again
|
|
135
|
+
</button>
|
|
136
|
+
</div>
|
|
137
|
+
</div>
|
|
138
|
+
</div>
|
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.
|
|
4
|
+
"version": "0.0.32",
|
|
5
5
|
"author": "Bret Comnes <bcomnes@gmail.com> (https://bret.io)",
|
|
6
6
|
"bugs": {
|
|
7
7
|
"url": "https://github.com/bcomnes/homebridge-yoto/issues"
|
|
@@ -38,6 +38,17 @@
|
|
|
38
38
|
"module": "index.js",
|
|
39
39
|
"main": "index.js",
|
|
40
40
|
"types": "index.d.ts",
|
|
41
|
+
"files": [
|
|
42
|
+
"index.js",
|
|
43
|
+
"index.d.ts",
|
|
44
|
+
"index.d.ts.map",
|
|
45
|
+
"config.schema.json",
|
|
46
|
+
"config.schema.cjs",
|
|
47
|
+
"lib/",
|
|
48
|
+
"homebridge-ui/",
|
|
49
|
+
"README.md",
|
|
50
|
+
"LICENSE"
|
|
51
|
+
],
|
|
41
52
|
"repository": {
|
|
42
53
|
"type": "git",
|
|
43
54
|
"url": "https://github.com/bcomnes/homebridge-yoto.git"
|
package/.github/dependabot.yml
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
# https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
|
|
2
|
-
version: 2
|
|
3
|
-
updates:
|
|
4
|
-
- package-ecosystem: "npm"
|
|
5
|
-
directory: "/"
|
|
6
|
-
# Check the npm registry for updates every day (weekdays)
|
|
7
|
-
schedule:
|
|
8
|
-
interval: "daily"
|
|
9
|
-
groups:
|
|
10
|
-
typescript:
|
|
11
|
-
patterns:
|
|
12
|
-
- "typescript"
|
|
13
|
-
- "@types/node"
|
|
14
|
-
- "@voxpelli/tsconfig"
|
|
15
|
-
- package-ecosystem: "github-actions"
|
|
16
|
-
directory: "/"
|
|
17
|
-
schedule:
|
|
18
|
-
interval: "daily"
|
package/.github/funding.yml
DELETED
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
name: npm bump
|
|
2
|
-
|
|
3
|
-
on:
|
|
4
|
-
workflow_dispatch:
|
|
5
|
-
inputs:
|
|
6
|
-
newversion:
|
|
7
|
-
description: 'npm version {major,minor,patch}'
|
|
8
|
-
required: true
|
|
9
|
-
|
|
10
|
-
env:
|
|
11
|
-
FORCE_COLOR: 1
|
|
12
|
-
|
|
13
|
-
concurrency: # prevent concurrent releases
|
|
14
|
-
group: npm-bump
|
|
15
|
-
cancel-in-progress: true
|
|
16
|
-
|
|
17
|
-
jobs:
|
|
18
|
-
version_and_release:
|
|
19
|
-
runs-on: ubuntu-latest
|
|
20
|
-
steps:
|
|
21
|
-
- uses: actions/checkout@v6
|
|
22
|
-
with:
|
|
23
|
-
# fetch full history so things like auto-changelog work properly
|
|
24
|
-
fetch-depth: 0
|
|
25
|
-
- name: Use Node.js ${{ env.node }}
|
|
26
|
-
uses: actions/setup-node@v6
|
|
27
|
-
with:
|
|
28
|
-
node-version-file: package.json
|
|
29
|
-
# setting a registry enables the NODE_AUTH_TOKEN env variable where we can set an npm token. REQUIRED
|
|
30
|
-
registry-url: 'https://registry.npmjs.org'
|
|
31
|
-
- run: npm i
|
|
32
|
-
- run: npm test
|
|
33
|
-
- name: npm version && npm publish
|
|
34
|
-
uses: bcomnes/npm-bump@v2
|
|
35
|
-
with:
|
|
36
|
-
git_email: bcomnes@gmail.com
|
|
37
|
-
git_username: ${{ github.actor }}
|
|
38
|
-
newversion: ${{ github.event.inputs.newversion }}
|
|
39
|
-
github_token: ${{ secrets.GITHUB_TOKEN }} # built in actions token. Passed tp gh-release if in use.
|
|
40
|
-
npm_token: ${{ secrets.NPM_TOKEN }} # user set secret token generated at npm
|
|
41
|
-
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
name: tests
|
|
2
|
-
|
|
3
|
-
on: [pull_request, push]
|
|
4
|
-
|
|
5
|
-
env:
|
|
6
|
-
FORCE_COLOR: 1
|
|
7
|
-
|
|
8
|
-
jobs:
|
|
9
|
-
test:
|
|
10
|
-
runs-on: ${{ matrix.os }}
|
|
11
|
-
|
|
12
|
-
strategy:
|
|
13
|
-
fail-fast: false
|
|
14
|
-
matrix:
|
|
15
|
-
os: [ubuntu-latest]
|
|
16
|
-
node: ['lts/*']
|
|
17
|
-
|
|
18
|
-
steps:
|
|
19
|
-
- uses: actions/checkout@v6
|
|
20
|
-
- name: Use Node.js ${{ matrix.node }}
|
|
21
|
-
uses: actions/setup-node@v6
|
|
22
|
-
with:
|
|
23
|
-
node-version: ${{ matrix.node }}
|
|
24
|
-
- run: npm i
|
|
25
|
-
- run: npm test --color=always
|
|
26
|
-
|
|
27
|
-
automerge:
|
|
28
|
-
needs: test
|
|
29
|
-
runs-on: ubuntu-latest
|
|
30
|
-
permissions:
|
|
31
|
-
pull-requests: write
|
|
32
|
-
contents: write
|
|
33
|
-
steps:
|
|
34
|
-
- uses: fastify/github-action-merge-dependabot@v3
|
|
35
|
-
if: ${{ github.actor == 'dependabot[bot]' && github.event_name == 'pull_request' && contains(github.head_ref, 'dependabot/github_actions') }}
|
|
36
|
-
with:
|
|
37
|
-
github-token: ${{secrets.github_token}}
|