UNPKG

homebridge-kasa-python

Version:

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

359 lines 17.3 kB
import { EventEmitter } from 'node:events'; import HomeKitDevice from './index.js'; import { deferAndCombine } from '../utils.js'; export default class HomeKitDeviceSwitchWithChildren extends HomeKitDevice { kasaDevice; isUpdating = false; previousKasaDevice; getSysInfo; pollingInterval; updateEmitter = new EventEmitter(); static locks = new Map(); constructor(platform, kasaDevice) { super(platform, kasaDevice, 8 /* Categories.SWITCH */, 'SWITCH'); this.kasaDevice = kasaDevice; this.log.debug(`Initializing HomeKitDeviceSwitchWithChildren 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 = HomeKitDeviceSwitchWithChildren.locks.get(key); if (!lock) { lock = Promise.resolve(); } const currentLock = lock.then(async () => { try { return await action(); } finally { if (HomeKitDeviceSwitchWithChildren.locks.get(key) === currentLock) { HomeKitDeviceSwitchWithChildren.locks.delete(key); } } }); HomeKitDeviceSwitchWithChildren.locks.set(key, currentLock.then(() => { })); return currentLock; } checkService(child, index) { const serviceType = this.getServiceType(child); const service = this.homebridgeAccessory.getServiceById(serviceType, `child-${index + 1}`) ?? this.addService(serviceType, child.alias, `child-${index + 1}`); this.checkCharacteristics(service, child); } getServiceType(child) { const { Lightbulb, Fanv2 } = this.platform.Service; return child.fan_speed_level !== undefined ? Fanv2 : Lightbulb; } checkCharacteristics(service, child) { const characteristics = this.getCharacteristics(child); characteristics.forEach(({ type, name }) => { this.getOrAddCharacteristic(service, type, name, child); }); } getCharacteristics(child) { const characteristics = []; if (child.fan_speed_level !== undefined) { characteristics.push({ type: this.platform.Characteristic.RotationSpeed, name: this.platform.getCharacteristicName(this.platform.Characteristic.RotationSpeed), }, { type: this.platform.Characteristic.Active, name: this.platform.getCharacteristicName(this.platform.Characteristic.Active), }); } if (child.brightness !== undefined) { characteristics.push({ type: this.platform.Characteristic.On, name: this.platform.getCharacteristicName(this.platform.Characteristic.On), }, { type: this.platform.Characteristic.Brightness, name: this.platform.getCharacteristicName(this.platform.Characteristic.Brightness), }); } 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)); 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 this.getDefaultValue(characteristicType); } 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 ?? this.getDefaultValue(characteristicType); } catch (error) { this.log.error(`Error getting current value for characteristic ${characteristicName} for device: ${child.alias}:`, error); this.kasaDevice.offline = true; this.stopPolling(); return this.getDefaultValue(characteristicType); } } getDefaultValue(characteristicType) { const zeroValueCharacteristics = [ this.platform.Characteristic.Brightness, this.platform.Characteristic.RotationSpeed, ]; if (zeroValueCharacteristics.includes(characteristicType)) { return 0; } else if (characteristicType === this.platform.Characteristic.Active) { return this.platform.Characteristic.Active.INACTIVE; } return false; } getInitialValue(characteristicType, child) { if (characteristicType === this.platform.Characteristic.Active) { return child.state ? this.platform.Characteristic.Active.ACTIVE : this.platform.Characteristic.Active.INACTIVE; } else if (characteristicType === this.platform.Characteristic.Brightness) { return child.brightness ?? 0; } else if (characteristicType === this.platform.Characteristic.RotationSpeed) { return this.mapRotationSpeedToValue(child.fan_speed_level) ?? 0; } else if (characteristicType === this.platform.Characteristic.On) { return child.state ?? false; } return false; } mapRotationSpeedToValue(value) { return value * 25; } 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); const controlValue = this.getControlValue(characteristicName, value); await this.deviceManager.controlDevice(this.kasaDevice.sys_info.host, characteristicKey, controlValue, childNumber); child[characteristicKey] = controlValue; 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.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 = { Active: 'state', Brightness: 'brightness', RotationSpeed: 'fan_speed_level', On: 'state', }; return characteristicMap[characteristicName ?? '']; } getControlValue(characteristicName, value) { if (characteristicName === 'Active') { return value === 1 ? true : false; } else if (characteristicName === 'RotationSpeed') { return this.mapRotationSpeedToValue(value); } return value; } 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(child, 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(child, childNumber) { if (child.brightness !== undefined) { return this.homebridgeAccessory.getServiceById(this.platform.Service.Lightbulb, `child-${childNumber + 1}`); } else if (child.fan_speed_level !== undefined) { return this.homebridgeAccessory.getServiceById(this.platform.Service.Fanv2, `child-${childNumber + 1}`); } return undefined; } updateChildState(service, child) { const previousChild = this.previousKasaDevice?.sys_info.children?.find(c => c.id === child.id); if (previousChild) { if (previousChild.state !== child.state) { if (service.UUID === this.platform.Service.Lightbulb.UUID) { this.updateValue(service, service.getCharacteristic(this.platform.Characteristic.On), child.alias, child.state); this.log.debug(`Updated state for child device: ${child.alias} to ${child.state}`); } else if (service.UUID === this.platform.Service.Fanv2.UUID) { this.updateValue(service, service.getCharacteristic(this.platform.Characteristic.Active), child.alias, child.state ? this.platform.Characteristic.Active.ACTIVE : this.platform.Characteristic.Active.INACTIVE); this.log.debug(`Updated active state for child device: ${child.alias} to ${child.state ? 'ACTIVE' : 'INACTIVE'}`); } } if (child.brightness !== undefined && previousChild.brightness !== child.brightness) { this.updateValue(service, service.getCharacteristic(this.platform.Characteristic.Brightness), child.alias, child.brightness); this.log.debug(`Updated brightness for child device: ${child.alias} to ${child.brightness}`); } if (child.fan_speed_level !== undefined && previousChild.fan_speed_level !== child.fan_speed_level) { const updateValue = this.mapRotationSpeedToValue(child.fan_speed_level); this.updateValue(service, service.getCharacteristic(this.platform.Characteristic.RotationSpeed), child.alias, updateValue); this.log.debug(`Updated fan speed for child device: ${child.alias} to ${updateValue}`); } } } updateAfterPeriodicDiscovery() { this.kasaDevice.sys_info.children?.forEach((child, index) => { const serviceType = this.getServiceType(child); 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(child); characteristics.forEach(({ type, name }) => { const characteristic = service.getCharacteristic(type); if (characteristic) { const characteristicKey = this.getCharacteristicKey(name); if (child[characteristicKey] !== undefined) { const value = child[characteristicKey]; const controlValue = this.getControlValue(name, value); this.log.debug(`Setting value for characteristic ${name} to ${controlValue}`); this.updateValue(service, characteristic, child.alias, controlValue); } } }); } 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=homekitSwitchWithChildren.js.map