From 31c5c5b9e2a764ef7eadb4abc5a4c045baca9f43 Mon Sep 17 00:00:00 2001 From: watsonb8 Date: Mon, 29 Jul 2019 12:05:30 -0400 Subject: [PATCH] Converted existing code to use platform agnostic device definitions. --- config.json | 35 +- src/Accessories/ControlUnit.ts | 8 - ...monyDataProvider.ts => HubDataProvider.ts} | 315 +++++++++--------- src/DataProviders/IDevice.ts | 16 + src/Models/DeviceSetupItem.ts | 10 +- src/Models/Matrix.ts | 5 + src/index.ts | 19 +- 7 files changed, 237 insertions(+), 171 deletions(-) rename src/DataProviders/{HarmonyDataProvider.ts => HubDataProvider.ts} (71%) create mode 100644 src/DataProviders/IDevice.ts diff --git a/config.json b/config.json index c5b6c4b..efbc711 100644 --- a/config.json +++ b/config.json @@ -13,8 +13,10 @@ }, { "platform": "HarmonyHubMatrix", - "hubIp": "192.168.1.14", + "harmonyHubIp": "192.168.1.14", + "neeoHubIp": "192.168.1.32", "Matrix": { + "Hub": "Harmony", "DeviceName": "Gefen AV Switch", "Inputs": [ { @@ -57,14 +59,17 @@ "DisplayName": "Chromecast", "DeviceSetupList": [ { + "Hub": "Harmony", "DeviceName": "LG TV", "Input": "HDMI1" }, { + "Hub": "Harmony", "DeviceName": "JVC AV Receiver", "Input": "DVD" }, { + "Hub": "Harmony", "DeviceName": "Chromecast", "Input": [] } @@ -78,14 +83,17 @@ "DisplayName": "Play Xbox One", "DeviceSetupList": [ { + "Hub": "Harmony", "DeviceName": "Microsoft Xbox One", "Input": [] }, { + "Hub": "Harmony", "DeviceName": "LG TV", "Input": "HDMI1" }, { + "Hub": "Harmony", "DeviceName": "JVC AV Receiver", "Input": "DVD" } @@ -99,14 +107,17 @@ "DisplayName": "Play PS4", "DeviceSetupList": [ { + "Hub": "Harmony", "DeviceName": "Sony PS4", "Input": [] }, { + "Hub": "Harmony", "DeviceName": "LG TV", "Input": "HDMI1" }, { + "Hub": "Harmony", "DeviceName": "JVC AV Receiver", "Input": "DVD" } @@ -120,14 +131,17 @@ "DisplayName": "Apple TV", "DeviceSetupList": [ { + "Hub": "Harmony", "DeviceName": "Apple TV Gen 4", "Input": [] }, { + "Hub": "Harmony", "DeviceName": "LG TV", "Input": "HDMI1" }, { + "Hub": "Harmony", "DeviceName": "JVC AV Receiver", "Input": "DVD" } @@ -141,6 +155,7 @@ "DisplayName": "Listen to Music", "DeviceSetupList": [ { + "Hub": "Harmony", "DeviceName": "JVC AV Receiver", "Input": "DVD" } @@ -159,14 +174,17 @@ "DisplayName": "Chromecast", "DeviceSetupList": [ { + "Hub": "Neeo", "DeviceName": "Vizio TV", "Input": "HDMI1" }, { + "Hub": "Neeo", "DeviceName": "Harman Kardon AV Receiver", "Input": "Game" }, { + "Hub": "Harmony", "DeviceName": "Chromecast", "Input": [] } @@ -180,14 +198,17 @@ "DisplayName": "Play Xbox One", "DeviceSetupList": [ { + "Hub": "Harmony", "DeviceName": "Microsoft Xbox One", "Input": [] }, { + "Hub": "Neeo", "DeviceName": "Vizio TV", "Input": "HDMI1" }, { + "Hub": "Neeo", "DeviceName": "Harman Kardon AV Receiver", "Input": "Game" } @@ -201,14 +222,17 @@ "DisplayName": "Play PS4", "DeviceSetupList": [ { + "Hub": "Harmony", "DeviceName": "Sony PS4", "Input": [] }, { + "Hub": "Neeo", "DeviceName": "Vizio TV", "Input": "HDMI1" }, { + "Hub": "Neeo", "DeviceName": "Harman Kardon AV Receiver", "Input": "DVD" } @@ -222,14 +246,17 @@ "DisplayName": "Apple TV", "DeviceSetupList": [ { + "Hub": "Harmony", "DeviceName": "Apple TV Gen 4", "Input": [] }, { + "Hub": "Neeo", "DeviceName": "Vizio TV", "Input": "HDMI1" }, { + "Hub": "Neeo", "DeviceName": "Harman Kardon AV Receiver", "Input": "Game" } @@ -248,10 +275,12 @@ "DisplayName": "Chromecast", "DeviceSetupList": [ { + "Hub": "Harmony", "DeviceName": "Chromecast", "Input": [] }, { + "Hub": "Harmony", "DeviceName": "Westinghouse TV", "Input": "HDMI1" } @@ -265,10 +294,12 @@ "DisplayName": "Watch Apple Tv", "DeviceSetupList": [ { + "Hub": "Harmony", "DeviceName": "Apple TV Gen 4", "Input": [] }, { + "Hub": "Harmony", "DeviceName": "Westinghouse TV", "Input": "HDMI1" } @@ -282,10 +313,12 @@ "DisplayName": "Play Xbox One", "DeviceSetupList": [ { + "Hub": "Harmony", "DeviceName": "Westinghouse TV", "Input": "HDMI1" }, { + "Hub": "Harmony", "DeviceName": "Microsoft Xbox One", "Input": [] } diff --git a/src/Accessories/ControlUnit.ts b/src/Accessories/ControlUnit.ts index 3e40c47..66bf0bf 100644 --- a/src/Accessories/ControlUnit.ts +++ b/src/Accessories/ControlUnit.ts @@ -173,14 +173,6 @@ export class ControlUnit implements IAccessory { */ private onSetRemoteKey = async (key: any) => { if (this.isExternal) { - //Set the active identifier with every key press - // let currentActivity: Activity = this.dataProvider.getIsActive(this.name)!; - // let identifier: number = 0; - // if (currentActivity) { - // identifier = this.activities.findIndex(e => e.displayName === currentActivity.displayName); - // } - // this.televisionService!.setCharacteristic(Characteristic.ActiveIdentifier, identifier); - this.dataProvider.sendKeyPress(this.name, key); } } diff --git a/src/DataProviders/HarmonyDataProvider.ts b/src/DataProviders/HubDataProvider.ts similarity index 71% rename from src/DataProviders/HarmonyDataProvider.ts rename to src/DataProviders/HubDataProvider.ts index 40d8684..f1e9983 100644 --- a/src/DataProviders/HarmonyDataProvider.ts +++ b/src/DataProviders/HubDataProvider.ts @@ -1,58 +1,59 @@ import { Activity } from "../Models/Activity"; import { DeviceSetupItem } from "../Models/DeviceSetupItem"; import { Input, Matrix, Output } from "../Models/Matrix"; +import { IDevice } from './IDevice'; import { RemoteKey } from '../Accessories/ControlUnit'; import { sleep } from '../Util/Sleep'; +import { hub } from ".."; let Characteristic: HAPNodeJS.Characteristic; const Harmony = require("harmony-websocket"); -interface IDevice { - id: string, - name: string, - supportsCommand(commandName: string): boolean, - getCommand(commandName: string): string, - commands: { [name: string]: string }; - on: boolean; -} interface IActivityState { currentActivity: Activity } -interface IHarmonyDataProviderProps { - hubAddress: string, +interface IHubDataProviderProps { + harmonyHubAddress: string, + neeoHubAddress: string, log: any, matrix: Matrix } -class HarmonyDataProvider { - private harmony: any; +class HubDataProvider { + //Harmony Fields + private harmonyHubAddress: string; + private harmonyConnected: boolean = false; + + //Neeo Fields + private neeoHubAddress: string; + + //Common Fields private log: any; - private hubAddress: string = ""; - private connected: boolean = false; private devices: { [name: string]: IDevice; } = {}; private states: { [controlUnitName: string]: (IActivityState | undefined) } = {}; private matrix: Matrix; - constructor(props: IHarmonyDataProviderProps) { + constructor(props: IHubDataProviderProps) { this.log = props.log; - this.hubAddress = props.hubAddress; + this.harmonyHubAddress = props.harmonyHubAddress; + this.neeoHubAddress = props.neeoHubAddress; this.matrix = props.matrix; - this.harmony = new Harmony(); + if (this.harmonyHubAddress) { + try { + this.connectHarmony(this.harmonyHubAddress); + } catch (err) { + this.log(`Error connecting to harmony hub at ${this.harmonyHubAddress}: ${err}`); + } + } + if (this.neeoHubAddress) { - //Listeners - this.harmony.on('open', () => { - this.connected = true; - }); - this.harmony.on('close', () => { - this.connected = false; - }); + } - this.connect(); } /** @@ -82,10 +83,13 @@ class HarmonyDataProvider { //Resolve device conflicts with other controlUnits devicesToTurnOff = this.sanitizeDeviceList(devicesToTurnOff, controlUnitName); - //Turn off devices - devicesToTurnOff.forEach((device: IDevice) => { - this.powerOffDevice(device); - }); + await Promise.all( + //Turn off devices + devicesToTurnOff.map(async (device: IDevice) => { + await device.powerOff(); + }) + ); + this.states[controlUnitName] = undefined; } @@ -113,7 +117,7 @@ class HarmonyDataProvider { if (device && device.name && this.devices[device.name]) { if (!device.on) { this.log(`Turning on device ${device.name}`) - await this.powerOnDevice(device); + await device.powerOn(); } } })); @@ -125,7 +129,7 @@ class HarmonyDataProvider { if (device && device.supportsCommand(`Input ${value.input}`)) { let command: string = device.getCommand(`Input ${value.input}`); - await this.sendCommand(command); + await device.sendCommand(command); } }) ); @@ -142,8 +146,8 @@ class HarmonyDataProvider { //Route hdmi if (matrixDevice.supportsCommand(inputCommandName) && matrixDevice.supportsCommand(outputCommandName)) { - await this.sendCommand(matrixDevice.getCommand(inputCommandName)); - await this.sendCommand(matrixDevice.getCommand(outputCommandName)); + await matrixDevice.sendCommand(matrixDevice.getCommand(inputCommandName)); + await matrixDevice.sendCommand(matrixDevice.getCommand(outputCommandName)); } } @@ -174,7 +178,7 @@ class HarmonyDataProvider { if (device) { if (device.on) { this.log(`Turning off device ${device.name}`) - await this.powerOffDevice(device); + await device.powerOff(); } } }) @@ -194,7 +198,7 @@ class HarmonyDataProvider { if (this.states[controlUnitName]) { let volumeDevice: IDevice = this.getDeviceFromName(this.states[controlUnitName]!.currentActivity.volumeDeviceId); if (volumeDevice.supportsCommand(volumeUpCommand)) { - this.sendCommand(volumeDevice.getCommand(volumeUpCommand)); + volumeDevice.sendCommand(volumeDevice.getCommand(volumeUpCommand)); } } } @@ -207,11 +211,46 @@ class HarmonyDataProvider { if (this.states[controlUnitName]) { let volumeDevice: IDevice = this.getDeviceFromName(this.states[controlUnitName]!.currentActivity.volumeDeviceId); if (volumeDevice.supportsCommand(volumeDownCommand)) { - this.sendCommand(volumeDevice.getCommand(volumeDownCommand)); + volumeDevice.sendCommand(volumeDevice.getCommand(volumeDownCommand)); } } } + /** + * Return if a control unit is active + * @param controlUnitName + */ + public getIsActive(controlUnitName: string): Activity | undefined { + return this.states[controlUnitName] ? this.states[controlUnitName]!.currentActivity : undefined; + } + + /** + * Helper function to make sure no control unit depends on device list. + * @param devicesToTurnOn The list of devices to modify. + * @param controlUnitName The name of the control unit in question. + */ + private sanitizeDeviceList(devicesToTurnOn: Array, controlUnitName: string): Array { + for (let controlUnitKey in this.states) { + //Skip self + if (controlUnitKey === controlUnitName) { + continue; + } + let currentOtherState: IActivityState = this.states[controlUnitKey]!; + + if (currentOtherState) { + currentOtherState.currentActivity.deviceSetupItems.forEach((value: DeviceSetupItem) => { + //there are devices to remove + if (devicesToTurnOn.some(e => e && e.name === value.deviceName)) { + let deviceToRemove: IDevice = devicesToTurnOn.filter(i => i.name === value.deviceName)[0]; + delete devicesToTurnOn[devicesToTurnOn.indexOf(deviceToRemove)]; + } + }); + } + } + + return devicesToTurnOn; + } + /** * Send key press for current activity. * @@ -263,93 +302,11 @@ class HarmonyDataProvider { } if (device && device.supportsCommand(commandName)) { - this.sendCommand(device.getCommand(commandName)); + device.sendCommand(device.getCommand(commandName)); } } } - /** - * Return if a control unit is active - * @param controlUnitName - */ - public getIsActive(controlUnitName: string): Activity | undefined { - return this.states[controlUnitName] ? this.states[controlUnitName]!.currentActivity : undefined; - } - - /** - * Connect to harmony and receive device info - */ - private connect = async () => { - await this.harmony.connect(this.hubAddress); - let self = this; - - setTimeout(async function () { - if (self.connected) { - let devices: any = await self.harmony.getDevices(); - try { - await Promise.all( - //Add each to dictionary - devices.map(async (dev: any) => { - //get commands - let commands: { [name: string]: string } = {}; - let deviceCommands: any = await self.harmony.getDeviceCommands(dev.id); - deviceCommands.forEach((command: any) => { - commands[command.label] = command.action; - }); - self.devices[dev.label] = { - id: dev.id, - name: dev.label, - commands: commands, - on: false, - //Define device methods - supportsCommand(commandName: string): boolean { - let command = commands[commandName]; - return (command) ? true : false; - }, - getCommand(commandName: string): string { - return commands[commandName]; - } - } - })); - self.log(`Harmony data provider ready`); - - } catch (err) { - self.log(`ERROR - error connecting to harmony: ${err}`); - } - } - }, 1000); - } - - /** - * Power off a device (Power toggle if no power off). - */ - private powerOffDevice = async (device: IDevice) => { - let powerOffCommand: string = "Power Off"; - let powerToggleCommand: string = "Power Toggle"; - if (device && device.supportsCommand(powerOffCommand)) { - await this.sendCommand(device.getCommand(powerOffCommand)); - device.on = false; - } else if (device && device.supportsCommand(powerToggleCommand)) { - await this.sendCommand(device.getCommand(powerToggleCommand)); - device.on = false; - } - } - - /** - * Power on a device (Power toggle if no power on). - */ - private powerOnDevice = async (device: IDevice) => { - let powerOnCommand: string = "Power On"; - let powerToggleCommand: string = "Power Toggle"; - if (device && device.supportsCommand(powerOnCommand)) { - await this.sendCommand(device.getCommand(powerOnCommand)); - device.on = true; - } else if (device && device.supportsCommand(powerToggleCommand)) { - await this.sendCommand(device.getCommand(powerToggleCommand)); - device.on = true; - } - } - /** * Get the IDevice by name. * @param deviceName The device to retrieve. @@ -359,48 +316,96 @@ class HarmonyDataProvider { } /** - * Helper function to make sure no control unit depends on device list. - * @param devicesToTurnOn The list of devices to modify. - * @param controlUnitName The name of the control unit in question. + * Connect to harmony hub and receive device info. */ - private sanitizeDeviceList(devicesToTurnOn: Array, controlUnitName: string): Array { - for (let controlUnitKey in this.states) { - //Skip self - if (controlUnitKey === controlUnitName) { - continue; - } - let currentOtherState: IActivityState = this.states[controlUnitKey]!; + private connectHarmony = async (address: string): Promise => { + let harmony: any = new Harmony(); + //Listeners + harmony.on('open', () => { + this.harmonyConnected = true; + }); + harmony.on('close', () => { + this.harmonyConnected = false; + }); + await harmony.connect(address); + let self = this; - if (currentOtherState) { - currentOtherState.currentActivity.deviceSetupItems.forEach((value: DeviceSetupItem) => { - //there are devices to remove - if (devicesToTurnOn.some(e => e && e.name === value.deviceName)) { - let deviceToRemove: IDevice = devicesToTurnOn.filter(i => i.name === value.deviceName)[0]; - delete devicesToTurnOn[devicesToTurnOn.indexOf(deviceToRemove)]; - } - }); - } - } + setTimeout(async function () { + if (self.harmonyConnected) { + let devices: any = await harmony.getDevices(); + try { + await Promise.all( + //Add each to dictionary + devices.map(async (dev: any) => { + //get commands + let commands: { [name: string]: string } = {}; + let deviceCommands: any = await harmony.getDeviceCommands(dev.id); + deviceCommands.forEach((command: any) => { + commands[command.label] = command.action; + }); + self.devices[dev.label] = { + id: dev.id, + name: dev.label, + commands: commands, + harmony: harmony, + hubType: "Harmony", + log: self.log, + on: false, + //Define device methods + supportsCommand(commandName: string): boolean { + let command = commands[commandName]; + return (command) ? true : false; + }, + getCommand(commandName: string): string { + return commands[commandName]; + }, + async powerOn(): Promise { + let powerOnCommand: string = "Power On"; + let powerToggleCommand: string = "Power Toggle"; + if (this.supportsCommand(powerOnCommand)) { + await this.sendCommand(this.getCommand(powerOnCommand)); + this.on = true; + } else if (this.supportsCommand(powerToggleCommand)) { + await this.sendCommand(this.getCommand(powerToggleCommand)); + this.on = true; + } + }, + async powerOff(): Promise { + let powerOffCommand: string = "Power Off"; + let powerToggleCommand: string = "Power Toggle"; + if (this.supportsCommand(powerOffCommand)) { + await this.sendCommand(this.getCommand(powerOffCommand)); + this.on = false; + } else if (this.supportsCommand(powerToggleCommand)) { + await this.sendCommand(this.getCommand(powerToggleCommand)); + this.on = false; + } + }, + async sendCommand(command: string): Promise { + try { + //Execute command + let response = await this.harmony.sendCommand(JSON.stringify(command)); - return devicesToTurnOn; + //Sleep + await sleep(800); + } catch (err) { + this.log(`ERROR - error sending command to harmony: ${err}`); + } + } + } + })); + self.log(`Harmony data provider ready`); + + } catch (err) { + self.log(`ERROR - error connecting to harmony: ${err}`); + } + } + }, 1000); } - /** - * 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}`); - } + private connectNeeo = async (address: string): Promise => { } } -export default HarmonyDataProvider; \ No newline at end of file +export default HubDataProvider; \ No newline at end of file diff --git a/src/DataProviders/IDevice.ts b/src/DataProviders/IDevice.ts new file mode 100644 index 0000000..40b1e2c --- /dev/null +++ b/src/DataProviders/IDevice.ts @@ -0,0 +1,16 @@ +import { hub } from ".."; + +export interface IDevice { + id: string, + name: string, + harmony: any | undefined, + log: any, + hubType: hub + supportsCommand(commandName: string): boolean, + getCommand(commandName: string): string, + sendCommand(command: string): void, + powerOn(): Promise, + powerOff(): Promise, + commands: { [name: string]: string }; + on: boolean; +} diff --git a/src/Models/DeviceSetupItem.ts b/src/Models/DeviceSetupItem.ts index e9e4f4e..c500a1f 100644 --- a/src/Models/DeviceSetupItem.ts +++ b/src/Models/DeviceSetupItem.ts @@ -1,7 +1,9 @@ +import { hub } from ".."; export interface IDeviceSetupItemProps { deviceName: string, - input: string + input: string, + hub: hub } /** @@ -10,9 +12,11 @@ export interface IDeviceSetupItemProps { export class DeviceSetupItem { private _deviceId: string = ""; private _input: string = ""; + private _hubName: hub; constructor(props: IDeviceSetupItemProps) { this._deviceId = props.deviceName; this._input = props.input; + this._hubName = props.hub; } public get deviceName() { @@ -22,4 +26,8 @@ export class DeviceSetupItem { public get input() { return this._input; } + + public get hubName() { + return this._hubName; + } } \ No newline at end of file diff --git a/src/Models/Matrix.ts b/src/Models/Matrix.ts index d067976..09d3c4e 100644 --- a/src/Models/Matrix.ts +++ b/src/Models/Matrix.ts @@ -1,7 +1,10 @@ +import { hub } from ".."; + export interface IMatrixProps { inputs: Array, outputs: Array, deviceName: string, + hub: hub } export interface Input { @@ -21,11 +24,13 @@ export class Matrix { private _inputs: Array = []; private _outputs: Array = []; private _deviceName: string; + private _hub: hub = "Harmony"; constructor(props: IMatrixProps) { this._inputs = props.inputs; this._outputs = props.outputs; this._deviceName = props.deviceName; + this._hub = props.hub; } public get inputs(): Array { diff --git a/src/index.ts b/src/index.ts index 313dd6a..3cca555 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,11 +2,13 @@ import { ControlUnit } from "./Accessories/ControlUnit"; import { Activity } from "./Models/Activity"; import { DeviceSetupItem } from "./Models/DeviceSetupItem"; import { Input, Output, Matrix } from "./Models/Matrix"; -import HarmonyDataProvider from "./DataProviders/HarmonyDataProvider"; +import HubDataProvider from './DataProviders/HubDataProvider'; let Accessory: any; let Homebridge: any; +export type hub = "Harmony" | "String"; + /** * Main entry. * @param homebridge @@ -27,7 +29,7 @@ class HarmonyMatrixPlatform { config: any = {}; api: any; externalAccessories: Array = []; - dataProvider: HarmonyDataProvider | null; + dataProvider: HubDataProvider | null; constructor(log: any, config: any, api: any) { this.log = log; @@ -60,12 +62,14 @@ class HarmonyMatrixPlatform { */ accessories(callback: (accessories: Array) => void) { //Parse ip - let hubIp: string = this.config["hubIp"]; + let harmonyIp: string = this.config["harmonyHubIp"]; + let neeoIp: string = this.config["neeoHubIp"]; //Parse matrix let configInputs: any = this.config["Matrix"]["Inputs"]; let configOutputs: any = this.config["Matrix"]["Outputs"]; let matrixName: string = this.config["Matrix"]["DeviceName"]; + let hub: hub = this.config["Matrix"]["Hub"]; let inputs: Array = []; let outputs: Array = []; @@ -98,11 +102,13 @@ class HarmonyMatrixPlatform { inputs: inputs, outputs: outputs, deviceName: matrixName, + hub: hub }); //construct data provider - this.dataProvider = new HarmonyDataProvider({ - hubAddress: hubIp, + this.dataProvider = new HubDataProvider({ + harmonyHubAddress: harmonyIp, + neeoHubAddress: neeoIp, matrix: matrix, log: this.log }); @@ -122,7 +128,8 @@ class HarmonyMatrixPlatform { //Add device devices.push(new DeviceSetupItem({ deviceName: configDevice["DeviceName"], - input: configDevice["Input"] + input: configDevice["Input"], + hub: configDevice["Hub"] })); this.log(`INFO - Added device '${configDevice["DeviceName"]}' for activity '${configActivity["DisplayName"]}'`); });