UNPKG

homebridge-kasa-python

Version:

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

344 lines 15.8 kB
import { EventEmitter } from 'node:events'; import HomeKitDevice from './index.js'; import { deferAndCombine } from '../utils.js'; export default class HomeKitDeviceLightBulb extends HomeKitDevice { kasaDevice; isUpdating = false; getSysInfo; hasBrightness; hasColorTemp; hasHSV; previousKasaDevice; pollingInterval; updateEmitter = new EventEmitter(); static locks = new Map(); constructor(platform, kasaDevice) { super(platform, kasaDevice, 5 /* Categories.LIGHTBULB */, 'LIGHTBULB'); this.kasaDevice = kasaDevice; this.log.debug(`Initializing HomeKitDeviceLightBulb for device: ${kasaDevice.sys_info.alias}`); this.hasBrightness = !!this.kasaDevice.feature_info.brightness; this.hasColorTemp = !!this.kasaDevice.feature_info.color_temp; this.hasHSV = !!this.kasaDevice.feature_info.hsv; this.checkService(); 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 = HomeKitDeviceLightBulb.locks.get(key); if (!lock) { lock = Promise.resolve(); } const currentLock = lock.then(async () => { try { return await action(); } finally { if (HomeKitDeviceLightBulb.locks.get(key) === currentLock) { HomeKitDeviceLightBulb.locks.delete(key); } } }); HomeKitDeviceLightBulb.locks.set(key, currentLock.then(() => { })); return currentLock; } checkService() { const serviceType = this.getServiceType(); const service = this.homebridgeAccessory.getService(serviceType) ?? this.addService(serviceType, this.name); this.checkCharacteristics(service); } getServiceType() { const { Lightbulb } = this.platform.Service; return Lightbulb; } checkCharacteristics(service) { const characteristics = this.getCharacteristics(); characteristics.forEach(({ type, name }) => { this.getOrAddCharacteristic(service, type, name); }); } getCharacteristics() { const characteristics = []; characteristics.push({ type: this.platform.Characteristic.On, name: this.platform.getCharacteristicName(this.platform.Characteristic.On), }); if (this.hasBrightness) { characteristics.push({ type: this.platform.Characteristic.Brightness, name: this.platform.getCharacteristicName(this.platform.Characteristic.Brightness), }); } if (this.hasColorTemp) { characteristics.push({ type: this.platform.Characteristic.ColorTemperature, name: this.platform.getCharacteristicName(this.platform.Characteristic.ColorTemperature), }); } if (this.hasHSV) { characteristics.push({ type: this.platform.Characteristic.Hue, name: this.platform.getCharacteristicName(this.platform.Characteristic.Hue), }, { type: this.platform.Characteristic.Saturation, name: this.platform.getCharacteristicName(this.platform.Characteristic.Saturation), }); } return characteristics; } getOrAddCharacteristic(service, characteristicType, characteristicName) { const characteristic = service.getCharacteristic(characteristicType) ?? service.addCharacteristic(characteristicType); characteristic.onGet(this.handleOnGet.bind(this, service, characteristicType, characteristicName)); characteristic.onSet(this.handleOnSet.bind(this, service, characteristicType, characteristicName)); } async handleOnGet(service, characteristicType, characteristicName) { 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); 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: ${this.name}:`, error); this.kasaDevice.offline = true; this.stopPolling(); return this.getDefaultValue(characteristicType); } } getDefaultValue(characteristicType) { const zeroValueCharacteristics = [ this.platform.Characteristic.Brightness, this.platform.Characteristic.ColorTemperature, this.platform.Characteristic.Hue, this.platform.Characteristic.Saturation, ]; if (zeroValueCharacteristics.includes(characteristicType)) { return 0; } return false; } getInitialValue(characteristicType) { if (characteristicType === this.platform.Characteristic.On) { return this.kasaDevice.sys_info.state ?? false; } else if (characteristicType === this.platform.Characteristic.Brightness) { return this.kasaDevice.sys_info.brightness ?? 0; } else if (characteristicType === this.platform.Characteristic.ColorTemperature) { return this.kasaDevice.sys_info.color_temp ?? 0; } else if (characteristicType === this.platform.Characteristic.Hue) { return this.kasaDevice.sys_info.hsv?.hue ?? 0; } else if (characteristicType === this.platform.Characteristic.Saturation) { return this.kasaDevice.sys_info.hsv?.saturation ?? 0; } return this.getDefaultValue(characteristicType); } async handleOnSet(service, characteristicType, characteristicName, value) { const lockKey = `${this.kasaDevice.sys_info.device_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}`); } await this.deviceManager.controlDevice(this.kasaDevice.sys_info.host, characteristicKey, value); this.kasaDevice.sys_info[characteristicKey] = value; this.updateValue(service, service.getCharacteristic(characteristicType), this.name, 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: ${this.name}:`, 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', Brightness: 'brightness', ColorTemperature: 'color_temp', Hue: 'hue', Saturation: 'saturation', }; 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(); const service = this.getService(); if (service && this.previousKasaDevice) { this.updateDeviceState(service); } else { this.log.warn(`Service not found for device: ${this.name} 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() { return this.homebridgeAccessory.getService(this.platform.Service.Lightbulb); } updateDeviceState(service) { const { state, brightness, color_temp, hsv } = this.kasaDevice.sys_info; const prevState = this.previousKasaDevice.sys_info; if (prevState.state !== state) { this.updateValue(service, service.getCharacteristic(this.platform.Characteristic.On), this.name, state ?? false); this.log.debug(`Updated state for device: ${this.name} to state: ${state}`); } if (this.hasBrightness && prevState.brightness !== brightness) { this.updateValue(service, service.getCharacteristic(this.platform.Characteristic.Brightness), this.name, brightness ?? 0); this.log.debug(`Updated brightness for device: ${this.name} to brightness: ${brightness}`); } if (this.hasColorTemp && prevState.color_temp !== color_temp) { this.updateValue(service, service.getCharacteristic(this.platform.Characteristic.ColorTemperature), this.name, color_temp ?? 0); this.log.debug(`Updated color_temp for device: ${this.name} to color_temp: ${color_temp}`); } if (this.hasHSV) { if (prevState.hsv?.hue !== hsv?.hue) { this.updateValue(service, service.getCharacteristic(this.platform.Characteristic.Hue), this.name, hsv?.hue ?? 0); this.log.debug(`Updated hue for device: ${this.name} to hue: ${hsv?.hue}`); } if (prevState.hsv?.saturation !== hsv?.saturation) { this.updateValue(service, service.getCharacteristic(this.platform.Characteristic.Saturation), this.name, hsv?.saturation ?? 0); this.log.debug(`Updated saturation for device: ${this.name} to saturation: ${hsv?.saturation}`); } } } updateAfterPeriodicDiscovery() { const serviceType = this.getServiceType(); const service = this.homebridgeAccessory.getService(serviceType); if (service) { this.updateCharacteristics(service); } else { this.log.debug(`Service not found for device: ${this.name}`); } } updateCharacteristics(service) { const characteristics = this.getCharacteristics(); characteristics.forEach(({ type, name }) => { const characteristic = service.getCharacteristic(type); if (characteristic) { const characteristicKey = this.getCharacteristicKey(name); if (this.kasaDevice.sys_info[characteristicKey] !== undefined) { const value = this.kasaDevice.sys_info[characteristicKey]; this.log.debug(`Setting value for characteristic ${name} to ${value}`); this.updateValue(service, characteristic, this.name, 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=homekitLightBulb.js.map