import { TapoApi } from './protocols/tapo-api'; import { TapoLightState } from './lightstate/tapo-lightstate'; import { KasaLightState } from './lightstate/kasa-lightstate'; import { decrypt, encrypt } from './utils/kasa-utils'; import axios from 'axios'; export enum DeviceProtocol { KASA = 'kasa', TAPO = 'tapo', } export interface UnifiedDevice { id: string; name: string; host: string; protocol: DeviceProtocol; deviceType: string; model: string; api?: TapoApi; } export class UnifiedDeviceFactory { private tapoApi?: TapoApi; constructor(config?: { email?: string; password?: string; token?: string }) { if (config) { this.tapoApi = new TapoApi(config); } } // Set power state for any device type async setPower(device: UnifiedDevice, power: boolean): Promise { if (device.protocol === DeviceProtocol.TAPO) { if (!this.tapoApi) { throw new Error('Tapo API not initialized'); } return await this.tapoApi.sendPower({ device: { id: device.id }, power }); } else if (device.protocol === DeviceProtocol.KASA) { return await this.sendKasaCommand(device, { system: { set_relay_state: { state: power ? 1 : 0 } } }); } return false; } // Set light state for any device type async setLightState(device: UnifiedDevice, state: any): Promise { if (device.protocol === DeviceProtocol.TAPO) { if (!this.tapoApi) { throw new Error('Tapo API not initialized'); } const tapoState = new TapoLightState(state); return await this.tapoApi.sendState({ device: { id: device.id }, state: tapoState }); } else if (device.protocol === DeviceProtocol.KASA) { const kasaState = new KasaLightState(state); const values = kasaState.getValues(); return await this.sendKasaCommand(device, { 'smartlife.iot.smartbulb.lightingservice': { transition_light_state: values } }); } return false; } // Get device info for any device type async getDeviceInfo(device: UnifiedDevice): Promise { if (device.protocol === DeviceProtocol.TAPO) { if (!this.tapoApi) { throw new Error('Tapo API not initialized'); } return await this.tapoApi.getDeviceInfo(device.id); } else if (device.protocol === DeviceProtocol.KASA) { return await this.sendKasaCommand(device, { system: { get_sysinfo: null } }); } return null; } // Get energy usage (for compatible devices) async getEnergyUsage(device: UnifiedDevice): Promise { if (device.protocol === DeviceProtocol.TAPO) { if (!this.tapoApi) { throw new Error('Tapo API not initialized'); } return await this.tapoApi.getEnergyUsage(device.id); } else if (device.protocol === DeviceProtocol.KASA) { // Kasa energy monitoring return await this.sendKasaCommand(device, { emeter: { get_realtime: null } }); } return null; } // Private method to send Kasa (older TP-Link) commands private async sendKasaCommand(device: UnifiedDevice, command: any): Promise { try { const payload = encrypt(JSON.stringify(command)); const response = await axios.post(`http://${device.host}:9999`, payload, { headers: { 'Content-Type': 'application/octet-stream', }, responseType: 'arraybuffer', timeout: 5000, }); const decrypted = decrypt(Buffer.from(response.data)); const result = JSON.parse(decrypted.toString()); // Check for Kasa errors if (result.system?.set_relay_state?.err_code !== 0 && result.system?.set_relay_state?.err_code !== undefined) { throw new Error(`Kasa command failed: ${result.system.set_relay_state.err_code}`); } return result; } catch (error: any) { console.error(`[UnifiedDevice] Kasa command failed for ${device.name}:`, error?.message); throw error; } } // Setup devices for Tapo protocol async setupTapoDevices(devices: Array<{ id: string; host: string }>): Promise> { if (!this.tapoApi) { throw new Error('Tapo API not initialized'); } const result = await this.tapoApi.setup(devices); return result.devices; } // Helper to determine device protocol based on device info static detectProtocol(deviceInfo: any): DeviceProtocol { // Tapo devices typically have different identifiers if (deviceInfo.deviceType?.startsWith('SMART.TAPO') || deviceInfo.device_type?.startsWith('SMART.TAPO') || deviceInfo.model?.startsWith('Tapo')) { return DeviceProtocol.TAPO; } // Kasa devices use older model naming if (deviceInfo.model?.match(/^(HS|KP|KL|LB|EP)\d+/i)) { return DeviceProtocol.KASA; } // Default to Kasa for older devices return DeviceProtocol.KASA; } // Convert device info to unified format static createUnifiedDevice(deviceInfo: any, protocol?: DeviceProtocol): UnifiedDevice { const detectedProtocol = protocol || UnifiedDeviceFactory.detectProtocol(deviceInfo); return { id: deviceInfo.id || deviceInfo.deviceId || deviceInfo.deviceMac, name: deviceInfo.name || deviceInfo.alias || deviceInfo.deviceName, host: deviceInfo.host || deviceInfo.address?.replace('http://', '') || deviceInfo.ip, protocol: detectedProtocol, deviceType: deviceInfo.deviceType || deviceInfo.device_type || 'unknown', model: deviceInfo.model || deviceInfo.deviceModel || 'unknown', }; } }