homebridge-vesync-v2
Version:
A Homebridge plugin for controlling VeSync smart devices including outlets, air purifiers, and humidifiers
278 lines (246 loc) • 10.2 kB
JavaScript
"use strict";
const BaseDevice = require('./BaseDevice');
class AirPurifierDevice extends BaseDevice {
constructor(accessory, client, log, debug, Service, Characteristic) {
super(accessory, client, log, debug, Service, Characteristic);
this.log("AP-INIT: Service=" + (!!this.Service) + " AP=" + (!!this.Service?.AirPurifier));
this.log("AP-CHARS: " + [
"Active=" + (!!this.Characteristic?.Active),
"Current=" + (!!this.Characteristic?.CurrentAirPurifierState),
"Target=" + (!!this.Characteristic?.TargetAirPurifierState),
"Speed=" + (!!this.Characteristic?.RotationSpeed)
].join(' '));
this.lastStatusCheck = 0;
}
configureService() {
this.log("AP-CONFIG: name=" + this.accessory?.context?.name);
if (!this.Service) {
this.log.error("AP-ERROR: Service missing");
throw new Error('Service is not initialized');
}
if (!this.Service.AirPurifier) {
this.log.error("AP-ERROR: AirPurifier service missing");
throw new Error('AirPurifier service not available');
}
if (!this.accessory) {
this.log.error("AP-ERROR: Accessory missing");
throw new Error('Accessory is not initialized');
}
this.log("AP-SERVICE: Adding service");
try {
const service = this.accessory.getService(this.Service.AirPurifier) ||
this.accessory.addService(this.Service.AirPurifier, this.accessory.context.name);
this.log("AP-CHARS-CONFIG: Adding characteristics");
// Required Characteristics
this.log("AP-CHAR: Adding Active");
service.getCharacteristic(this.Characteristic.Active)
.on('get', this.getActive.bind(this))
.on('set', this.setActive.bind(this));
this.log("AP-CHAR: Adding CurrentState");
service.getCharacteristic(this.Characteristic.CurrentAirPurifierState)
.on('get', this.getCurrentState.bind(this));
this.log("AP-CHAR: Adding TargetState");
service.getCharacteristic(this.Characteristic.TargetAirPurifierState)
.on('get', this.getTargetState.bind(this))
.on('set', this.setTargetState.bind(this));
this.log("AP-CHAR: Adding RotationSpeed");
service.getCharacteristic(this.Characteristic.RotationSpeed)
.setProps({
minValue: 0,
maxValue: 100,
minStep: 1
})
.on('get', this.getRotationSpeed.bind(this))
.on('set', this.setRotationSpeed.bind(this));
this.log("AP-SUCCESS: Service configured");
return service;
} catch (error) {
this.log.error("AP-ERROR: Service setup failed:", error.message);
throw error;
}
}
// Helper method to get device status
async _getStatus() {
this.log("AP-API: Getting status");
if (!this.client) {
throw new Error('Client is not initialized');
}
// Rate limiting: Only allow one request every 5 seconds
const now = Date.now();
const timeSinceLastCheck = now - this.lastStatusCheck;
if (timeSinceLastCheck < 5000) {
this.log("AP-API: Using cached status (last check was " + timeSinceLastCheck + "ms ago)");
if (this.lastStatus) {
return { data: this.lastStatus };
}
}
try {
const method = 'getPurifierStatus';
this.log("AP-API: Calling bypassV2 method=" + method);
const requestData = {
method: 'bypassV2',
deviceRegion: this.accessory.context.region,
cid: this.accessory.context.cid,
configModule: this.accessory.context.configModule,
payload: {
data: {},
method: method,
source: 'APP'
}
};
this.log("AP-API: Request=", JSON.stringify(requestData));
const response = await this.client.bypassV2(requestData);
this.lastStatusCheck = now;
this.log("AP-API: Raw response=", JSON.stringify(response));
if (!response) {
this.log.error("AP-API: Response is null or undefined");
throw new Error('Invalid response from device');
}
if (!response.data) {
this.log.error("AP-API: Response has no data:", JSON.stringify(response));
throw new Error('Invalid response from device');
}
this.log("AP-API: Status response=", JSON.stringify(response.data));
this.lastStatus = response.data;
return response;
} catch (error) {
this.log.error("AP-ERROR: Getting status:", error.message);
if (error.response) {
this.log.error("AP-ERROR: Response data:", JSON.stringify(error.response));
}
throw error;
}
}
async getActive(callback) {
this.log("AP-GET: Active");
try {
const status = await this._getStatus();
this.log("AP-GET: Active status=", status?.data);
if (!status.data || typeof status.data.power === 'undefined') {
throw new Error('Invalid power state data');
}
callback(null, status.data.power === 1 ? 1 : 0);
} catch (error) {
this.log.error("AP-ERROR: Getting active state:", error.message);
callback(error);
}
}
async setActive(value, callback) {
this.log("AP-SET: Active=" + value);
try {
await this.client.bypassV2({
method: 'bypassV2',
deviceRegion: this.accessory.context.region,
cid: this.accessory.context.cid,
configModule: this.accessory.context.configModule,
payload: {
data: {
power: value ? 1 : 0
},
method: 'setSwitch',
source: 'APP'
}
});
this.log("AP-SET: Active success");
callback();
} catch (error) {
this.log.error("AP-ERROR: Setting active state:", error.message);
callback(error);
}
}
async getCurrentState(callback) {
this.log("AP-GET: CurrentState");
try {
const status = await this._getStatus();
this.log("AP-GET: CurrentState status=", status?.data);
if (!status.data || typeof status.data.power === 'undefined') {
throw new Error('Invalid current state data');
}
let currentState;
if (status.data.power === 0) {
currentState = 0; // INACTIVE
} else {
currentState = status.data.level > 0 ? 2 : 1;
}
callback(null, currentState);
} catch (error) {
this.log.error("AP-ERROR: Getting current state:", error.message);
callback(error);
}
}
async getTargetState(callback) {
try {
const status = await this._getStatus();
if (!status.data || typeof status.data.mode === 'undefined') {
throw new Error('Invalid target state data');
}
// Map device mode to HomeKit states
// AUTO = 0
// MANUAL = 1
const targetState = status.data.mode === 'auto' ? 0 : 1;
callback(null, targetState);
} catch (error) {
this.log.error('Error getting target state:', error);
callback(error);
}
}
async setTargetState(value, callback) {
try {
await this.client.bypassV2({
method: 'bypassV2',
deviceRegion: this.accessory.context.region,
cid: this.accessory.context.cid,
configModule: this.accessory.context.configModule,
payload: {
data: {
mode: value === 0 ? 'auto' : 'manual'
},
method: 'setPurifierMode',
source: 'APP'
}
});
callback();
} catch (error) {
this.log.error('Error setting target state:', error);
callback(error);
}
}
async getRotationSpeed(callback) {
try {
const status = await this._getStatus();
if (!status.data || typeof status.data.level === 'undefined') {
throw new Error('Invalid speed data');
}
// Convert device speed to percentage (assuming level is 1-3)
const percentage = Math.round((status.data.level / 3) * 100);
callback(null, percentage);
} catch (error) {
this.log.error('Error getting rotation speed:', error);
callback(error);
}
}
async setRotationSpeed(value, callback) {
try {
// Convert percentage to device levels (1-3)
const level = Math.max(1, Math.min(3, Math.round((value / 100) * 3)));
await this.client.bypassV2({
method: 'bypassV2',
deviceRegion: this.accessory.context.region,
cid: this.accessory.context.cid,
configModule: this.accessory.context.configModule,
payload: {
data: {
level: level
},
method: 'setLevel',
source: 'APP'
}
});
callback();
} catch (error) {
this.log.error('Error setting rotation speed:', error);
callback(error);
}
}
}
module.exports = AirPurifierDevice;