diff --git a/src/accessories/BaseAccessory.ts b/src/accessories/BaseAccessory.ts index d295581..61dba5d 100644 --- a/src/accessories/BaseAccessory.ts +++ b/src/accessories/BaseAccessory.ts @@ -1,6 +1,14 @@ import {HomebridgeAccessory, TuyaWebPlatform} from '../platform'; -import {Categories, Logger, WithUUID} from 'homebridge'; -import {Characteristic, CharacteristicGetCallback, CharacteristicValue, Nullable, Service} from 'homebridge'; +import { + Categories, + Characteristic, + CharacteristicGetCallback, + CharacteristicValue, + Logger, + Nullable, + Service, + WithUUID, +} from 'homebridge'; import {TuyaDevice} from '../TuyaWebApi'; import {PLUGIN_NAME} from '../settings'; import {inspect} from 'util'; @@ -12,7 +20,7 @@ export type CharacteristicConstructor = WithUUID<{ type UpdateCallback = (data?: DeviceConfig['data'], callback?: CharacteristicGetCallback) => void class Cache { - private state: Map}> = new Map(); + private state: Map }> = new Map(); private _valid = false; public get valid(): boolean { @@ -34,7 +42,7 @@ class Cache { public get(char: CharacteristicConstructor): Nullable { const cache = this.state.get(char); - if(!this._valid || !cache || cache.validUntil < Cache.getCurrentEpoch()) { + if (!this._valid || !cache || cache.validUntil < Cache.getCurrentEpoch()) { return null; } return cache.value; @@ -88,18 +96,21 @@ export abstract class BaseAccessory { deviceConfig: FanAccessoryConfig) { super(platform, homebridgeAccessory, deviceConfig, Categories.FAN); - new OnCharacteristic(this as BaseAccessory); + new ActiveCharacteristic(this as BaseAccessory); new RotationSpeedCharacteristic(this as BaseAccessory); } } diff --git a/src/accessories/characteristics/active.ts b/src/accessories/characteristics/active.ts new file mode 100644 index 0000000..8a27964 --- /dev/null +++ b/src/accessories/characteristics/active.ts @@ -0,0 +1,60 @@ +import {TuyaDevice, TuyaDeviceState} from '../../TuyaWebApi'; +import {CharacteristicGetCallback, CharacteristicSetCallback, CharacteristicValue} from 'homebridge'; +import {TuyaWebCharacteristic} from './base'; +import {BaseAccessory} from '../BaseAccessory'; + +export type ActiveCharacteristicData = { state: boolean | 'true' | 'false' } +type DeviceWithActiveCharacteristic = TuyaDevice + +export class ActiveCharacteristic extends TuyaWebCharacteristic { + public static Title = 'Characteristic.Active' + + public static HomekitCharacteristic(accessory: BaseAccessory) { + return accessory.platform.Characteristic.Active; + } + + public static isSupportedByAccessory(accessory): boolean { + return accessory.deviceConfig.data.state !== undefined; + } + + public getRemoteValue(callback: CharacteristicGetCallback): void { + // Retrieve state from cache + const cachedState = this.accessory.getCachedState(this.homekitCharacteristic); + if (cachedState) { + callback(null, cachedState); + } else { + // Retrieve device state from Tuya Web API + this.accessory.platform.tuyaWebApi.getDeviceState(this.accessory.deviceId).then((data) => { + this.debug('[GET] %s', data?.state); + this.updateValue(data, callback); + }).catch((error) => { + this.error('[GET] %s', error.message); + this.accessory.invalidateCache(); + callback(error); + }); + } + } + + public setRemoteValue(homekitValue: CharacteristicValue, callback: CharacteristicSetCallback): void { + // Set device state in Tuya Web API + const value = homekitValue ? 1 : 0; + + this.accessory.platform.tuyaWebApi.setDeviceState(this.accessory.deviceId, 'turnOnOff', {value}).then(() => { + this.debug('[SET] %s %s', homekitValue, value); + this.accessory.setCachedState(this.homekitCharacteristic, homekitValue); + callback(); + }).catch((error) => { + this.error('[SET] %s', error.message); + this.accessory.invalidateCache(); + callback(error); + }); + } + + updateValue(data: DeviceWithActiveCharacteristic['data'] | undefined, callback?: CharacteristicGetCallback): void { + if (data?.state !== undefined) { + const stateValue = (String(data.state).toLowerCase() === 'true'); + this.accessory.setCharacteristic(this.homekitCharacteristic, stateValue, !callback); + callback && callback(null, stateValue); + } + } +} diff --git a/src/accessories/characteristics/index.ts b/src/accessories/characteristics/index.ts index 66436b0..7859adc 100644 --- a/src/accessories/characteristics/index.ts +++ b/src/accessories/characteristics/index.ts @@ -1,3 +1,4 @@ +export * from './active'; export * from './brightness'; export * from './colorTemperature'; export * from './hue'; diff --git a/src/config.ts b/src/config.ts new file mode 100644 index 0000000..a228d33 --- /dev/null +++ b/src/config.ts @@ -0,0 +1,18 @@ +import {TuyaDeviceType, TuyaPlatform} from './TuyaWebApi'; +import {PlatformConfig} from 'homebridge'; + +export type TuyaDeviceDefaults = { id: string, device_type: TuyaDeviceType } + +type Config = { + options?: { + username?: string, + password?: string, + countryCode?: string, + platform?: TuyaPlatform, + pollingInterval?: number + }, + defaults?: Partial[], + scenes?: boolean | string[] +} + +export type TuyaWebConfig = PlatformConfig & Config; diff --git a/src/helpers/DeepPartial.ts b/src/helpers/DeepPartial.ts deleted file mode 100644 index bc1ff91..0000000 --- a/src/helpers/DeepPartial.ts +++ /dev/null @@ -1,3 +0,0 @@ -export type DeepPartial = { - [P in keyof T]?: DeepPartial; -}; diff --git a/src/platform.ts b/src/platform.ts index 0b8ed69..958cdfc 100644 --- a/src/platform.ts +++ b/src/platform.ts @@ -1,15 +1,7 @@ -import { - API, - Characteristic, - DynamicPlatformPlugin, - Logger, - PlatformAccessory, - PlatformConfig, - Service, -} from 'homebridge'; +import {API, Characteristic, DynamicPlatformPlugin, Logger, PlatformAccessory, Service} from 'homebridge'; import {PLATFORM_NAME, PLUGIN_NAME} from './settings'; -import {TuyaDevice, TuyaDeviceType, TuyaDeviceTypes, TuyaPlatform, TuyaPlatforms, TuyaWebApi} from './TuyaWebApi'; +import {TuyaDevice, TuyaDeviceType, TuyaDeviceTypes, TuyaPlatforms, TuyaWebApi} from './TuyaWebApi'; import { BaseAccessory, DimmerAccessory, @@ -19,23 +11,12 @@ import { SceneAccessory, SwitchAccessory, } from './accessories'; -import {DeepPartial} from './helpers/DeepPartial'; +import {TuyaDeviceDefaults, TuyaWebConfig} from './config'; export type HomebridgeAccessory = PlatformAccessory & { controller?: BaseAccessory } -type Config = { - options: { - username: string, - password: string, - countryCode: string, - platform: TuyaPlatform, - pollingInterval?: number - }, - defaults: { id: string, device_type: TuyaDeviceType }[], - scenes: boolean | string[] -} /** * HomebridgePlatform @@ -59,7 +40,7 @@ export class TuyaWebPlatform implements DynamicPlatformPlugin { constructor( public readonly log: Logger, - public readonly config: PlatformConfig & DeepPartial, + public readonly config: TuyaWebConfig, public readonly api: API, ) { this.log.debug('Finished initializing platform:', this.config.name); @@ -131,9 +112,10 @@ export class TuyaWebPlatform implements DynamicPlatformPlugin { } // Called from device classes - public registerPlatformAccessory(platformAccessory): void { - this.log.debug('Register Platform Accessory (%s)', platformAccessory.displayName); - this.api.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [platformAccessory]); + public registerPlatformAccessory(accessory: PlatformAccessory): void { + this.log.debug('Register Platform Accessory (%s)', accessory.displayName); + this.api.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [accessory]); + this.accessories.set(accessory.UUID, accessory); } private async refreshDeviceStates(devices?: TuyaDevice[]): Promise { @@ -218,29 +200,29 @@ export class TuyaWebPlatform implements DynamicPlatformPlugin { * @param devices * @private */ - private parseDefaultsForDevices(devices: TuyaDevice[]): Array { + private parseDefaultsForDevices(devices: TuyaDevice[]): Array { const defaults = this.config.defaults; if (!defaults) { return []; } - const parsedDefaults: Array = []; - for (const configuredDefault of defaults as Config['defaults']) { + const parsedDefaults: Array = []; + for (const configuredDefault of defaults as Partial[]) { const device = devices.find(device => device.id === configuredDefault.id); if (!device) { this.log.warn('Added default for id: "%s" which is not a valid device-id.', configuredDefault.id); continue; } - if (!TuyaDeviceTypes.includes(configuredDefault.device_type)) { + if (configuredDefault.device_type === undefined || !TuyaDeviceTypes.includes(configuredDefault.device_type)) { this.log.warn( 'Added defaults for id: "%s" - device-type "%s" is not a valid device-type.', device.id, configuredDefault.device_type, ); continue; } - parsedDefaults.push({...configuredDefault, device}); + parsedDefaults.push({...(configuredDefault as TuyaDeviceDefaults), device}); } return parsedDefaults; @@ -256,7 +238,7 @@ export class TuyaWebPlatform implements DynamicPlatformPlugin { return []; } - const scenes: {[key: string]: string} = devices.filter(d => d.dev_type === 'scene').reduce((devices, device) => { + const scenes: { [key: string]: string } = devices.filter(d => d.dev_type === 'scene').reduce((devices, device) => { devices[device.id] = device.name; return devices; }, {}); @@ -273,7 +255,7 @@ export class TuyaWebPlatform implements DynamicPlatformPlugin { continue; } - if(Object.values(scenes).includes(toWhitelistSceneId)) { + if (Object.values(scenes).includes(toWhitelistSceneId)) { whitelistedSceneIds.push(Object.keys(scenes).find(key => scenes[key] === toWhitelistSceneId)!); continue; }