homebridge-vesync-v2
Version:
A Homebridge plugin for controlling VeSync smart devices including outlets, air purifiers, and humidifiers
216 lines (177 loc) • 8.19 kB
JavaScript
"use strict";
const EtekcityClient = require('./EtekCityClient');
const DeviceFactory = require('./DeviceFactory');
class VeseyncPlugPlatform {
constructor(log, config, api) {
this.log = log;
this.config = config;
this.api = api;
this.accessories = {};
this.cache_timeout = 10; // seconds
this.debug = config['debug'] || false;
this.username = config['username'];
this.password = config['password'];
this.exclude = config['exclude']?.split(',') || [];
this.client = new EtekcityClient(log, this.exclude);
// Set up HAP references here so they're available immediately
if (this.api) {
this.Accessory = this.api.platformAccessory;
this.Service = this.api.hap.Service;
this.Characteristic = this.api.hap.Characteristic;
this.UUIDGen = this.api.hap.uuid;
this.api.on('didFinishLaunching', () => {
this.deviceDiscovery();
setInterval(() => this.deviceDiscovery(), this.cache_timeout * 6000);
});
}
}
configureAccessory(accessory) {
const accessoryId = accessory.context?.id;
if (!accessoryId) {
// If we don't have an ID, this accessory is invalid. Remove it.
this.log("Cached accessory has no ID, removing:", accessory.displayName);
this.removeAccessory(accessory);
return;
}
if (this.debug) this.log("Configuring accessory: " + accessoryId);
// Handle duplicate accessory case
if (this.accessories[accessoryId]) {
this.log("Duplicate accessory detected, removing existing one");
try {
this.removeAccessory(this.accessories[accessoryId]);
this.setupDevice(accessory);
} catch (error) {
this.removeAccessory(accessory);
// fallback to old accessory
accessory = this.accessories[accessoryId];
}
} else {
this.setupDevice(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) {
this.log("Starting addAccessory for:", data.name, "ID:", data.id, "Type:", data.type);
if (!this.accessories[data.id]) {
this.log("Creating new accessory for:", data.name);
const uuid = this.UUIDGen.generate(data.id);
const newAccessory = new this.Accessory(data.name, uuid);
this.log("Created new accessory with UUID:", uuid);
this.log("Setting context for new accessory");
newAccessory.context = {
name: data.name,
id: data.id,
type: data.type,
cid: data.cid,
region: data.region,
configModule: data.configModule
};
this.log("Context set:", JSON.stringify(newAccessory.context));
this.log("Setting up device with new accessory");
this.setupDevice(newAccessory);
this.log("Registering platform accessory");
this.api.registerPlatformAccessories("homebridge-vesync-v2", "VesyncPlug", [newAccessory]);
}
this.log("Getting accessory from accessories map, ID:", data.id);
const accessory = this.accessories[data.id];
this.log("Got accessory:", accessory ? "yes" : "no", "Context:", accessory?.context ? "yes" : "no");
this.log("Initializing accessory info");
this.initAccessoryInfo(accessory, data);
this.log("Setting accessory in accessories map");
this.accessories[data.id] = accessory;
}
initAccessoryInfo(accessory, data) {
this.log("Initializing accessory info for:", accessory?.context?.name);
this.log("Accessory state:", {
hasAccessory: !!accessory,
hasContext: !!accessory?.context,
hasService: !!this.Service,
contextData: accessory?.context
});
let info = accessory.getService(this.Service.AccessoryInformation);
this.log("Got AccessoryInformation service");
accessory.context.manufacturer = "Etekcity";
info.setCharacteristic(this.Characteristic.Manufacturer, accessory.context.manufacturer);
accessory.context.model = "ESW01-USA";
info.setCharacteristic(this.Characteristic.Model, accessory.context.model);
info.setCharacteristic(this.Characteristic.SerialNumber, accessory.context.id);
this.log("Getting initial service for:", accessory.context?.name);
const service = accessory.getService(this.Service.Outlet) ||
accessory.getService(this.Service.Fan) ||
accessory.getService(this.Service.AirPurifier) ||
accessory.getService(this.Service.HumidifierDehumidifier) ||
accessory.getService(this.Service.Lightbulb);
if (service && service.getCharacteristic(this.Characteristic.On)) {
this.log("Getting initial On value for:", accessory.context?.name);
service.getCharacteristic(this.Characteristic.On).getValue();
}
}
setupDevice(accessory) {
this.log("SETUP: name=" + accessory?.context?.name + " type=" + accessory?.context?.type);
this.log("PLATFORM-SERVICES-ALL: " + (this.Service ? Object.keys(this.Service).join(',') : 'none'));
const deviceInstance = DeviceFactory.createDevice(
accessory,
this.client,
this.log,
this.debug,
this.Service,
this.Characteristic
);
this.log("DEVICE: Created, configuring...");
deviceInstance.configureService();
}
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) => 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);
});
}
identify(thisPlug, paired, callback) {
this.log("Identify requested for " + thisPlug.name);
callback();
}
}
module.exports = VeseyncPlugPlatform;