import once from 'lodash.once' import debounce from 'lodash.debounce' import { CharacteristicEventTypes, CharacteristicGetCallback, CharacteristicSetCallback, CharacteristicValue, Service } from 'homebridge' import { Shared } from './shared' import { GLOBAL_DEVICE_FORMATTED_VALUE_CACHE, GLOBAL_DEVICE_PROPERTIES_CACHE } from './constant' import { EventAdaptorConfig, Resolve } from './main' import { HAPStatus } from 'hap-nodejs/dist/lib/HAPServer' import { WithUUID } from 'hap-nodejs/dist/types' import { Characteristic } from 'hap-nodejs/dist/lib/Characteristic' export function sleep (delay: number) { return new Promise((resolve) => setTimeout(resolve, delay)) } class Queue { private queue: Resolve[] = [] private readonly debouncedResolveAll: () => void constructor (value: (...args: any) => any, delay: number) { this.debouncedResolveAll = debounce(async () => { const v = await value() const queueWillResolve = [...this.queue] this.queue = [] queueWillResolve.forEach(resolve => resolve(v)) }, delay, { trailing: true }) } public append (resolve: Resolve) { this.queue.push(resolve) this.debouncedResolveAll() } } export function debounceAsync any> (func: T, delay: number): () => Promise> { const queue = new Queue(func, delay) return () => new Promise((resolve => queue.append(resolve))) } export function handleAddEventListener ({ name, type, config, service, getProperty, setProperty, }: { name: string, type: WithUUID<{ new (): Characteristic }>, config: EventAdaptorConfig, service?: Service, getProperty: () => Promise, setProperty: (property: any, value: any) => void, }) { // Guard: empty service props if (!service) { Shared.log.error(`[${name}] service is not defined`) return } // Guard: empty service props if (!service) { Shared.log.error(`[${name}] service is not defined`) return } // Property cache if (!GLOBAL_DEVICE_PROPERTIES_CACHE.get(name)) { GLOBAL_DEVICE_PROPERTIES_CACHE.set(name, {}) } const previousProperty = GLOBAL_DEVICE_PROPERTIES_CACHE.get(name) || {} // Formatted cache if (!GLOBAL_DEVICE_FORMATTED_VALUE_CACHE.get(name)) { GLOBAL_DEVICE_FORMATTED_VALUE_CACHE.set(name, {}) } const previousFormatted = GLOBAL_DEVICE_FORMATTED_VALUE_CACHE.get(name) || {} // Add handlers const characteristic = service.getCharacteristic(type) Shared.log.debug(`[${name}] adding characteristic: `, characteristic.displayName, characteristic.UUID, 'to service:', service.displayName, service.UUID) // Add getter // it will get the value from device and format to homeBridge value if ('get' in config) { characteristic.on(CharacteristicEventTypes.GET, async (callback: CharacteristicGetCallback) => { // No matter what happen, callback should be call after requestCharacteristicTimeout let isCallbackTimedOut = false const callbackOnce = once(callback) const timeoutCallbackTimer = setTimeout(() => { // Mark as timed out isCallbackTimedOut = true callbackOnce(HAPStatus.OPERATION_TIMED_OUT, previousFormatted[type.toString()] || config.get.defaultValue) }, Shared.config.requestCharacteristicTimeout) // Start to fetch data try { Shared.log.debug(`[${name}] start to getting property ${type.name} for service ${service.name}`) // Get value const property = await getProperty() Shared.log.debug(`[${name}] property ${type.name} get success`) // Cache value GLOBAL_DEVICE_PROPERTIES_CACHE.set(name, property) // Format value const valueFormatted = config.get.formatter(property) Shared.log.debug(`[${name}] getting property ${type.name} is formatted`, valueFormatted) // Cache value previousFormatted[type.toString()] = valueFormatted // Cancel the fallback timer first clearTimeout(timeoutCallbackTimer) // Then callback the value directly callbackOnce(HAPStatus.SUCCESS, valueFormatted) // Update the latest value setTimeout(() => characteristic.updateValue(valueFormatted), 0) } catch (e) { Shared.log.error(`[${name}] getting property ${type.name} ERROR`, e) callback(e) } }) } // Add setter // it will receive value from homeBridge and format to device value 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(`[${name}] start to setting property ${type.name} with raw value ${value}`) // Guard: validator const allow = Object.keys(previousProperty).length > 0 ? validator(value, previousProperty) : true Shared.log.debug(`[${name}] setting validator`, allow ? 'allow to set, applying...' : 'prevent to set, skipped...') if (!allow) return // Format value const valueFormatted = set.formatter(value, previousProperty) Shared.log.debug(`[${name}] setting property ${type.name} is formatted`, valueFormatted, 'start applying to device') // Set value to device await setProperty(set.property, valueFormatted) Shared.log.debug(`[${name}] setting property ${type.name} is success`) // Hb Callback callback(undefined) } catch (e) { Shared.log.error(`[${name}] setting ${type.name} ERROR`, e) callback(e) } }) } } }