UNPKG

@palekseii/homebridge-tuya-platform

Version:

Fork version of official Tuya Homebridge plugin. Brings a bunch of bug fix and new device support.

295 lines 12.5 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const debounce_1 = require("debounce"); const lodash_isequal_1 = __importDefault(require("lodash.isequal")); const TuyaDevice_1 = require("../device/TuyaDevice"); const util_1 = require("../util/util"); const Logger_1 = require("../util/Logger"); const MANUFACTURER = 'Tuya Inc.'; const SCHEMA_CODE = { BATTERY_STATE: ['battery_state'], BATTERY_PERCENT: ['battery_percentage', 'residual_electricity', 'wireless_electricity', 'va_battery', 'battery'], BATTERY_CHARGING: ['charge_state'], }; /** * Homebridge Accessory Categories Documentation: * https://developers.homebridge.io/#/categories * Tuya Standard Instruction Set Documentation: * https://developer.tuya.com/en/docs/iot/standarddescription?id=K9i5ql6waswzq */ class BaseAccessory { constructor(platform, accessory) { var _a, _b; this.platform = platform; this.accessory = accessory; this.Service = this.platform.api.hap.Service; this.Characteristic = this.platform.api.hap.Characteristic; this.deviceManager = this.platform.deviceManager; this.device = this.deviceManager.getDevice(this.accessory.context.deviceID); this.log = new Logger_1.PrefixLogger(this.platform.log, this.device.name.length > 0 ? this.device.name : this.device.id, this.platform.options.debug && (((_a = this.platform.options.debugLevel) !== null && _a !== void 0 ? _a : '').length > 0 ? (_b = this.platform.options.debugLevel) === null || _b === void 0 ? void 0 : _b.includes(this.device.id) : true)); this.intialized = false; this.sendQueue = new Map(); this.debounceSendCommands = (0, debounce_1.debounce)(async () => { const commands = [...this.sendQueue.values()]; if (commands.length === 0) { return; } await this.deviceManager.sendCommands(this.device.id, commands); this.sendQueue.clear(); }, 100); this.addAccessoryInfoService(); this.addBatteryService(); } addAccessoryInfoService() { const service = this.accessory.getService(this.Service.AccessoryInformation) || this.accessory.addService(this.Service.AccessoryInformation); service .setCharacteristic(this.Characteristic.Manufacturer, MANUFACTURER) .setCharacteristic(this.Characteristic.Model, this.device.product_id) .setCharacteristic(this.Characteristic.Name, this.device.name) .setCharacteristic(this.Characteristic.ConfiguredName, this.device.name) .setCharacteristic(this.Characteristic.SerialNumber, this.device.uuid); } addBatteryService() { const percentSchema = this.getSchema(...SCHEMA_CODE.BATTERY_PERCENT); if (!percentSchema) { return; } const { BATTERY_LEVEL_NORMAL, BATTERY_LEVEL_LOW } = this.Characteristic.StatusLowBattery; const service = this.accessory.getService(this.Service.Battery) || this.accessory.addService(this.Service.Battery); const stateSchema = this.getSchema(...SCHEMA_CODE.BATTERY_STATE); if (stateSchema || percentSchema) { service.getCharacteristic(this.Characteristic.StatusLowBattery) .onGet(() => { if (stateSchema) { const status = this.getStatus(stateSchema.code); return (status.value === 'low') ? BATTERY_LEVEL_LOW : BATTERY_LEVEL_NORMAL; } // fallback const status = this.getStatus(percentSchema.code); return (status.value <= 20) ? BATTERY_LEVEL_LOW : BATTERY_LEVEL_NORMAL; }); } const property = percentSchema.property; const multiple = Math.pow(10, property ? property.scale : 0); service.getCharacteristic(this.Characteristic.BatteryLevel) .onGet(() => { const status = this.getStatus(percentSchema.code); return (0, util_1.limit)(status.value / multiple, 0, 100); }); const chargingSchema = this.getSchema(...SCHEMA_CODE.BATTERY_CHARGING); if (chargingSchema) { const { NOT_CHARGING, CHARGING } = this.Characteristic.ChargingState; service.getCharacteristic(this.Characteristic.ChargingState) .onGet(() => { const status = this.getStatus(chargingSchema.code); return status.value ? CHARGING : NOT_CHARGING; }); } } configureStatusActive() { for (const service of this.accessory.services) { if (!service.testCharacteristic(this.Characteristic.StatusActive)) { // silence warning service.addOptionalCharacteristic(this.Characteristic.StatusActive); } service.getCharacteristic(this.Characteristic.StatusActive) .onGet(() => this.device.online); } } async updateAllValues() { for (const service of this.accessory.services) { for (const characteristic of service.characteristics) { if (characteristic.UUID === this.Characteristic.ProgrammableSwitchEvent.UUID) { continue; } let newValue = characteristic.value; const getHandler = characteristic['getHandler']; if (getHandler) { try { newValue = await getHandler(); } catch (error) { // TODO: why `characteristic.updateValue(HapStatusError)` not working? // newValue = error as Error; continue; } } if (characteristic.value !== newValue && !(newValue instanceof Error)) { this.log.debug('[%s/%s/%s] Update value: %o => %o', service.constructor.name, service.subtype, characteristic.constructor.name, characteristic.value, newValue); } characteristic.updateValue(newValue); } } } checkOnlineStatus() { if (!this.device.online) { const { HapStatusError, HAPStatus } = this.platform.api.hap; throw new HapStatusError(-70402 /* HAPStatus.SERVICE_COMMUNICATION_FAILURE */); } } getSchema(...codes) { for (const code of codes) { const schema = this.device.schema.find(schema => schema.code === code); if (!schema) { continue; } // Readable schema must have a status if ([TuyaDevice_1.TuyaDeviceSchemaMode.READ_WRITE, TuyaDevice_1.TuyaDeviceSchemaMode.READ_ONLY].includes(schema.mode) && !this.getStatus(schema.code)) { continue; } return schema; } return undefined; } getStatus(code) { return this.device.status.find(status => status.code === code); } async sendCommands(commands, debounce = false) { if (commands.length === 0) { return; } commands = commands.filter((status) => status.code && status.value !== undefined); if (this.device.online === false) { this.log.warn('Device is offline, skip send command.'); this.updateAllValues(); const { HapStatusError, HAPStatus } = this.platform.api.hap; throw new HapStatusError(-70402 /* HAPStatus.SERVICE_COMMUNICATION_FAILURE */); return; } // Update cache immediately for (const newStatus of commands) { const oldStatus = this.device.status.find(_status => _status.code === newStatus.code); if (oldStatus) { oldStatus.value = newStatus.value; } } if (debounce === false) { return await this.deviceManager.sendCommands(this.device.id, commands); } for (const newStatus of commands) { // Update send queue this.sendQueue.set(newStatus.code, newStatus); } this.debounceSendCommands(); } checkRequirements() { let result = true; for (const codes of this.requiredSchema()) { const schema = this.getSchema(...codes); if (schema) { continue; } this.log.warn('Product Category: %s', this.device.category); this.log.warn('Missing one of the required schema: %s', codes); this.log.warn('Please switch device control mode to "DP Insctrution", and set `deviceOverrides` manually.'); this.log.warn('Detail information: https://github.com/0x5e/homebridge-tuya-platform#faq'); result = false; } if (!result) { this.log.warn('Existing schema: %o', this.device.schema); } return result; } requiredSchema() { return []; } configureServices() { // } async onDeviceInfoUpdate(info) { this.updateAllValues(); } async onDeviceStatusUpdate(status) { this.updateAllValues(); } } // Overriding getSchema, getStatus, sendCommands class OverridedBaseAccessory extends BaseAccessory { constructor() { super(...arguments); this.eval = (script, device, value) => eval(script); } getOverridedSchema(code) { const schemaConfig = this.platform.getDeviceSchemaConfig(this.device, code); if (!schemaConfig) { return undefined; } const oldSchema = this.device.schema.find(schema => schema.code === schemaConfig.code); if (!oldSchema) { return undefined; } const schema = { code, mode: oldSchema.mode, type: schemaConfig.type || oldSchema.type, property: schemaConfig.property || oldSchema.property, _hidden: schemaConfig.hidden, }; if (!(0, lodash_isequal_1.default)(oldSchema, schema)) { this.log.debug('Override schema %o => %o', oldSchema, schema); } return schema; } getSchema(...codes) { for (const code of codes) { const schema = this.getOverridedSchema(code) || super.getSchema(code); if (!schema) { continue; } if (schema['_hidden']) { return undefined; } return schema; } return undefined; } getOverridedStatus(code) { const schemaConfig = this.platform.getDeviceSchemaConfig(this.device, code); if (!schemaConfig) { return undefined; } const oldStatus = super.getStatus(schemaConfig.code); if (!oldStatus) { return undefined; } const status = { code: schemaConfig.newCode || schemaConfig.code, value: oldStatus.value }; if (schemaConfig.onGet) { status.value = this.eval(schemaConfig.onGet, this.device, oldStatus.value); } if (!(0, lodash_isequal_1.default)(oldStatus, status)) { this.log.debug('Override status %o => %o', oldStatus, status); } return status; } getStatus(code) { return this.getOverridedStatus(code) || super.getStatus(code); } async sendCommands(commands, debounce) { // convert to original commands for (const command of commands) { const schemaConfig = this.platform.getDeviceSchemaConfig(this.device, command.code); if (!schemaConfig) { continue; } const oldCommand = { code: schemaConfig.code, value: command.value }; if (schemaConfig.onSet) { oldCommand.value = this.eval(schemaConfig.onSet, this.device, command.value); } if (!(0, lodash_isequal_1.default)(oldCommand, command)) { this.log.debug('Override command %o => %o', command, oldCommand); command.code = oldCommand.code; command.value = oldCommand.value; } } await super.sendCommands(commands, debounce); } } exports.default = OverridedBaseAccessory; //# sourceMappingURL=BaseAccessory.js.map