homebridge-kasa-python
Version:
Plugin that uses Python-Kasa API to communicate with Kasa Devices.
225 lines • 8.7 kB
JavaScript
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