From 588205e507b1cc489e1053d19f30a1097e96b1f9 Mon Sep 17 00:00:00 2001 From: watsonb8 Date: Wed, 1 Jan 2020 22:31:25 -0500 Subject: [PATCH] Added device buttons --- src/Accessories/DeviceButton.ts | 143 +++++++++++++++++++++++ src/DataProviders/HarmonyDataProvider.ts | 48 +++++--- src/Models/DeviceButton.ts | 5 + src/Models/IConfig.ts | 2 + src/index.ts | 31 ++++- 5 files changed, 206 insertions(+), 23 deletions(-) create mode 100644 src/Accessories/DeviceButton.ts create mode 100644 src/Models/DeviceButton.ts diff --git a/src/Accessories/DeviceButton.ts b/src/Accessories/DeviceButton.ts new file mode 100644 index 0000000..17ad1b9 --- /dev/null +++ b/src/Accessories/DeviceButton.ts @@ -0,0 +1,143 @@ +import HarmonyDataProvider from "../DataProviders/HarmonyDataProvider"; +import { IDeviceButton } from "../Models/DeviceButton"; +import { IAccessory } from "./IAccessory"; +import { sleep } from "../Util/Sleep"; + +let Service: HAPNodeJS.Service; +let Characteristic: HAPNodeJS.Characteristic; + +export interface IDeviceButtonProps { + dataProvider: HarmonyDataProvider, + name: string, + deviceInfo: IDeviceButton, + api: any, + log: any, + homebridge: any, +} + +export class DeviceButton implements IAccessory { + private _api: any; + private _homebridge: any; + private _log: any = {}; + + //Service fields + private _switchService: HAPNodeJS.Service; + private _infoService: HAPNodeJS.Service; + + private _buttonInfo: IDeviceButton; + + private _dataProvider: HarmonyDataProvider; + + private _deviceCommand: string = ""; + + private _buttonState: boolean; + + + constructor(props: IDeviceButtonProps) { + //Assign class variables + this._log = props.log; + this._api = props.api; + Service = props.api.hap.Service; + Characteristic = props.api.hap.Characteristic; + this.name = props.name; + this._homebridge = props.homebridge; + + this._buttonInfo = props.deviceInfo; + + this._dataProvider = props.dataProvider; + + this._buttonState = false; + + this.platformAccessory = new this._homebridge.platformAccessory(this.name, this.generateUUID(), this._homebridge.hap.Accessory.Categories.SWITCH); + + //@ts-ignore + this._infoService = new Service.AccessoryInformation(); + this._infoService.setCharacteristic(Characteristic.Manufacturer, "The Watson Project") + this._infoService.setCharacteristic(Characteristic.Model, "Device Button") + this._infoService.setCharacteristic(Characteristic.SerialNumber, "123-456-789"); + + this._switchService = new Service.Switch( + this.name, + 'switchService' + ) + + this._switchService.getCharacteristic(Characteristic.On) + //@ts-ignore + .on("set", this.onSwitchSet) + .updateValue(this._buttonState) + .on("get", this.onSwitchGet); + } + + /** + * Required by homebridge. + */ + public name: string; + + public platformAccessory: any; + + /** + * Called by homebridge to gather services. + */ + public getServices = (): Array => { + return [this._infoService, this._switchService!]; + } + + /** + * Handler for switch set event + * @param callback The callback function to call when complete + */ + private onSwitchSet = async (activeState: boolean, callback: (error?: Error | null | undefined) => void) => { + if (!this._buttonInfo.IsStateful && activeState === this._buttonState) { + return callback(); + } + + //Get device command if we don't have it + if (!this._deviceCommand) { + this._deviceCommand = this._dataProvider.getCommand(this._buttonInfo.ButtonName, this._buttonInfo.DeviceName); + } + + //Execute command + if (this._deviceCommand) { + await this._dataProvider.sendCommand(this._deviceCommand); + + //change state if stateful + if (this._buttonInfo.IsStateful) { + this._buttonState != this._buttonState + } else { + this._switchService.getCharacteristic(Characteristic.On).updateValue(false); + return callback(new Error("Normal Response")); + } + + } + return callback(); + } + + /** + * Handler for switch get event + * @param callback The callback function to call when complete + */ + private onSwitchGet = (callback: (error: Error | null, value: boolean) => void) => { + //Only return state if button is stateful + if (this._buttonInfo.IsStateful) { + return callback(null, this._buttonState); + } else { + return callback(null, false) + } + + } + + /** + * Helper function to generate a UUID + */ + private generateUUID(): string { // Public Domain/MIT + var d = new Date().getTime(); + if (typeof performance !== 'undefined' && typeof performance.now === 'function') { + d += performance.now(); //use high-precision timer if available + } + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { + var r = (d + Math.random() * 16) % 16 | 0; + d = Math.floor(d / 16); + return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16); + }); + } +} \ No newline at end of file diff --git a/src/DataProviders/HarmonyDataProvider.ts b/src/DataProviders/HarmonyDataProvider.ts index 5075548..d6abb70 100644 --- a/src/DataProviders/HarmonyDataProvider.ts +++ b/src/DataProviders/HarmonyDataProvider.ts @@ -278,6 +278,37 @@ class HarmonyDataProvider { return this.states[controlUnitName] ? this.states[controlUnitName]!.currentActivity : undefined; } + /** + * Gets device button commands + * @param deviceCommandName The device command name + * @param deviceName The device name + */ + public getCommand(deviceCommandName: string, deviceName: string): string { + const device: IDevice = this.getDeviceFromName(deviceName); + if (device && device.supportsCommand(deviceCommandName)) { + return device.getCommand(deviceCommandName); + } else { + return ""; + } + } + + /** + * Send a command to the harmony hub. + * @param command The command to send. + */ + public sendCommand = async (command: string) => { + try { + //Execute command + let response = await this.harmony.sendCommand(JSON.stringify(command)); + + //Sleep + await sleep(800); + } catch (err) { + this.log(`ERROR - error sending command to harmony: ${err}`); + } + + } + /** * Connect to harmony and receive device info */ @@ -386,23 +417,6 @@ class HarmonyDataProvider { return devicesToTurnOn; } - - /** - * Send a command to the harmony hub. - * @param command The command to send. - */ - private sendCommand = async (command: string) => { - try { - //Execute command - let response = await this.harmony.sendCommand(JSON.stringify(command)); - - //Sleep - await sleep(800); - } catch (err) { - this.log(`ERROR - error sending command to harmony: ${err}`); - } - - } } export default HarmonyDataProvider; \ No newline at end of file diff --git a/src/Models/DeviceButton.ts b/src/Models/DeviceButton.ts new file mode 100644 index 0000000..6cece38 --- /dev/null +++ b/src/Models/DeviceButton.ts @@ -0,0 +1,5 @@ +export interface IDeviceButton { + DeviceName: string; + ButtonName: string; + IsStateful: boolean; +} \ No newline at end of file diff --git a/src/Models/IConfig.ts b/src/Models/IConfig.ts index 08e34d8..9267a89 100644 --- a/src/Models/IConfig.ts +++ b/src/Models/IConfig.ts @@ -1,5 +1,6 @@ import { IMatrix } from "./Matrix"; import { IActivity } from "./Activity"; +import { IDeviceButton } from "./DeviceButton"; export interface IControlUnit { DisplayName: string; @@ -10,4 +11,5 @@ export interface IConfig { hubIp: string; Matrix: IMatrix ControlUnits: Array + DeviceButtons: Array } \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index b92022b..9b78d9b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,9 +1,12 @@ import { ControlUnit } from "./Accessories/ControlUnit"; +import { DeviceButton } from './Accessories/DeviceButton'; import { IActivity } from "./Models/Activity"; import { IDeviceSetupItem } from "./Models/DeviceSetupItem"; import { IInput, IOutput, IMatrix } from "./Models/Matrix"; import HarmonyDataProvider from "./DataProviders/HarmonyDataProvider"; import { IConfig, IControlUnit } from "./Models/IConfig"; +import { IDeviceButton } from "./Models/DeviceButton"; +import { IAccessory } from "./Accessories/IAccessory"; let Accessory: any; let Homebridge: any; @@ -28,7 +31,7 @@ class HarmonyMatrixPlatform { config: IConfig; api: any; dataProvider: HarmonyDataProvider | null; - controlUnits: Array = []; + accessoryList: Array = []; constructor(log: any, config: any, api: any) { this.log = log; @@ -48,8 +51,10 @@ class HarmonyMatrixPlatform { this.log(`Publishing external accessories`); //This is required in order to have multiple tv remotes on one platform - this.controlUnits.forEach((accessory: ControlUnit) => { - this.api.publishExternalAccessories("HarmonyMatrixPlatform", [accessory.platformAccessory]); + this.accessoryList.forEach((accessory: IAccessory) => { + if (accessory instanceof ControlUnit) { + this.api.publishExternalAccessories("HarmonyMatrixPlatform", [accessory.platformAccessory]); + } }) } @@ -57,7 +62,7 @@ class HarmonyMatrixPlatform { * Called by homebridge to gather accessories. * @param callback */ - accessories(callback: (accessories: Array) => void) { + accessories(callback: (accessories: Array) => void) { //construct data provider this.dataProvider = new HarmonyDataProvider({ hubAddress: this.config.hubIp, @@ -65,8 +70,9 @@ class HarmonyMatrixPlatform { log: this.log }); + //Add control units this.config.ControlUnits.forEach((unit: IControlUnit) => { - this.controlUnits.push(new ControlUnit({ + this.accessoryList.push(new ControlUnit({ dataProvider: this.dataProvider!, displayName: unit.DisplayName, api: this.api, @@ -75,6 +81,19 @@ class HarmonyMatrixPlatform { homebridge: Homebridge, })); }); - callback(this.controlUnits); + + //Add device buttons + this.config.DeviceButtons.forEach((button: IDeviceButton) => { + this.accessoryList.push(new DeviceButton({ + dataProvider: this.dataProvider!, + name: button.ButtonName, + deviceInfo: button, + api: this.api, + log: this.log, + homebridge: Homebridge, + + })) + }); + callback(this.accessoryList); } } \ No newline at end of file