homebridge-vesync-v2
Version:
A Homebridge plugin for controlling VeSync smart devices including outlets, air purifiers, and humidifiers
283 lines (242 loc) • 10.6 kB
JavaScript
"use strict";
let EtekcityClient = require('./lib/client');
let Accessory, Service, Characteristic, UUIDGen;
module.exports = function (homebridge) {
Accessory = homebridge.platformAccessory;
Service = homebridge.hap.Service;
Characteristic = homebridge.hap.Characteristic;
UUIDGen = homebridge.hap.uuid;
homebridge.registerPlatform("homebridge-vesync-v2", "VesyncPlug", VeseyncPlugPlatform);
};
class VeseyncPlugPlatform {
constructor(log, config, api) {
this.log = log;
this.config = config;
this.api = api;
this.accessories = {}; // Keep as object for current version
this.cache_timeout = 10; // seconds
this.debug = config['debug'] || false;
this.username = config['username'];
this.password = config['password'];
this.exclude = config['exclude']?.split(',') || [];
if (api) {
this.api = api;
this.api.on('didFinishLaunching', () => {
this.deviceDiscovery();
setInterval(() => this.deviceDiscovery(), this.cache_timeout * 6000);
});
}
this.client = new EtekcityClient(log, this.exclude);
}
configureAccessory(accessory) {
const accessoryId = accessory.context.id;
if (this.debug) this.log("Configuring accessory: " + accessoryId);
// Handle rename case
if (this.accessories[accessoryId]) {
this.log("Duplicate accessory detected, removing existing one");
try {
this.removeAccessory(this.accessories[accessoryId]);
this.setService(accessory);
} catch (error) {
this.removeAccessory(accessory);
accessory = this.accessories[accessoryId];
}
} else {
this.setService(accessory);
}
this.accessories[accessoryId] = accessory;
}
removeAccessory(accessory, accessoryId = undefined) {
if (!accessory) return;
const id = accessoryId ?? accessory.context?.id;
if (this.debug) this.log("Removing accessory: " + id);
try {
this.api.unregisterPlatformAccessories("homebridge-vesync-v2", "VesyncPlug", [accessory]);
} catch (error) {
this.log("Error removing accessory: " + error);
}
if (id) {
delete this.accessories[id];
}
}
addAccessory(data) {
if (!this.accessories[data.id]) {
const uuid = UUIDGen.generate(data.id);
const newAccessory = new Accessory(data.name, uuid);
newAccessory.context = {
name: data.name,
id: data.id,
type: data.type // Add type to context for service identification
};
// Add appropriate service based on device type
if (this.isDeviceType(data.type, 'fan')) {
newAccessory.addService(Service.Fan, data.name);
} else if (this.isDeviceType(data.type, 'airPurifier')) {
newAccessory.addService(Service.AirPurifier, data.name);
} else if (this.isDeviceType(data.type, 'humidifier')) {
newAccessory.addService(Service.HumidifierDehumidifier, data.name);
} else if (this.isDeviceType(data.type, 'lightbulb')) {
newAccessory.addService(Service.Lightbulb, data.name);
} else {
// Default to outlet for all other devices (smart plugs, power strips)
newAccessory.addService(Service.Outlet, data.name);
}
this.setService(newAccessory);
this.api.registerPlatformAccessories("homebridge-vesync-v2", "VesyncPlug", [newAccessory]);
}
const accessory = this.accessories[data.id];
this.getInitState(accessory, data);
this.accessories[data.id] = accessory;
}
isDeviceType(deviceType, category) {
const deviceCategories = {
fan: ['LTF-F422S-WUSR', 'LTF-F411S-WUS'],
airPurifier: [
'LV-PUR131S', 'Core200S', 'Core300S', 'Core400S',
'Core600S', 'Core100S', 'LAP-C201S-AUSR', 'LAP-C202S-WUSR',
'Vital100S', 'Vital200S'
],
humidifier: [
'Classic300S', 'Classic200S', 'Dual200S', 'OasisMist500S',
'LUH-D301S-WUS', 'LV600S', 'Dual100S', 'LUH-A601S-WUSR'
],
lightbulb: ['ESL100', 'ESL100CW', 'ESL100MC']
};
return deviceCategories[category]?.some(type =>
type === deviceType || deviceType?.startsWith(type)
) || false;
}
deviceDiscovery() {
let me = this;
if (me.debug) me.log("DeviceDiscovery invoked");
this.client.login(this.username, this.password).then(() => {
return this.client.getDevices();
}).then(devices => {
if (me.debug) me.log("Adding discovered devices");
for (let i in devices) {
let existing = me.accessories[devices[i].id];
if (!existing) {
me.log("Adding device: ", devices[i].id, devices[i].name);
me.addAccessory(devices[i]);
} else {
if (me.debug) me.log("Skipping existing device", i);
}
}
if (devices) {
for (let index in me.accessories) {
var acc = me.accessories[index];
var found = devices.find((device) => {
return device.id == index;
});
if (!found) {
me.log("Previously configured accessory not found, removing", index);
me.removeAccessory(me.accessories[index]);
} else if (found.name != acc.context.name) {
me.log("Accessory name does not match device name, got " + found.name + " expected " + acc.context.name);
me.removeAccessory(me.accessories[index]);
me.addAccessory(found);
me.log("Accessory removed & readded!");
}
}
}
if (me.debug) me.log("Discovery complete");
}).catch((err) => {
me.log("ERROR: " + err);
});
}
setService(accessory) {
let service;
// Get the appropriate service based on device type
if (this.isDeviceType(accessory.context.type, 'fan')) {
service = accessory.getService(Service.Fan);
} else if (this.isDeviceType(accessory.context.type, 'airPurifier')) {
service = accessory.getService(Service.AirPurifier);
} else if (this.isDeviceType(accessory.context.type, 'humidifier')) {
service = accessory.getService(Service.HumidifierDehumidifier);
} else if (this.isDeviceType(accessory.context.type, 'lightbulb')) {
service = accessory.getService(Service.Lightbulb);
} else {
service = accessory.getService(Service.Outlet);
}
if (!service) {
this.log("Could not find service for accessory", accessory.displayName);
return;
}
// Set up power control characteristic for all devices
service
.getCharacteristic(Characteristic.On)
.on('get', (callback) => {
this.getPowerState(accessory.context, callback);
})
.on('set', (value, callback) => {
this.setPowerState(accessory.context, value, callback);
});
// Additional characteristics could be added here for specific device types
}
getInitState(accessory, data) {
let info = accessory.getService(Service.AccessoryInformation);
accessory.context.manufacturer = "Etekcity";
info.setCharacteristic(Characteristic.Manufacturer, accessory.context.manufacturer);
accessory.context.model = "ESW01-USA";
info.setCharacteristic(Characteristic.Model, accessory.context.model);
info.setCharacteristic(Characteristic.SerialNumber, accessory.context.id);
accessory.getService(Service.Outlet)
.getCharacteristic(Characteristic.On)
.getValue();
}
setPowerState(thisPlug, powerState, callback) {
let that = this;
if (this.debug) this.log("Sending device status change");
return this.client.login(this.username, this.password).then(() => {
return this.client.getDevices();
}).then(devices => {
return devices.find((device) => {
return device.name.includes(thisPlug.name);
});
}).then((device) => {
thisPlug.status = device.status;
if (device.status == 'on' && powerState == false) {
return this.client.turnDevice(device, "off");
}
if (device.status == 'off' && powerState == true) {
return this.client.turnDevice(device, "on");
}
}).then(() => {
callback();
}).catch((err) => {
if (err == 'Error: No Content') {
callback();
return;
}
this.log(err);
this.log("Failed to set power state to", powerState);
callback(err);
});
}
getPowerState(thisPlug, callback) {
if (this.accessories[thisPlug.id]) {
return this.client.login(this.username, this.password).then(() => {
return this.client.getDevices();
}).then(devices => {
return devices.find((device) => {
return device.name.includes(thisPlug.name);
});
}).then((device) => {
if (typeof device === 'undefined') {
if (this.debug) this.log("Removing undefined device", thisPlug.name);
this.removeAccessory(thisPlug)
} else {
thisPlug.status = device.status;
if (this.debug) this.log("getPowerState complete");
callback(null, device.status == 'on');
}
});
} else {
callback(new Error("Device not found"));
}
}
identify(thisPlug, paired, callback) {
this.log("Identify requested for " + thisPlug.name);
callback();
}
}