From a7ae2595674589250d2f7ebdd9e57a1c5d0dac9e Mon Sep 17 00:00:00 2001 From: watsonb8 Date: Sun, 16 Jun 2019 11:19:32 -0400 Subject: [PATCH] Can successfully start an activity on power on. --- src/Accessories/ControlUnit.ts | 17 +- src/DataProviders/HarmonyDataProvider.ts | 274 ++++++++++++++++++++++- src/Models/Activity.ts | 4 +- src/Models/DeviceSetupItem.ts | 6 +- src/Models/Matrix.ts | 7 + src/index.ts | 18 +- 6 files changed, 313 insertions(+), 13 deletions(-) diff --git a/src/Accessories/ControlUnit.ts b/src/Accessories/ControlUnit.ts index 8a00cae..d61bd99 100644 --- a/src/Accessories/ControlUnit.ts +++ b/src/Accessories/ControlUnit.ts @@ -2,15 +2,16 @@ import { Activity } from '../Models/Activity'; import { Matrix } from '../Models/Matrix'; import { IAccessory } from './IAccessory'; import callbackify from '../Util/Callbackify'; +import HarmonyDataProvider from '../DataProviders/HarmonyDataProvider'; let Service: HAPNodeJS.Service; let Characteristic: HAPNodeJS.Characteristic; let Api: any; export interface IControlUnitProps { + dataProvider: HarmonyDataProvider, displayName: string, activities: Array, - matrix: Matrix, api: any, log: any, } @@ -34,7 +35,7 @@ export class ControlUnit implements IAccessory { //Harmony fields private activities: Array = []; - private matrix: Matrix; + private dataProvider: HarmonyDataProvider; /** * Constructor @@ -50,7 +51,8 @@ export class ControlUnit implements IAccessory { this.name = this.displayName; this.activities = props.activities; - this.matrix = props.matrix; + + this.dataProvider = props.dataProvider; //Configure services this.configureTvService(); @@ -100,12 +102,19 @@ export class ControlUnit implements IAccessory { //TODO private onSetAccessoryActive = async (value: any) => { this.log(`set active + ${value}`); + switch (value) { + case 0: this.dataProvider.powerOff(this.name); break; + case 1: this.dataProvider.powerOn(this.name, this.activities[0]); break; + } } //TODO private onGetAccessoryActive = async () => { this.log(`get active`) - return Characteristic.Active.Active; + + //@ts-ignore + return this.dataProvider.getIsActive ? Characteristic.Active.Active : Characteristic.Active.Inactive + // return Characteristic.Active.Active; } //TODO diff --git a/src/DataProviders/HarmonyDataProvider.ts b/src/DataProviders/HarmonyDataProvider.ts index 9947787..915dd5c 100644 --- a/src/DataProviders/HarmonyDataProvider.ts +++ b/src/DataProviders/HarmonyDataProvider.ts @@ -1,5 +1,275 @@ -class HarmonyDataProvider { +import { Activity } from "../Models/Activity"; +import { DeviceSetupItem } from "../Models/DeviceSetupItem"; +import { threadId } from "worker_threads"; +import { Input, Matrix, Output } from "../Models/Matrix"; +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; } -export default new HarmonyDataProvider(); \ No newline at end of file +interface IActivityState { + currentActivity: Activity +} + +interface IHarmonyDataProviderProps { + hubAddress: string, + log: any, + matrix: Matrix +} + +class HarmonyDataProvider { + private harmony: any; + 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) { + this.log = props.log; + this.hubAddress = props.hubAddress; + this.matrix = props.matrix; + + this.harmony = new Harmony(); + + //Listeners + this.harmony.on('open', () => { + this.log('Hub open'); + this.connected = true; + }); + this.harmony.on('close', () => { + this.log('Hub closed'); + this.connected = false; + }); + + this.connect(); + } + + public powerOn = async (controlUnitName: string, activity: Activity) => { + await this.startActivity(controlUnitName, activity); + } + + public powerOff = async (controlUnitName: string) => { + if (!this.states[controlUnitName]) { + return; + } + //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); + }); + + //Resolve device conflicts with other controlUnits + devicesToTurnOff = this.sanitizeDeviceList(devicesToTurnOff, controlUnitName); + + //Turn off devices + devicesToTurnOff.forEach((device: IDevice) => { + this.powerOffDevice(device); + }); + + this.states[controlUnitName] = undefined; + } + + public startActivity = async (controlUnitName: string, activity: Activity) => { + let lastActivity: Activity | undefined = undefined; + if (this.states[controlUnitName]) { + lastActivity = this.states[controlUnitName]!.currentActivity; + } + + // this.log(`asdf: ${JSON.stringify(this.devices)}`); + //Build potential list of devices to to turn on + let devicesToTurnOn: Array = activity.deviceSetupItems.map((value: DeviceSetupItem): IDevice => { + return this.getDeviceFromName(value.deviceName); + }); + + // this.log(`Devices to turn on: ${JSON.stringify(devicesToTurnOn)}`); + + //Resolve device conflicts with other controlUnits + devicesToTurnOn = this.sanitizeDeviceList(devicesToTurnOn, controlUnitName); + // this.log(`Sanatized devices to turn on: ${JSON.stringify(devicesToTurnOn)}`) + + //Turn on devices + await Promise.all(devicesToTurnOn.map(async (device: IDevice) => { + this.log(`Turning on device ${device.name}`) + await this.powerOnDevice(device); + })); + + //Assign correct input + await Promise.all( + activity.deviceSetupItems.map(async (value: DeviceSetupItem) => { + let device: IDevice = this.getDeviceFromName(value.deviceName); + + if (device.supportsCommand(`Input ${value.input}`)) { + let command: string = device.getCommand(`Input ${value.input}`); + await this.sendCommand(command); + } + }) + ); + + + if (activity.useMatrix) { + //get input and output + let input: Input = this.matrix.inputs.filter(e => e.inputDevice === activity.controlDeviceId)[0]; + let output: Output = this.matrix.outputs.filter(e => e.outputDevice === activity.outputDeviceId)[0]; + + let inputCommandName: string = `In ${input.inputNumber}`; + let outputCommandName: string = `Out ${output.outputLetter}`; + + // this.log(`Matrix input command: ${inputCommandName} Matrix output command: ${outputCommandName}`); + + let matrixDevice: IDevice = this.getDeviceFromName(this.matrix.deviceName); + + //Rout hdmi + if (matrixDevice.supportsCommand(inputCommandName) && matrixDevice.supportsCommand(outputCommandName)) { + await this.sendCommand(matrixDevice.getCommand(inputCommandName)); + await this.sendCommand(matrixDevice.getCommand(outputCommandName)); + } + } + + //Build potential list of devices to turn off + if (lastActivity) { + let devicesToTurnOff: Array = lastActivity.deviceSetupItems.map((value: DeviceSetupItem): IDevice => { + return this.getDeviceFromName(value.deviceName); + }); + + this.log(`Devices to turn off: ${JSON.stringify(devicesToTurnOff)}`); + + //Resolve device conflicts with other controlUnits + devicesToTurnOff = this.sanitizeDeviceList(devicesToTurnOff, controlUnitName); + + this.log(`Sanatized devices to turn off: ${JSON.stringify(devicesToTurnOff)}`); + + await Promise.all( + //Turn off devices + devicesToTurnOff.map(async (device: IDevice) => { + await this.powerOffDevice(device); + }) + ); + + } + + //Assign current activity + this.states[controlUnitName] = { currentActivity: activity }; + } + + public volumeUp = async (controlUnitName: string) => { + + } + + public volumeDown = async (controlUnitName: string) => { + + } + + public sendKeyPress = async (controlUnitName: string) => { + + } + + public getIsActive(controlUnitName: string): boolean { + return this.states[controlUnitName] ? true : false + } + + /** + * 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]; + } + } + })); + + } catch (err) { + self.log(`ERROR - error connecting to harmony: ${err}`); + } + } + }, 1000); + } + + private powerOffDevice = async (device: IDevice) => { + let powerOffCommand: string = "Power Off"; + if (device.supportsCommand(powerOffCommand)) { + await this.sendCommand(device.getCommand(powerOffCommand)); + } + } + + private powerOnDevice = async (device: IDevice) => { + let powerOnCommand: string = "Power On"; + if (device.supportsCommand(powerOnCommand)) { + await this.sendCommand(device.getCommand(powerOnCommand)); + } + } + + private getDeviceFromName(deviceName: string): IDevice { + return this.devices[deviceName]; + } + + 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.name === value.deviceName)) { + let deviceToRemove: IDevice = devicesToTurnOn.filter(i => i.name === value.deviceName)[0]; + delete devicesToTurnOn[devicesToTurnOn.indexOf(deviceToRemove)]; + } + }); + } + } + + return devicesToTurnOn; + } + + private sendCommand = async (command: string) => { + try { + let response = await this.harmony.sendCommand(JSON.stringify(command)); + this.log(`Sent command: ${JSON.stringify(command)} response: ${JSON.stringify(response)}`); + } 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/Activity.ts b/src/Models/Activity.ts index 9bf8d25..14d9d4b 100644 --- a/src/Models/Activity.ts +++ b/src/Models/Activity.ts @@ -48,7 +48,7 @@ export class Activity { /** * The device associated with output. */ - public outputDeviceId(): string { + public get outputDeviceId(): string { return this._outputDeviceId; }; @@ -59,7 +59,7 @@ export class Activity { return this._displayName; } - public get deviceSetupItem(): Array { + public get deviceSetupItems(): Array { return this._deviceSetupItems } diff --git a/src/Models/DeviceSetupItem.ts b/src/Models/DeviceSetupItem.ts index b9a311a..c763270 100644 --- a/src/Models/DeviceSetupItem.ts +++ b/src/Models/DeviceSetupItem.ts @@ -1,6 +1,6 @@ export interface IDeviceSetupItemProps { - deviceId: string, + deviceName: string, input: string } @@ -8,11 +8,11 @@ export class DeviceSetupItem { private _deviceId: string = ""; private _input: string = ""; constructor(props: IDeviceSetupItemProps) { - this._deviceId = props.deviceId; + this._deviceId = props.deviceName; this._input = props.input; } - public get deviceId() { + public get deviceName() { return this._deviceId; } diff --git a/src/Models/Matrix.ts b/src/Models/Matrix.ts index dafc2ac..15b5f92 100644 --- a/src/Models/Matrix.ts +++ b/src/Models/Matrix.ts @@ -1,6 +1,7 @@ export interface IMatrixProps { inputs: Array, outputs: Array, + deviceName: string, } export interface Input { @@ -16,10 +17,12 @@ export interface Output { export class Matrix { private _inputs: Array = []; private _outputs: Array = []; + private _deviceName: string; constructor(props: IMatrixProps) { this._inputs = props.inputs; this._outputs = props.outputs; + this._deviceName = props.deviceName; } public get inputs(): Array { @@ -29,4 +32,8 @@ export class Matrix { public get outputs(): Array { return this._outputs; } + + public get deviceName(): string { + return this._deviceName; + } } \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index efcb074..302332b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,6 +2,7 @@ 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"; export default function (homebridge: any) { homebridge.registerPlatform( @@ -29,9 +30,14 @@ class HarmonyMatrixPlatform { * @param callback */ accessories(callback: (accessories: Array) => void) { + //Parse ip + let hubIp: string = this.config["hubIp"]; + //Parse matrix let configInputs: any = this.config["Matrix"]["Inputs"]; let configOutputs: any = this.config["Matrix"]["Outputs"]; + let matrixName: string = this.config["Matrix"]["DeviceName"]; + let inputs: Array = []; let outputs: Array = []; @@ -62,6 +68,14 @@ class HarmonyMatrixPlatform { let matrix = new Matrix({ inputs: inputs, outputs: outputs, + deviceName: matrixName, + }); + + //construct data provider + let dataProvider = new HarmonyDataProvider({ + hubAddress: hubIp, + matrix: matrix, + log: this.log, }); //Parse control units @@ -78,7 +92,7 @@ class HarmonyMatrixPlatform { configDevices.forEach((configDevice: any) => { //Add device devices.push(new DeviceSetupItem({ - deviceId: configDevice["DeviceName"], + deviceName: configDevice["DeviceName"], input: configDevice["Input"] })); this.log(`INFO - Added device '${configDevice["DeviceName"]}' for activity '${configActivity["DisplayName"]}'`); @@ -98,11 +112,11 @@ class HarmonyMatrixPlatform { //Add control unit controlUnits.push(new ControlUnit({ + dataProvider: dataProvider, displayName: configControlUnit["DisplayName"], api: this.api, log: this.log, activities: activities, - matrix: matrix, })); this.log(`INFO - Added ControlUnit`); });