import { Buffer } from 'node:buffer' import crypto from 'node:crypto' import fs from 'node:fs' import { HomebridgePluginUiServer } from '@homebridge/plugin-ui-utils' interface CustomRequestResponse { status: string data?: any } export class PluginUiServer extends HomebridgePluginUiServer { constructor() { super() // OAuth PKCE flow handler this.onRequest('startOAuthFlow', async (payload): Promise => { try { const { consumerKey, consumerSecret, code, codeVerifier } = payload || {} if (!consumerKey || !consumerSecret) { return { status: 'error', data: 'Missing consumerKey or consumerSecret' } } // If no code, generate PKCE and return authUrl for UI if (!code) { const generatedVerifier = crypto.randomBytes(32).toString('base64url') const challenge = crypto.createHash('sha256').update(generatedVerifier).digest('base64url') const redirectUri = 'urn:ietf:wg:oauth:2.0:oob' const authUrl = `https://api.honeywell.com/oauth2/authorize?response_type=code&client_id=${encodeURIComponent(consumerKey)}&redirect_uri=${encodeURIComponent(redirectUri)}&code_challenge=${encodeURIComponent(challenge)}&code_challenge_method=S256&appSelect=1` return { status: 'need_code', data: { authUrl, codeVerifier: generatedVerifier } } } // Exchange code for tokens if (!codeVerifier) { return { status: 'error', data: 'Missing PKCE code_verifier' } } const redirectUri = 'urn:ietf:wg:oauth:2.0:oob' const tokenRes = await fetch('https://api.honeywell.com/oauth2/token', { method: 'POST', headers: { 'Authorization': `Basic ${Buffer.from(`${consumerKey}:${consumerSecret}`).toString('base64')}`, 'Accept': 'application/json', 'Content-Type': 'application/x-www-form-urlencoded', }, body: `grant_type=authorization_code&code=${encodeURIComponent(code)}&redirect_uri=${encodeURIComponent(redirectUri)}&code_verifier=${encodeURIComponent(codeVerifier)}`, }) const tokenData = await tokenRes.json() if (tokenData.refresh_token) { return { status: 'ok', data: { refreshToken: tokenData.refresh_token, accessToken: tokenData.access_token } } } else { return { status: 'error', data: tokenData } } } catch (err) { return { status: 'error', data: (err && typeof err === 'object' && 'message' in err) ? (err as any).message : String(err) } } }) // Device discovery handler this.onRequest('discoverDevices', async (payload): Promise => { try { const { accessToken } = payload || {} if (!accessToken) { return { status: 'error', data: 'No access token provided' } } const res = await fetch('https://api.honeywell.com/v2/devices', { headers: { Authorization: `Bearer ${accessToken}`, Accept: 'application/json', }, }) if (!res.ok) { return { status: 'error', data: await res.text() } } const data = await res.json() return { status: 'ok', data } } catch (err) { return { status: 'error', data: (err && typeof err === 'object' && 'message' in err) ? (err as any).message : String(err) } } }) // Legacy: getCachedAccessories for older config-ui-x this.onRequest('/getCachedAccessories', async (): Promise => { try { const plugin = '@homebridge-plugins/homebridge-firstalert' const devicesToReturn: any[] = [] const accFile = `${this.homebridgeStoragePath}/accessories/cachedAccessories` if (fs.existsSync(accFile)) { const cachedAccessoriesData = await fs.promises.readFile(accFile, 'utf8') const cachedAccessories: any[] = JSON.parse(cachedAccessoriesData) cachedAccessories .filter((accessory: any) => accessory.plugin === plugin) .forEach((accessory: any) => devicesToReturn.push(accessory)) } return { status: 'ok', data: devicesToReturn } } catch (err) { return { status: 'error', data: [] } } }) this.ready() } } (() => new PluginUiServer())()