UNPKG

homebridge-kasa-python

Version:

Plugin that uses Python-Kasa API to communicate with Kasa Devices.

176 lines 6.92 kB
import axios from 'axios'; import { EventSource } from 'eventsource'; import { EventEmitter } from 'node:events'; export const deviceEventEmitter = new EventEmitter(); export default class DeviceManager { platform; log; apiUrl; username; password; additionalBroadcasts; manualDeviceHosts; excludeMacs; includeMacs; constructor(platform) { this.platform = platform; this.refreshConfigSnapshot(); } refreshConfigSnapshot() { this.log = this.platform.log; this.username = this.platform.config.username; this.password = this.platform.config.password; this.apiUrl = `http://127.0.0.1:${this.platform.port}`; this.additionalBroadcasts = this.platform.config.discoveryOptions.additionalBroadcasts; this.manualDeviceHosts = this.platform.config.discoveryOptions.manualDevices.map(d => d.host); this.excludeMacs = this.platform.config.discoveryOptions.excludeMacAddresses; this.includeMacs = this.platform.config.discoveryOptions.includeMacAddresses; } async discoverDevices() { this.log.debug('Starting device discovery...'); try { const authConfig = this.username && this.password ? { auth: { username: this.username, password: this.password } } : {}; await axios.post(`${this.apiUrl}/discover`, { additionalBroadcasts: this.additionalBroadcasts, manualDevices: this.manualDeviceHosts, excludeMacAddresses: this.excludeMacs, includeMacAddresses: this.includeMacs, }, authConfig); const eventSource = new EventSource(`${this.apiUrl}/stream`); eventSource.onmessage = (event) => { try { const data = JSON.parse(event.data); this.log.debug('SSE event data:', data); if (data.status === 'discovery_complete') { this.log.debug('Device discovery complete.'); eventSource.close(); return; } if (!data.sys_info || !data.sys_info.host) { this.log.error('Invalid device payload (missing sys_info.host):', data); return; } const device = { sys_info: data.sys_info, feature_info: data.feature_info, last_seen: new Date(), offline: false, }; this.log.debug(`Discovered device: ${device.sys_info.alias} (${device.sys_info.host})`); this.updateDeviceAlias(device.sys_info); deviceEventEmitter.emit('deviceDiscovered', device); } catch (error) { this.log.error('Error parsing discovery SSE event:', error); } }; eventSource.onerror = (error) => { this.log.error('Discovery EventSource error:', error); eventSource.close(); }; await new Promise(resolve => setTimeout(resolve, 10000)); eventSource.close(); } catch (error) { this.handleAxiosError(error, 'discoverDevices'); } } async getSysInfo(host) { try { const response = await axios.post(`${this.apiUrl}/getSysInfo`, { host }); const sysInfo = response.data.sys_info; if (!sysInfo) { return undefined; } this.updateDeviceAlias(sysInfo); return sysInfo; } catch (error) { this.handleAxiosError(error, 'getSysInfo'); throw error; } } async controlDevice(host, feature, value, childNum) { const action = this.mapFeatureToAction(feature, value); await this.performDeviceAction(host, feature, action, value, childNum); } mapFeatureToAction(feature, value) { switch (feature) { case 'brightness': case 'color_temp': case 'fan_speed_level': return `set_${feature}`; case 'hsv': return 'set_hsv'; case 'state': return value ? 'turn_on' : 'turn_off'; default: throw new Error(`Unsupported feature: ${feature}`); } } async performDeviceAction(host, feature, action, value, childNumber) { const url = `${this.apiUrl}/controlDevice`; const payload = { host, feature, action, value, ...(childNumber !== undefined && { child_num: childNumber }), }; try { const response = await axios.post(url, payload); if (response.data.status !== 'success') { this.log.error(`Action failed (${feature}/${action}) on ${host}: ${response.data.message}`); } } catch (error) { this.handleAxiosError(error, 'controlDevice'); } } updateDeviceAlias(sysInfo) { if (!sysInfo.alias) { return; } const aliasPatterns = { 'TP-LINK_Power Strip_': 'Power Strip', 'TP-LINK_Smart Plug_': 'Smart Plug', 'TP-LINK_Smart Bulb_': 'Smart Bulb', }; for (const [pattern, replacement] of Object.entries(aliasPatterns)) { if (sysInfo.alias.includes(pattern)) { sysInfo.alias = `${replacement} ${sysInfo.alias.slice(-4)}`; break; } } } handleAxiosError(error, context) { if (axios.isAxiosError(error)) { if (error.response) { const statusCode = error.response.status; const message = error.response.data?.error || error.response.statusText || 'Unknown error'; this.log.error(`[${context}] HTTP ${statusCode}: ${message}`); } else if (error.code === 'ECONNREFUSED') { this.log.error(`[${context}] Connection refused (device API offline?)`); } else if (error.code === 'ETIMEDOUT') { this.log.error(`[${context}] Connection timed out (network issue)`); } else { this.log.error(`[${context}] Axios error: ${error.message}`); } } else if (error instanceof Error) { this.log.error(`[${context}] Error: ${error.message}`); if (error.stack) { this.log.debug(error.stack); } } else { this.log.error(`[${context}] Unknown error: ${JSON.stringify(error)}`); } } } //# sourceMappingURL=deviceManager.js.map