homebridge-kasa-python
Version:
Plugin that uses Python-Kasa API to communicate with Kasa Devices.
294 lines • 13.2 kB
JavaScript
import { EventEmitter } from 'node:events';
import HomeKitDevice from './index.js';
import { deferAndCombine } from '../utils.js';
export default class HomeKitDeviceSwitch extends HomeKitDevice {
kasaDevice;
isUpdating = false;
previousKasaDevice;
getSysInfo;
hasBrightness;
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 HomeKitDeviceSwitch for device: ${kasaDevice.sys_info.alias}`);
this.hasBrightness = !!this.kasaDevice.feature_info.brightness;
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 = HomeKitDeviceSwitch.locks.get(key);
if (!lock) {
lock = Promise.resolve();
}
const currentLock = lock.then(async () => {
try {
return await action();
}
finally {
if (HomeKitDeviceSwitch.locks.get(key) === currentLock) {
HomeKitDeviceSwitch.locks.delete(key);
}
}
});
HomeKitDeviceSwitch.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 { Switch, Lightbulb } = this.platform.Service;
return this.hasBrightness ? Lightbulb : Switch;
}
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),
});
}
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) {
if (characteristicType === this.platform.Characteristic.Brightness) {
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;
}
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',
};
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.Switch) ??
this.homebridgeAccessory.getService(this.platform.Service.Lightbulb);
}
updateDeviceState(service) {
const { state, brightness } = 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}`);
}
}
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=homekitSwitch.js.map