import type { API, DynamicPlatformPlugin, Logger, PlatformAccessory, PlatformConfig } from 'homebridge' import { ResideoClient } from './api/resideoClient.js' import { Thermostats } from './devices/thermostats.js' import { LeakSensor } from './devices/leaksensors.js' import { Valve } from './devices/valve.js' import { deviceBase } from './devices/device.js' import { SmokeSensor } from './devices/smoke.js' export class ResideoPlatform implements DynamicPlatformPlugin { private readonly log: Logger private readonly config: PlatformConfig private readonly api: API private readonly pollInterval: number public readonly client!: ResideoClient private pollTimer?: any private accessories: PlatformAccessory[] = [] private deviceInstances: Record = {} constructor(log: Logger, config: PlatformConfig, api: API) { this.log = log this.config = config this.api = api this.pollInterval = config.pollInterval || 60 if (!config.refreshToken) { this.log.error('No refresh token configured. Please log in via the Homebridge UI.') return } this.client = new ResideoClient(config.refreshToken) api.on('didFinishLaunching', () => this.discoverDevices()) } async discoverDevices() { try { const account = await this.client.getAccount() for (const device of account.devices) { // Find or create accessory let accessory = this.accessories.find(a => a.context.deviceID === device.deviceId) if (!accessory) { accessory = new this.api.platformAccessory(device.name, this.api.hap.uuid.generate(device.deviceId)) accessory.context.deviceID = device.deviceId accessory.context.deviceType = device.globalDeviceType this.api.registerPlatformAccessories('@homebridge-plugins/homebridge-firstalert', 'FirstAlert', [accessory]) this.accessories.push(accessory) this.log.info(`Registered new accessory: ${device.name} (${device.deviceId})`) } // Instantiate correct device class let instance: deviceBase | undefined switch (device.globalDeviceType) { case 'thermostat': instance = new Thermostats(this, accessory, device as any) break case 'leakSensor': instance = new LeakSensor(this, accessory, device as any) break case 'valve': instance = new Valve(this, accessory, device as any) break case 'smoke': case 'smokeSensor': case 'smokeDetector': case 'carbonMonoxideSensor': case 'carbonDioxideSensor': instance = new SmokeSensor(this, accessory, device as any) break default: this.log.warn(`Unknown device type: ${device.globalDeviceType} (${device.deviceId})`) } if (instance) { this.deviceInstances[device.deviceId] = instance } this.log.info(`Discovered device: ${device.name} (${device.deviceId})`) } this.startPolling(account.devices.map(d => d.deviceId)) } catch (err) { this.log.error('Failed to discover devices:', err) } } startPolling(deviceIds: string[]) { if (this.pollTimer) { clearInterval(this.pollTimer) } this.pollTimer = setInterval(() => this.pollDevices(deviceIds), this.pollInterval * 1000) this.pollDevices(deviceIds) // Initial poll } async pollDevices(deviceIds: string[]) { for (const deviceId of deviceIds) { const instance = this.deviceInstances[deviceId] if (instance && typeof (instance as any).updateState === 'function') { try { await (instance as any).updateState() } catch (err) { this.log.warn(`Failed to update state for device ${deviceId}:`, err) } } else { try { const state = await this.client.getDeviceState(deviceId) this.log.debug(`Polled device ${deviceId}:`, JSON.stringify(state)) } catch (err) { this.log.warn(`Failed to poll device ${deviceId}:`, err) } } } } configureAccessory(accessory: PlatformAccessory) { // Required for restoring cached accessories this.accessories.push(accessory) } }