Skip to content

Commit

Permalink
Improve handling of new accessories
Browse files Browse the repository at this point in the history
  • Loading branch information
milo526 committed Jul 29, 2020
1 parent 16526c9 commit 28df8ba
Show file tree
Hide file tree
Showing 7 changed files with 117 additions and 48 deletions.
31 changes: 21 additions & 10 deletions src/accessories/BaseAccessory.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -12,7 +20,7 @@ export type CharacteristicConstructor = WithUUID<{
type UpdateCallback<DeviceConfig extends TuyaDevice> = (data?: DeviceConfig['data'], callback?: CharacteristicGetCallback) => void

class Cache {
private state: Map<CharacteristicConstructor, {validUntil: number, value: Nullable<CharacteristicValue>}> = new Map();
private state: Map<CharacteristicConstructor, { validUntil: number, value: Nullable<CharacteristicValue> }> = new Map();
private _valid = false;

public get valid(): boolean {
Expand All @@ -34,7 +42,7 @@ class Cache {

public get(char: CharacteristicConstructor): Nullable<CharacteristicValue> {
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;
Expand Down Expand Up @@ -88,18 +96,21 @@ export abstract class BaseAccessory<DeviceConfig extends TuyaDevice = TuyaDevice
}
this.log.info(
'Existing Accessory found [Name: %s] [Tuya ID: %s] [HomeBridge ID: %s]',
homebridgeAccessory.displayName,
homebridgeAccessory.context.deviceId,
homebridgeAccessory.UUID);
this.homebridgeAccessory.displayName,
this.homebridgeAccessory.context.deviceId,
this.homebridgeAccessory.UUID);
this.homebridgeAccessory.displayName = this.deviceConfig.name;
} else {
this.log.info('Creating New Accessory %s', this.deviceConfig.id);
this.homebridgeAccessory = new this.platform.platformAccessory(
this.deviceConfig.name,
this.platform.generateUUID(this.deviceConfig.id),
categoryType);
this.homebridgeAccessory.context.deviceId = this.deviceConfig.id;
this.homebridgeAccessory.controller = this;
this.log.info('Created new Accessory [Name: %s] [Tuya ID: %s] [HomeBridge ID: %s]',
this.homebridgeAccessory.displayName,
this.homebridgeAccessory.context.deviceId,
this.homebridgeAccessory.UUID);
this.platform.registerPlatformAccessory(this.homebridgeAccessory);
}

Expand Down Expand Up @@ -164,8 +175,8 @@ export abstract class BaseAccessory<DeviceConfig extends TuyaDevice = TuyaDevice
}

private updateState(data: DeviceConfig['data']): void {
for(const [, callback] of this.updateCallbackList) {
if(callback !== null) {
for (const [, callback] of this.updateCallbackList) {
if (callback !== null) {
callback(data);
}
}
Expand Down
4 changes: 2 additions & 2 deletions src/accessories/FanAccessory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import {BaseAccessory} from './BaseAccessory';
import {TuyaDevice, TuyaDeviceState} from '../TuyaWebApi';
import {HomebridgeAccessory, TuyaWebPlatform} from '../platform';
import {Categories} from 'homebridge';
import {OnCharacteristic, OnCharacteristicData, RotationSpeedCharacteristic} from './characteristics';
import {ActiveCharacteristic, OnCharacteristicData, RotationSpeedCharacteristic} from './characteristics';

type FanAccessoryConfig = TuyaDevice & {
data: TuyaDeviceState & OnCharacteristicData
Expand All @@ -16,7 +16,7 @@ export class FanAccessory extends BaseAccessory<FanAccessoryConfig> {
deviceConfig: FanAccessoryConfig) {
super(platform, homebridgeAccessory, deviceConfig, Categories.FAN);

new OnCharacteristic(this as BaseAccessory);
new ActiveCharacteristic(this as BaseAccessory);
new RotationSpeedCharacteristic(this as BaseAccessory);
}
}
60 changes: 60 additions & 0 deletions src/accessories/characteristics/active.ts
Original file line number Diff line number Diff line change
@@ -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<TuyaDeviceState & ActiveCharacteristicData>

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<ActiveCharacteristicData>(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);
}
}
}
1 change: 1 addition & 0 deletions src/accessories/characteristics/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './active';
export * from './brightness';
export * from './colorTemperature';
export * from './hue';
Expand Down
18 changes: 18 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
@@ -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<TuyaDeviceDefaults>[],
scenes?: boolean | string[]
}

export type TuyaWebConfig = PlatformConfig & Config;
3 changes: 0 additions & 3 deletions src/helpers/DeepPartial.ts

This file was deleted.

48 changes: 15 additions & 33 deletions src/platform.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -19,23 +11,12 @@ import {
SceneAccessory,
SwitchAccessory,
} from './accessories';
import {DeepPartial} from './helpers/DeepPartial';
import {TuyaDeviceDefaults, TuyaWebConfig} from './config';

export type HomebridgeAccessory<DeviceConfig extends TuyaDevice> =
PlatformAccessory
& { controller?: BaseAccessory<DeviceConfig> }

type Config = {
options: {
username: string,
password: string,
countryCode: string,
platform: TuyaPlatform,
pollingInterval?: number
},
defaults: { id: string, device_type: TuyaDeviceType }[],
scenes: boolean | string[]
}

/**
* HomebridgePlatform
Expand All @@ -59,7 +40,7 @@ export class TuyaWebPlatform implements DynamicPlatformPlugin {

constructor(
public readonly log: Logger,
public readonly config: PlatformConfig & DeepPartial<Config>,
public readonly config: TuyaWebConfig,
public readonly api: API,
) {
this.log.debug('Finished initializing platform:', this.config.name);
Expand Down Expand Up @@ -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<void> {
Expand Down Expand Up @@ -218,29 +200,29 @@ export class TuyaWebPlatform implements DynamicPlatformPlugin {
* @param devices
* @private
*/
private parseDefaultsForDevices(devices: TuyaDevice[]): Array<Config['defaults'][number] & { device: TuyaDevice }> {
private parseDefaultsForDevices(devices: TuyaDevice[]): Array<TuyaDeviceDefaults & { device: TuyaDevice }> {
const defaults = this.config.defaults;

if (!defaults) {
return [];
}

const parsedDefaults: Array<Config['defaults'][number] & { device: TuyaDevice }> = [];
for (const configuredDefault of defaults as Config['defaults']) {
const parsedDefaults: Array<TuyaDeviceDefaults & { device: TuyaDevice }> = [];
for (const configuredDefault of defaults as Partial<TuyaDeviceDefaults>[]) {
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;
Expand All @@ -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;
}, {});
Expand All @@ -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;
}
Expand Down

0 comments on commit 28df8ba

Please sign in to comment.