UNPKG

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
"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;