UNPKG

homebridge-kasa-python

Version:

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

225 lines 8.7 kB
import axios from 'axios'; import path from 'node:path'; import { promises as fs } from 'node:fs'; import { parseConfig } from '../config.js'; export default class DeviceManager { platform; log; apiUrl; username; password; additionalBroadcasts; manualDevices; excludeMacAddresses; constructor(platform) { this.platform = platform; this.log = platform.log; this.username = platform.config.username; this.password = platform.config.password; this.apiUrl = `http://127.0.0.1:${platform.port}`; this.additionalBroadcasts = platform.config.discoveryOptions.additionalBroadcasts; this.manualDevices = platform.config.discoveryOptions.manualDevices.map(device => device.host); this.excludeMacAddresses = platform.config.discoveryOptions.excludeMacAddresses; } convertManualDevices(manualDevices) { return manualDevices.map(device => { if (typeof device === 'string') { return { host: device, alias: 'Will Be Filled By Plug-In Automatically' }; } else if ('breakoutChildDevices' in device) { delete device.breakoutChildDevices; } else if ('host' in device && !('alias' in device)) { device.alias = 'Will Be Filled By Plug-In Automatically'; } return device; }); } updateDeviceAlias(device) { let sysInfo; if (this.isKasaDevice(device)) { sysInfo = device.sys_info; } else { sysInfo = device; } if (sysInfo.alias) { const aliasMappings = { '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(aliasMappings)) { if (sysInfo.alias.includes(pattern)) { sysInfo.alias = `${replacement} ${sysInfo.alias.slice(-4)}`; break; } } } } isKasaDevice(device) { return device.sys_info !== undefined; } async readConfigFile(configPath) { try { const configData = await fs.readFile(configPath, 'utf8'); return JSON.parse(configData); } catch (error) { this.log.error(`Error reading config file: ${error}`); throw error; } } async writeConfigFile(configPath, fileConfig) { try { await fs.writeFile(configPath, JSON.stringify(fileConfig, null, 2), 'utf8'); } catch (error) { this.log.error(`Error writing config file: ${error}`); } } async discoverDevices() { this.log.info('Discovering devices...'); try { const config = this.username && this.password ? { auth: { username: this.username, password: this.password } } : {}; const response = await axios.post(`${this.apiUrl}/discover`, { additionalBroadcasts: this.additionalBroadcasts, manualDevices: this.manualDevices, excludeMacAddresses: this.excludeMacAddresses, }, config); const devices = response.data; if (!devices || Object.keys(devices).length === 0) { this.log.error('No devices found.'); return {}; } const configPath = path.join(this.platform.storagePath, 'config.json'); const fileConfig = await this.readConfigFile(configPath); const platformConfig = fileConfig.platforms.find((platformConfig) => platformConfig.platform === 'KasaPython'); if (!platformConfig) { this.log.error('KasaPython configuration not found in config file.'); return {}; } platformConfig.manualDevices = platformConfig.manualDevices || []; platformConfig.manualDevices = platformConfig.manualDevices.filter((device) => { if (typeof device === 'string') { return true; } else if (!device.host) { this.log.warn(`Removing manual device without host: ${JSON.stringify(device)}`); return false; } return true; }); if (this.shouldConvertManualDevices(platformConfig.manualDevices)) { platformConfig.manualDevices = this.convertManualDevices(platformConfig.manualDevices); } const processedDevices = {}; Object.keys(devices).forEach(ip => { const deviceInfo = devices[ip].sys_info; const featureInfo = devices[ip].feature_info; const device = { sys_info: deviceInfo, feature_info: featureInfo, last_seen: new Date(), offline: false, }; this.processDevice(device, platformConfig); processedDevices[ip] = device; }); if (!platformConfig.manualDevices.length) { delete platformConfig.manualDevices; } await this.writeConfigFile(configPath, fileConfig); this.platform.config = parseConfig(platformConfig); return processedDevices; } catch (error) { this.handleAxiosError(error, 'discoverDevices'); return {}; } } processDevice(device, platformConfig) { try { this.updateDeviceAlias(device); const existingDevice = platformConfig.manualDevices.find((d) => d.host === device.sys_info.host); if (existingDevice) { existingDevice.host = device.sys_info.host; existingDevice.alias = device.sys_info.alias; } } catch (error) { this.log.error(`Error processing device: ${error.message}`); } } shouldConvertManualDevices(manualDevices) { return manualDevices.length > 0 && (typeof manualDevices[0] === 'string' || manualDevices.some((device) => typeof device !== 'string')); } async getSysInfo(host) { try { const response = await axios.post(`${this.apiUrl}/getSysInfo`, { host }); const sysInfo = response.data.sys_info; this.updateDeviceAlias(sysInfo); return sysInfo; } catch (error) { this.handleAxiosError(error, 'getSysInfo'); } } async controlDevice(host, feature, value, child_num) { let action; switch (feature) { case 'brightness': case 'color_temp': case 'fan_speed_level': action = `set_${feature}`; break; case 'hue': case 'saturation': action = 'set_hsv'; break; case 'state': action = value ? 'turn_on' : 'turn_off'; break; default: throw new Error(`Unsupported feature: ${feature}`); } await this.performDeviceAction(host, feature, action, value, child_num); } async performDeviceAction(host, feature, action, value, childNumber) { const url = `${this.apiUrl}/controlDevice`; const data = { host, feature, action, value, ...(childNumber !== undefined && { child_num: childNumber }), }; try { const response = await axios.post(url, data); if (response.data.status !== 'success') { this.log.error(`Error performing action: ${response.data.message}`); } } catch (error) { this.handleAxiosError(error, 'controlDevice'); } } handleAxiosError(error, context) { if (axios.isAxiosError(error) && error.response) { const statusCode = error.response.status; const errorMessage = error.response.data.error; if (statusCode === 500) { this.log.error(`Exception during ${context} post request: ${errorMessage}`); } else { this.log.error(`Unexpected error during ${context} post request: ${errorMessage}`); } } else { this.log.error(`Error during ${context} post request:`, error); } } } //# sourceMappingURL=deviceManager.js.map