UNPKG

homebridge-kasa-python

Version:

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

299 lines 14.1 kB
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