homebridge-tsvesync
Version:
Homebridge plugin for VeSync devices including Levoit air purifiers, humidifiers, and Etekcity smart outlets
286 lines • 12.8 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.BaseAccessory = void 0;
const retry_1 = require("../utils/retry");
const logger_1 = require("../utils/logger");
class BaseAccessory {
constructor(platform, accessory, device) {
this.needsRetry = false;
this.isInitialized = false;
this.isInitializing = false;
// Cache for device details to reduce API calls
this.deviceDetailsCache = null;
this.lastDetailsFetch = 0;
this.CACHE_TTL = 60 * 1000; // 1 minute cache TTL
this.platform = platform;
this.accessory = accessory;
this.device = device;
// Create initialization promise
this.initializationPromise = new Promise((resolve) => {
this.initializationResolver = resolve;
});
// Initialize managers
this.logger = new logger_1.PluginLogger(this.platform.log, this.platform.config.debug);
this.retryManager = new retry_1.RetryManager(this.platform.log, this.platform.config.retry);
// Set up the accessory
this.setupAccessory();
this.logger.debug('Accessory created', this.getLogContext());
}
/**
* Get base context for logging
*/
getLogContext(operation, characteristic, value) {
return {
deviceName: this.device.deviceName,
deviceType: this.getDeviceType(),
operation,
characteristic,
value,
};
}
/**
* Get the device type for polling configuration
*/
getDeviceType() {
// Default implementation - override in subclasses if needed
if (this.device.deviceType.toLowerCase().includes('air')) {
return 'airPurifier';
}
else if (this.device.deviceType.toLowerCase().includes('humidifier')) {
return 'humidifier';
}
else if (this.device.deviceType.toLowerCase().includes('fan')) {
return 'fan';
}
else if (this.device.deviceType.toLowerCase().includes('bulb')) {
return 'light';
}
else if (this.device.deviceType.toLowerCase().includes('outlet')) {
return 'outlet';
}
return 'default';
}
/**
* Set up the accessory services
*/
setupAccessory() {
// Set accessory information
const infoService = this.accessory.getService(this.platform.Service.AccessoryInformation);
if (infoService) {
infoService
.setCharacteristic(this.platform.Characteristic.Manufacturer, 'VeSync')
.setCharacteristic(this.platform.Characteristic.Model, this.device.deviceType)
.setCharacteristic(this.platform.Characteristic.SerialNumber, this.device.uuid);
}
// Set up device-specific service
this.setupService();
}
/**
* Initialize the accessory
*/
async initialize() {
if (this.isInitializing) {
return;
}
this.isInitializing = true;
try {
// Ensure platform is ready before proceeding
await this.platform.isReady();
// Try to refresh the device details first
try {
// Use type assertion to access potential getDetails method
const deviceWithDetails = this.device;
if (typeof deviceWithDetails.getDetails === 'function') {
this.logger.debug('Refreshing device details during initialization', this.getLogContext());
await deviceWithDetails.getDetails();
this.logger.debug(`Device status after refresh: ${this.device.deviceStatus}`, this.getLogContext());
}
}
catch (refreshError) {
this.logger.warn('Failed to refresh device details during initialization', this.getLogContext(), refreshError instanceof Error ? refreshError : new Error(String(refreshError)));
}
// Update states using device info we have
await this.updateDeviceSpecificStates(this.device);
this.isInitialized = true;
this.logger.debug('Accessory initialized', this.getLogContext());
}
catch (error) {
const err = error instanceof Error ? error : new Error(String(error));
this.logger.error('Failed to initialize device state', this.getLogContext(), err);
}
finally {
this.isInitializing = false;
this.initializationResolver();
}
}
/**
* Sync the device state with VeSync
*/
async syncDeviceState() {
try {
// Try to refresh the device details first, using cache when possible
try {
// Use type assertion to access potential getDetails method
const deviceWithDetails = this.device;
if (typeof deviceWithDetails.getDetails === 'function') {
const now = Date.now();
const shouldUseCache = this.deviceDetailsCache !== null &&
(now - this.lastDetailsFetch < this.CACHE_TTL);
if (shouldUseCache) {
this.logger.debug('Using cached device details', this.getLogContext());
// Apply cached details to device if available
if (this.deviceDetailsCache) {
Object.assign(deviceWithDetails, this.deviceDetailsCache);
}
}
else {
this.logger.debug('Refreshing device details during sync', this.getLogContext());
const refreshResult = await deviceWithDetails.getDetails();
// Check if the API call was blocked due to quota
if (refreshResult === null) {
this.logger.warn('Device refresh skipped due to API quota limits', this.getLogContext());
// Continue with existing device state or cached state if available
}
else {
this.logger.debug(`Device status after refresh: ${this.device.deviceStatus}`, this.getLogContext());
// Update cache with fresh data
this.deviceDetailsCache = { ...deviceWithDetails };
this.lastDetailsFetch = now;
}
}
}
}
catch (refreshError) {
// Check if this is a quota error
const errorMsg = String(refreshError);
if (errorMsg.includes('quota') || errorMsg.includes('rate limit')) {
this.logger.warn('API quota exceeded during device refresh', this.getLogContext());
}
else {
this.logger.warn('Failed to refresh device details during sync', this.getLogContext(), refreshError instanceof Error ? refreshError : new Error(String(refreshError)));
}
}
// Update states using the device's internal state (even if refresh failed)
await this.updateDeviceSpecificStates(this.device);
}
catch (error) {
await this.handleDeviceError('Failed to sync device state', error instanceof Error ? error : new Error(String(error)));
}
}
/**
* Helper method to convert air quality values to HomeKit format
*/
convertAirQualityToHomeKit(pm25) {
if (pm25 <= 12)
return 1; // EXCELLENT
if (pm25 <= 35)
return 2; // GOOD
if (pm25 <= 55)
return 3; // FAIR
if (pm25 <= 150)
return 4; // INFERIOR
return 5; // POOR
}
/**
* Helper method to set up a characteristic with get/set handlers
*/
setupCharacteristic(characteristic, onGet, onSet, props, service) {
const targetService = service || this.service;
const char = targetService.getCharacteristic(characteristic);
if (onGet) {
char.onGet(async () => {
const context = this.getLogContext('get characteristic', characteristic.name);
try {
const value = await onGet();
this.logger.stateChange({ ...context, value });
return value;
}
catch (error) {
this.logger.error('Failed to get characteristic value', context, error);
throw error;
}
});
}
if (onSet) {
char.onSet(async (value) => {
const context = this.getLogContext('set characteristic', characteristic.name, value);
try {
await onSet(value);
this.logger.stateChange(context);
}
catch (error) {
this.logger.error('Failed to set characteristic value', context, error);
throw error;
}
});
}
if (props) {
char.setProps(props);
}
}
/**
* Helper method to update a characteristic value
*/
updateCharacteristicValue(characteristic, value) {
this.service.updateCharacteristic(characteristic, value);
this.logger.stateChange({
...this.getLogContext('update characteristic', characteristic.name, value),
deviceName: this.device.deviceName,
deviceType: this.getDeviceType(),
});
}
/**
* Persist device state to accessory context
*/
async persistDeviceState(key, value) {
var _a;
try {
this.accessory.context.device = {
...this.accessory.context.device,
details: {
...(_a = this.accessory.context.device) === null || _a === void 0 ? void 0 : _a.details,
[key]: value
}
};
await this.platform.api.updatePlatformAccessories([this.accessory]);
this.logger.debug(`Updated ${key} to ${value}`, this.getLogContext('persist state'));
}
catch (error) {
this.handleDeviceError(`persist ${key} state`, error);
}
}
/**
* Handle device errors with appropriate recovery actions
*/
async handleDeviceError(message, error) {
var _a, _b, _c, _d, _e, _f, _g, _h, _j;
const context = this.getLogContext();
const retryCount = this.retryManager.getRetryCount();
const wrappedError = error instanceof Error ? error : new Error(JSON.stringify(error));
// Handle device not found error
if (((_a = error === null || error === void 0 ? void 0 : error.error) === null || _a === void 0 ? void 0 : _a.code) === 4041008 || ((_c = (_b = error === null || error === void 0 ? void 0 : error.error) === null || _b === void 0 ? void 0 : _b.msg) === null || _c === void 0 ? void 0 : _c.includes('Device not found'))) {
this.logger.warn('Device not found', context, wrappedError);
return;
}
// Handle quota limit error
if (((_d = error === null || error === void 0 ? void 0 : error.error) === null || _d === void 0 ? void 0 : _d.code) === -16906086 || ((_f = (_e = error === null || error === void 0 ? void 0 : error.error) === null || _e === void 0 ? void 0 : _e.msg) === null || _f === void 0 ? void 0 : _f.includes('quota'))) {
this.logger.warn(`API quota exceeded`, context, wrappedError);
return;
}
// Handle rate limit error
if (((_g = error === null || error === void 0 ? void 0 : error.message) === null || _g === void 0 ? void 0 : _g.includes('rate limit')) || ((_h = error === null || error === void 0 ? void 0 : error.message) === null || _h === void 0 ? void 0 : _h.includes('429'))) {
this.logger.warn(`Hit API rate limit (attempt ${retryCount})`, context, wrappedError);
return;
}
// Handle network errors
if (((_j = error === null || error === void 0 ? void 0 : error.code) === null || _j === void 0 ? void 0 : _j.startsWith('ECONN')) || (error === null || error === void 0 ? void 0 : error.code) === 'ETIMEDOUT') {
this.logger.warn('Network error', context, wrappedError);
return;
}
// Handle other errors
this.logger.error(message, context, wrappedError);
this.needsRetry = true;
}
}
exports.BaseAccessory = BaseAccessory;
//# sourceMappingURL=base.accessory.js.map