diff --git a/config.json b/config.json index efbc711..981a7fa 100644 --- a/config.json +++ b/config.json @@ -14,7 +14,7 @@ { "platform": "HarmonyHubMatrix", "harmonyHubIp": "192.168.1.14", - "neeoHubIp": "192.168.1.32", + "neeoHubIp": "192.168.1.24", "Matrix": { "Hub": "Harmony", "DeviceName": "Gefen AV Switch", @@ -180,7 +180,7 @@ }, { "Hub": "Neeo", - "DeviceName": "Harman Kardon AV Receiver", + "DeviceName": "Harmon Kardon Receicer", "Input": "Game" }, { @@ -189,7 +189,7 @@ "Input": [] } ], - "VolumeDevice": "Harman Kardon AV Receiver", + "VolumeDevice": "Harmon Kardon Receicer", "ControlDevice": "Chromecast", "OutputDevice": "Vizio TV", "UseMatrix": "true" @@ -209,11 +209,11 @@ }, { "Hub": "Neeo", - "DeviceName": "Harman Kardon AV Receiver", + "DeviceName": "Harmon Kardon Receicer", "Input": "Game" } ], - "VolumeDevice": "Harman Kardon AV Receiver", + "VolumeDevice": "Harmon Kardon Receicer", "ControlDevice": "Microsoft Xbox One", "OutputDevice": "Vizio TV", "UseMatrix": "true" @@ -233,11 +233,11 @@ }, { "Hub": "Neeo", - "DeviceName": "Harman Kardon AV Receiver", + "DeviceName": "Harmon Kardon Receicer", "Input": "DVD" } ], - "VolumeDevice": "Harman Kardon AV Receiver", + "VolumeDevice": "Harmon Kardon Receicer", "ControlDevice": "Sony PS4", "OutputDevice": "LG TV", "UseMatrix": "true" @@ -257,11 +257,11 @@ }, { "Hub": "Neeo", - "DeviceName": "Harman Kardon AV Receiver", + "DeviceName": "Harmon Kardon Receicer", "Input": "Game" } ], - "VolumeDevice": "Harman Kardon AV Receiver", + "VolumeDevice": "Harmon Kardon Receicer", "ControlDevice": "Apple TV Gen 4", "OutputDevice": "Vizio TV", "UseMatrix": "true" diff --git a/package-lock.json b/package-lock.json index 92dac8c..c64da37 100644 --- a/package-lock.json +++ b/package-lock.json @@ -748,6 +748,14 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-12.0.7.tgz", "integrity": "sha512-1YKeT4JitGgE4SOzyB9eMwO0nGVNkNEsm9qlIt1Lqm/tG2QEiSMTD4kS3aO6L+w5SClLVxALmIBESK6Mk5wX0A==" }, + "@types/node-fetch": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.5.0.tgz", + "integrity": "sha512-TLFRywthBgL68auWj+ziWu+vnmmcHCDFC/sqCOQf1xTz4hRq8cu79z8CtHU9lncExGBsB8fXA4TiLDLt6xvMzw==", + "requires": { + "@types/node": "*" + } + }, "ajv": { "version": "6.10.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.0.tgz", @@ -1541,6 +1549,11 @@ "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.0.tgz", "integrity": "sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg==" }, + "node-fetch": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.0.tgz", + "integrity": "sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA==" + }, "node-persist": { "version": "0.0.8", "resolved": "https://registry.npmjs.org/node-persist/-/node-persist-0.0.8.tgz", diff --git a/package.json b/package.json index be44d75..cf139da 100644 --- a/package.json +++ b/package.json @@ -35,8 +35,10 @@ "@babel/preset-env": "^7.4.5", "@babel/preset-typescript": "^7.3.3", "@types/node": "^12.0.7", + "@types/node-fetch": "^2.5.0", "harmony-websocket": "^1.1.0", "homebridge": "^0.4.50", + "node-fetch": "^2.6.0", "request": "^2.88.0" } } diff --git a/src/Accessories/ControlUnit.ts b/src/Accessories/ControlUnit.ts index 66bf0bf..860f40e 100644 --- a/src/Accessories/ControlUnit.ts +++ b/src/Accessories/ControlUnit.ts @@ -2,7 +2,7 @@ import { Activity } from '../Models/Activity'; import { Matrix } from '../Models/Matrix'; import { IAccessory } from './IAccessory'; import callbackify from '../Util/Callbackify'; -import HarmonyDataProvider from '../DataProviders/HarmonyDataProvider'; +import HubDataProvider from '../DataProviders/HubDataProvider'; let Service: HAPNodeJS.Service; let Characteristic: HAPNodeJS.Characteristic; @@ -29,7 +29,7 @@ export enum RemoteKey { } export interface IControlUnitProps { - dataProvider: HarmonyDataProvider, + dataProvider: HubDataProvider, displayName: string, activities: Array, api: any, @@ -58,7 +58,7 @@ export class ControlUnit implements IAccessory { //Harmony fields private activities: Array = []; - private dataProvider: HarmonyDataProvider; + private dataProvider: HubDataProvider; public platformAccessory: any; diff --git a/src/DataProviders/HubDataProvider.ts b/src/DataProviders/HubDataProvider.ts index f1e9983..35bf308 100644 --- a/src/DataProviders/HubDataProvider.ts +++ b/src/DataProviders/HubDataProvider.ts @@ -5,6 +5,7 @@ import { IDevice } from './IDevice'; import { RemoteKey } from '../Accessories/ControlUnit'; import { sleep } from '../Util/Sleep'; import { hub } from ".."; +import fetch from 'node-fetch'; let Characteristic: HAPNodeJS.Characteristic; @@ -21,6 +22,12 @@ interface IHubDataProviderProps { matrix: Matrix } +interface INeeoMacro { + roomKey: string, + macroKey: string, + deviceKey: string, +} + class HubDataProvider { //Harmony Fields private harmonyHubAddress: string; @@ -45,15 +52,18 @@ class HubDataProvider { if (this.harmonyHubAddress) { try { - this.connectHarmony(this.harmonyHubAddress); + Promise.resolve(this.connectHarmony(this.harmonyHubAddress)); } catch (err) { this.log(`Error connecting to harmony hub at ${this.harmonyHubAddress}: ${err}`); } } if (this.neeoHubAddress) { - + try { + Promise.resolve(this.connectNeeo(this.neeoHubAddress)); + } catch (err) { + this.log(`Error connecting to neeo hub at ${this.neeoHubAddress}: ${err}`) + } } - } /** @@ -77,7 +87,7 @@ class HubDataProvider { //Build potential list of devices to turn off let devicesToTurnOff: Array = this.states[controlUnitName]!.currentActivity.deviceSetupItems .map((value: DeviceSetupItem): IDevice => { - return this.getDeviceFromName(value.deviceName); + return this.getDeviceFromName(value.deviceName, value.hubType); }); //Resolve device conflicts with other controlUnits @@ -106,7 +116,7 @@ class HubDataProvider { //Build potential list of devices to to turn on let devicesToTurnOn: Array = activity.deviceSetupItems.map((value: DeviceSetupItem): IDevice => { - return this.getDeviceFromName(value.deviceName); + return this.getDeviceFromName(value.deviceName, value.hubType); }); //Resolve device conflicts with other controlUnits @@ -125,7 +135,7 @@ class HubDataProvider { //Assign correct input await Promise.all( activity.deviceSetupItems.map(async (value: DeviceSetupItem) => { - let device: IDevice = this.getDeviceFromName(value.deviceName); + let device: IDevice = this.getDeviceFromName(value.deviceName, value.hubType); if (device && device.supportsCommand(`Input ${value.input}`)) { let command: string = device.getCommand(`Input ${value.input}`); @@ -142,7 +152,7 @@ class HubDataProvider { let inputCommandName: string = `In ${input.inputNumber}`; let outputCommandName: string = `Out ${output.outputLetter}`; - let matrixDevice: IDevice = this.getDeviceFromName(this.matrix.deviceName); + let matrixDevice: IDevice = this.getDeviceFromName(this.matrix.deviceName, this.matrix.hubType); //Route hdmi if (matrixDevice.supportsCommand(inputCommandName) && matrixDevice.supportsCommand(outputCommandName)) { @@ -154,7 +164,7 @@ class HubDataProvider { //Build potential list of devices to turn off if (lastActivity) { let devicesToTurnOff: Array = lastActivity.deviceSetupItems.map((value: DeviceSetupItem): IDevice => { - return this.getDeviceFromName(value.deviceName); + return this.getDeviceFromName(value.deviceName, value.hubType); }); //remove devices that will be used for next activity from list @@ -195,8 +205,11 @@ class HubDataProvider { */ public volumeUp = async (controlUnitName: string) => { let volumeUpCommand: string = "Volume Up" + let currentActivity: Activity = this.states[controlUnitName]!.currentActivity; + let deviceSetupItem: DeviceSetupItem = currentActivity.deviceSetupItems.filter(e => e.deviceName == currentActivity.volumeDeviceId)[0]; + if (this.states[controlUnitName]) { - let volumeDevice: IDevice = this.getDeviceFromName(this.states[controlUnitName]!.currentActivity.volumeDeviceId); + let volumeDevice: IDevice = this.getDeviceFromName(currentActivity.volumeDeviceId, deviceSetupItem.hubType); if (volumeDevice.supportsCommand(volumeUpCommand)) { volumeDevice.sendCommand(volumeDevice.getCommand(volumeUpCommand)); } @@ -208,8 +221,11 @@ class HubDataProvider { */ public volumeDown = async (controlUnitName: string) => { let volumeDownCommand: string = "Volume Down" + let currentActivity: Activity = this.states[controlUnitName]!.currentActivity; + let deviceSetupItem: DeviceSetupItem = currentActivity.deviceSetupItems.filter(e => e.deviceName == currentActivity.volumeDeviceId)[0]; + if (this.states[controlUnitName]) { - let volumeDevice: IDevice = this.getDeviceFromName(this.states[controlUnitName]!.currentActivity.volumeDeviceId); + let volumeDevice: IDevice = this.getDeviceFromName(currentActivity.volumeDeviceId, deviceSetupItem.hubType); if (volumeDevice.supportsCommand(volumeDownCommand)) { volumeDevice.sendCommand(volumeDevice.getCommand(volumeDownCommand)); } @@ -261,7 +277,10 @@ class HubDataProvider { if (this.states[controlUnitName]) { let commandName: string = ""; - let device: IDevice = this.getDeviceFromName(this.states[controlUnitName]!.currentActivity.controlDeviceId); + let currentActivity: Activity = this.states[controlUnitName]!.currentActivity; + let deviceSetupItem: DeviceSetupItem = currentActivity.deviceSetupItems.filter(e => e.deviceName == currentActivity.volumeDeviceId)[0]; + + let device: IDevice = this.getDeviceFromName(this.states[controlUnitName]!.currentActivity.controlDeviceId, deviceSetupItem.hubType); switch (key) { case RemoteKey.ARROW_UP: { commandName = "Direction Up"; @@ -311,8 +330,8 @@ class HubDataProvider { * Get the IDevice by name. * @param deviceName The device to retrieve. */ - private getDeviceFromName(deviceName: string): IDevice { - return this.devices[deviceName]; + private getDeviceFromName(deviceName: string, hubType: hub): IDevice { + return this.devices[`${hubType}-${deviceName}`]; } /** @@ -343,7 +362,7 @@ class HubDataProvider { deviceCommands.forEach((command: any) => { commands[command.label] = command.action; }); - self.devices[dev.label] = { + self.devices[`Harmony-${dev.label}`] = { id: dev.id, name: dev.label, commands: commands, @@ -405,6 +424,84 @@ class HubDataProvider { private connectNeeo = async (address: string): Promise => { + let baseUrl = `http://${address}:3000/v1/projects/home`; + + let response: any = await fetch(`${baseUrl}/rooms`); + let rooms: { [key: string]: any } = await response.json(); + + function regularizeString(str: string) { + str = str.toLowerCase(); + return str.replace(/(?:^|\s)\S/g, function (a) { return a.toUpperCase(); }); + } + + for (let key in rooms) { + let devices: { [key: string]: any } = rooms[key].devices; + for (let deviceKey in devices) { + let device = devices[deviceKey]; + + //shape commands from macros + let commands: { [item: string]: any } = {}; + for (let macroKey in device.macros) { + let macro: any = device.macros[macroKey]; + commands[regularizeString(macro.name)] = { + deviceKey: macro.deviceKey, + macroKey: macro.key, + roomKey: macro.roomKey, + }; + } + + this.devices[`Neeo-${deviceKey}`] = { + id: device.key, + name: device.name, + log: this.log, + hubType: "Neeo", + commands: commands, + on: false, + supportsCommand(commandName: string): boolean { + let command = this.commands[commandName]; + return (command) ? true : false; + }, + + getCommand(commandName: string): string { + return this.commands[commandName]; + }, + + async sendCommand(command: string): Promise { + if (this.supportsCommand(command)) { + let macro: INeeoMacro = this.commands[command]; + let url: string = `${baseUrl}/rooms/${macro.roomKey}/devices/${macro.deviceKey}/macros/${macro.macroKey}/trigger`; + this.log(`Sending command ${url}`); + fetch(`${baseUrl}/rooms/${macro.roomKey}/devices/${macro.deviceKey}/macros/${macro.macroKey}/trigger`); + } + }, + + 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; + } + } + } + } + } + + this.log(`Device list: ${JSON.stringify(this.devices)}`); } } diff --git a/src/DataProviders/IDevice.ts b/src/DataProviders/IDevice.ts index 40b1e2c..8595d7f 100644 --- a/src/DataProviders/IDevice.ts +++ b/src/DataProviders/IDevice.ts @@ -3,7 +3,7 @@ import { hub } from ".."; export interface IDevice { id: string, name: string, - harmony: any | undefined, + harmony?: any, log: any, hubType: hub supportsCommand(commandName: string): boolean, @@ -11,6 +11,6 @@ export interface IDevice { sendCommand(command: string): void, powerOn(): Promise, powerOff(): Promise, - commands: { [name: string]: string }; + commands: { [name: string]: any }; on: boolean; } diff --git a/src/Models/DeviceSetupItem.ts b/src/Models/DeviceSetupItem.ts index c500a1f..df2e979 100644 --- a/src/Models/DeviceSetupItem.ts +++ b/src/Models/DeviceSetupItem.ts @@ -12,11 +12,11 @@ export interface IDeviceSetupItemProps { export class DeviceSetupItem { private _deviceId: string = ""; private _input: string = ""; - private _hubName: hub; + private _hubType: hub; constructor(props: IDeviceSetupItemProps) { this._deviceId = props.deviceName; this._input = props.input; - this._hubName = props.hub; + this._hubType = props.hub; } public get deviceName() { @@ -27,7 +27,7 @@ export class DeviceSetupItem { return this._input; } - public get hubName() { - return this._hubName; + public get hubType() { + return this._hubType; } } \ No newline at end of file diff --git a/src/Models/Matrix.ts b/src/Models/Matrix.ts index 09d3c4e..e4de212 100644 --- a/src/Models/Matrix.ts +++ b/src/Models/Matrix.ts @@ -4,7 +4,7 @@ export interface IMatrixProps { inputs: Array, outputs: Array, deviceName: string, - hub: hub + hubType: hub } export interface Input { @@ -24,13 +24,13 @@ export class Matrix { private _inputs: Array = []; private _outputs: Array = []; private _deviceName: string; - private _hub: hub = "Harmony"; + private _hubType: hub = "Harmony"; constructor(props: IMatrixProps) { this._inputs = props.inputs; this._outputs = props.outputs; this._deviceName = props.deviceName; - this._hub = props.hub; + this._hubType = props.hubType; } public get inputs(): Array { @@ -44,4 +44,8 @@ export class Matrix { public get deviceName(): string { return this._deviceName; } + + public get hubType(): hub { + return this._hubType; + } } \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index 3cca555..e3be69f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,7 +7,7 @@ import HubDataProvider from './DataProviders/HubDataProvider'; let Accessory: any; let Homebridge: any; -export type hub = "Harmony" | "String"; +export type hub = "Harmony" | "Neeo"; /** * Main entry. @@ -102,7 +102,7 @@ class HarmonyMatrixPlatform { inputs: inputs, outputs: outputs, deviceName: matrixName, - hub: hub + hubType: hub }); //construct data provider