// Libs import { CharacteristicEventTypes, CharacteristicGetCallback, CharacteristicSetCallback, CharacteristicValue, } from 'homebridge' // XiaoMi import { MiDevice } from './xiaomi.device' import { ErrorMessages } from './xiaomi.constant' // Utils import { Shared } from '../shared' import { debounceAsync } from '../utils' import { DeviceResponseValueMapping, MIIORegisterConfig, MIIOSpecMapping, PropertyValue } from '../main' export class MIIODevice extends MiDevice { // Base private propertiesCache: DeviceResponseValueMapping = {} private valueFormattedCache: { [type: string]: any } = {} // Setup public addCharacteristicListener = (type: any, config: MIIORegisterConfig) => { const service = config.service || this.service const characteristic = service.getCharacteristic(type) Shared.log.debug('MiIO is adding characteristic: ', characteristic.displayName, characteristic.UUID, 'to service:', service.displayName, service.getServiceId()) if ('get' in config) { characteristic.on(CharacteristicEventTypes.GET, async (callback: CharacteristicGetCallback) => { const startTime = Date.now() let valueFormatted: PropertyValue let isValueCallbackInTimes = false // No matter what happen, callback needs to be call after requestCharacteristicTimeout setTimeout(() => { // Only valid if CallbackInTimes are no trigger if (!isValueCallbackInTimes) { // Callback with cached value or default value if (valueFormatted === undefined) { return callback(undefined, this.valueFormattedCache[type] || config.get.defaultValue) } } }, Shared.config.requestCharacteristicTimeout) // Start to fetch data try { Shared.log.debug(`MiIO start to getting property ${type.name}`) const property = await this.getProperty() valueFormatted = config.get.formatter(property) this.valueFormattedCache[type] = valueFormatted Shared.log.debug(`MiIO getting property ${type.name} is formatted`, valueFormatted) // Update value const currentTime = Date.now() if (currentTime - startTime < Shared.config.requestCharacteristicTimeout) { // in timeout, callback the value directly isValueCallbackInTimes = true callback(undefined, valueFormatted) } else { // or using the updateValue characteristic.updateValue(valueFormatted) } } catch (e) { Shared.log.error(`MiIO getting ${type.name} ERROR`, e) callback(e) } }) } if ('set' in config) { const set = config.set if (set) { const validator = set.validator || (() => true) characteristic.on(CharacteristicEventTypes.SET, async (value: CharacteristicValue, callback: CharacteristicSetCallback) => { try { Shared.log.debug(`MiIO start to setting property ${type.name} with raw value ${value}`) const allow = Object.keys(this.propertiesCache).length > 0 ? validator(value, this.propertiesCache) : true Shared.log.debug(`MiIO setting validator`, allow ? 'allow to set, applying...' : 'prevent to set, skipped...') if (allow) { const valueFormatted = set.formatter(value, this.propertiesCache) Shared.log.debug(`MiIO setting property ${type.name} is formatted`, valueFormatted, 'start applying to device') await this.setProperty(set.property, valueFormatted) Shared.log.debug(`MiIO setting property ${type.name} is success`) } callback(undefined) } catch (e) { Shared.log.error(`MiIO setting ${type.name} ERROR`, e) callback(e) } }) } } } // Get & Set private getSpec = () => Object.values(this.specs) private getProperty = debounceAsync(async () => { // Guard const device = await this.device.getInstance() if (!device?.did) throw new Error(ErrorMessages.NotConnect) // Spec const targetSpecs = this.getSpec() // Get properties const properties = await device.miioCall('get_prop', targetSpecs) Shared.log.debug(`MiIO Merging request of ${this.identify.name} ${this.identify.address}`) this.propertiesCache = targetSpecs.reduce((acc, cur, idx) => ({ ...acc, [cur]: properties[idx] }), {} as DeviceResponseValueMapping) return this.propertiesCache }, Shared.config.requestPropertyDebounceThreshold) private setProperty = async (name: string, value: PropertyValue[]) => { // Guard const device = await this.device.getInstance() if (!device?.did) throw new Error(ErrorMessages.NotConnect) // Action return device.miioCall(name, value) } }