homebridge-kasa-python
Version:
Plugin that uses Python-Kasa API to communicate with Kasa Devices.
359 lines • 17.3 kB
JavaScript
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