homebridge-kasa-python
Version:
Plugin that uses Python-Kasa API to communicate with Kasa Devices.
299 lines • 14.1 kB
JavaScript
import { EventEmitter } from 'node:events';
import HomeKitDevice from './index.js';
import { deferAndCombine } from '../utils.js';
export default class HomeKitDevicePowerStrip extends HomeKitDevice {
kasaDevice;
isUpdating = false;
previousKasaDevice;
getSysInfo;
pollingInterval;
updateEmitter = new EventEmitter();
static locks = new Map();
constructor(platform, kasaDevice) {
super(platform, kasaDevice, 7 /* Categories.OUTLET */, 'OUTLET');
this.kasaDevice = kasaDevice;
this.log.debug(`Initializing HomeKitDevicePowerStrip for device: ${kasaDevice.sys_info.alias}`);
this.kasaDevice.sys_info.children?.forEach((child, index) => {
this.checkService(child, index);
});
this.getSysInfo = deferAndCombine(async () => {
if (this.deviceManager) {
this.previousKasaDevice = JSON.parse(JSON.stringify(this.kasaDevice));
this.kasaDevice.sys_info = await this.deviceManager.getSysInfo(this.kasaDevice.sys_info.host);
this.log.debug(`Updated sys_info for device: ${this.kasaDevice.sys_info.alias}`);
}
else {
this.log.warn('Device manager is not available');
}
}, platform.config.advancedOptions.waitTimeUpdate);
this.startPolling();
platform.periodicDeviceDiscoveryEmitter.on('periodicDeviceDiscoveryComplete', () => {
this.updateEmitter.emit('periodicDeviceDiscoveryComplete');
});
}
async withLock(key, action) {
let lock = HomeKitDevicePowerStrip.locks.get(key);
if (!lock) {
lock = Promise.resolve();
}
const currentLock = lock.then(async () => {
try {
return await action();
}
finally {
if (HomeKitDevicePowerStrip.locks.get(key) === currentLock) {
HomeKitDevicePowerStrip.locks.delete(key);
}
}
});
HomeKitDevicePowerStrip.locks.set(key, currentLock.then(() => { }));
return currentLock;
}
checkService(child, index) {
const serviceType = this.getServiceType();
const service = this.homebridgeAccessory.getServiceById(serviceType, `child-${index + 1}`) ??
this.addService(serviceType, child.alias, `child-${index + 1}`);
const oldService = this.homebridgeAccessory.getServiceById(serviceType, `outlet-${index + 1}`);
if (oldService) {
this.homebridgeAccessory.removeService(oldService);
}
this.checkCharacteristics(service, child);
}
getServiceType() {
const { Outlet } = this.platform.Service;
return Outlet;
}
checkCharacteristics(service, child) {
const characteristics = this.getCharacteristics();
characteristics.forEach(({ type, name }) => {
this.getOrAddCharacteristic(service, type, name, child);
});
}
getCharacteristics() {
const characteristics = [];
characteristics.push({
type: this.platform.Characteristic.On,
name: this.platform.getCharacteristicName(this.platform.Characteristic.On),
}, {
type: this.platform.Characteristic.OutletInUse,
name: this.platform.getCharacteristicName(this.platform.Characteristic.OutletInUse),
});
return characteristics;
}
getOrAddCharacteristic(service, characteristicType, characteristicName, child) {
const characteristic = service.getCharacteristic(characteristicType) ??
service.addCharacteristic(characteristicType);
characteristic.onGet(this.handleOnGet.bind(this, service, characteristicType, characteristicName, child));
if (characteristicType === this.platform.Characteristic.On) {
characteristic.onSet(this.handleOnSet.bind(this, service, characteristicType, characteristicName, child));
}
}
async handleOnGet(service, characteristicType, characteristicName, child) {
if (this.kasaDevice.offline || this.platform.isShuttingDown) {
this.log.warn(`Device is offline or platform is shutting down, cannot get value for characteristic ${characteristicName}`);
return false;
}
try {
let characteristicValue = service.getCharacteristic(characteristicType).value;
if (!characteristicValue) {
characteristicValue = this.getInitialValue(characteristicType, child);
service.getCharacteristic(characteristicType).updateValue(characteristicValue);
}
this.log.debug(`Got value for characteristic ${characteristicName}: ${characteristicValue}`);
return characteristicValue ?? false;
}
catch (error) {
this.log.error(`Error getting current value for characteristic ${characteristicName} for device: ${child.alias}:`, error);
this.kasaDevice.offline = true;
this.stopPolling();
return false;
}
}
getInitialValue(characteristicType, child) {
if (characteristicType === this.platform.Characteristic.On || characteristicType === this.platform.Characteristic.OutletInUse) {
return child.state ?? false;
}
return false;
}
async handleOnSet(service, characteristicType, characteristicName, child, value) {
const lockKey = `${this.kasaDevice.sys_info.device_id}:${child.id}`;
await this.withLock(lockKey, async () => {
if (this.kasaDevice.offline || this.platform.isShuttingDown) {
this.log.warn(`Device is offline or platform is shutting down, cannot set value for characteristic ${characteristicName}`);
return;
}
const task = async () => {
if (this.deviceManager) {
try {
this.isUpdating = true;
this.log.debug(`Setting value for characteristic ${characteristicName} to ${value}`);
const characteristicKey = this.getCharacteristicKey(characteristicName);
if (!characteristicKey) {
throw new Error(`Characteristic key not found for ${characteristicName}`);
}
const childNumber = parseInt(child.id.slice(-1), 10);
await this.deviceManager.controlDevice(this.kasaDevice.sys_info.host, characteristicKey, value, childNumber);
child[characteristicKey] = value;
const childIndex = this.kasaDevice.sys_info.children?.findIndex(c => c.id === child.id);
if (childIndex !== undefined && childIndex !== -1) {
this.kasaDevice.sys_info.children[childIndex] = { ...child };
}
this.updateValue(service, service.getCharacteristic(characteristicType), child.alias, value);
this.updateValue(service, service.getCharacteristic(this.platform.Characteristic.OutletInUse), child.alias, value);
this.previousKasaDevice = JSON.parse(JSON.stringify(this.kasaDevice));
this.log.debug(`Set value for characteristic ${characteristicName} to ${value} successfully`);
}
catch (error) {
this.log.error(`Error setting current value for characteristic ${characteristicName} for device: ${child.alias}:`, error);
this.kasaDevice.offline = true;
this.stopPolling();
}
finally {
this.isUpdating = false;
this.updateEmitter.emit('updateComplete');
}
}
else {
throw new Error('Device manager is undefined.');
}
};
await task();
});
}
getCharacteristicKey(characteristicName) {
const characteristicMap = {
On: 'state',
};
return characteristicMap[characteristicName ?? ''];
}
async updateState() {
const lockKey = `${this.kasaDevice.sys_info.device_id}`;
await this.withLock(lockKey, async () => {
if (this.kasaDevice.offline || this.platform.isShuttingDown) {
this.stopPolling();
return;
}
if (this.isUpdating || this.platform.periodicDeviceDiscovering) {
let periodicDiscoveryComplete = false;
await Promise.race([
new Promise((resolve) => this.updateEmitter.once('updateComplete', resolve)),
new Promise((resolve) => {
this.updateEmitter.once('periodicDeviceDiscoveryComplete', () => {
periodicDiscoveryComplete = true;
resolve();
});
}),
]);
if (periodicDiscoveryComplete) {
if (this.pollingInterval) {
await new Promise((resolve) => setTimeout(resolve, this.platform.config.discoveryOptions.pollingInterval));
}
else {
return;
}
}
}
this.isUpdating = true;
const task = async () => {
try {
await this.getSysInfo();
this.kasaDevice.sys_info.children?.forEach((child) => {
const childNumber = parseInt(child.id.slice(-1), 10);
const service = this.getService(childNumber);
if (service && this.previousKasaDevice) {
this.updateChildState(service, child);
}
else {
this.log.warn(`Service not found for child device: ${child.alias} or previous Kasa device is undefined`);
}
});
}
catch (error) {
this.log.error('Error updating device state:', error);
this.kasaDevice.offline = true;
this.stopPolling();
}
finally {
this.isUpdating = false;
this.updateEmitter.emit('updateComplete');
}
};
await task();
});
}
getService(childNumber) {
return this.homebridgeAccessory.getServiceById(this.platform.Service.Outlet, `child-${childNumber + 1}`);
}
updateChildState(service, child) {
const previousChild = this.previousKasaDevice?.sys_info.children?.find(c => c.id === child.id);
if (previousChild) {
if (previousChild.state !== child.state) {
this.updateValue(service, service.getCharacteristic(this.platform.Characteristic.On), child.alias, child.state);
this.updateValue(service, service.getCharacteristic(this.platform.Characteristic.OutletInUse), child.alias, child.state);
this.log.debug(`Updated state for child device: ${child.alias} to ${child.state}`);
}
}
}
updateAfterPeriodicDiscovery() {
this.kasaDevice.sys_info.children?.forEach((child, index) => {
const serviceType = this.getServiceType();
const service = this.homebridgeAccessory.getServiceById(serviceType, `child-${index + 1}`);
if (service) {
this.updateCharacteristics(service, child);
}
else {
this.log.debug(`Service not found for child device: ${child.alias}`);
}
});
}
updateCharacteristics(service, child) {
const characteristics = this.getCharacteristics();
characteristics.forEach(({ type, name }) => {
if (type === this.platform.Characteristic.On) {
const characteristic = service.getCharacteristic(type);
if (characteristic) {
const characteristicKey = this.getCharacteristicKey(name);
if (child[characteristicKey] !== undefined) {
const value = child[characteristicKey];
this.log.debug(`Setting value for characteristic ${name} to ${value}`);
this.updateValue(service, characteristic, child.alias, value);
this.updateValue(service, service.getCharacteristic(this.platform.Characteristic.OutletInUse), child.alias, value);
}
}
}
});
}
startPolling() {
if (this.kasaDevice.offline || this.platform.isShuttingDown) {
this.stopPolling();
return;
}
if (this.pollingInterval) {
clearInterval(this.pollingInterval);
}
this.log.debug('Starting polling for device:', this.name);
this.pollingInterval = setInterval(async () => {
if (this.kasaDevice.offline || this.platform.isShuttingDown) {
if (this.isUpdating) {
this.isUpdating = false;
this.updateEmitter.emit('updateComplete');
}
this.stopPolling();
}
else {
await this.updateState();
}
}, this.platform.config.discoveryOptions.pollingInterval);
}
stopPolling() {
if (this.pollingInterval) {
clearInterval(this.pollingInterval);
this.pollingInterval = undefined;
this.log.debug('Stopped polling');
}
}
identify() {
this.log.info('identify');
}
}
//# sourceMappingURL=homekitPowerStrip.js.map