import { IActivity } from "../Models/Config/activity"; import { IDeviceSetupItem } from "../Models/Config/deviceSetupItem"; import { IInput, IMatrix, IOutput } from "../Models/Config/matrix"; import { RemoteKey } from "../Accessories/controlUnit"; import { EventEmitter } from "events"; import { IHub } from "../Models/Config/hub"; import { IDeviceConfig } from "../Models/Config/deviceConfig"; import { HarmonyDevice } from "../Models/harmonyDevice"; import { HarmonyHub } from "../Models/harmonyHub"; import { IConfig } from "../Models/Config"; import { inject, injectable } from "tsyringe"; import { Logger, Logging } from "homebridge"; const Harmony = require("harmony-websocket"); interface IActivityState { currentActivity: IActivity; } interface IHarmonyDataProviderProps { hubs: Array; deviceConfigs: Array; log: any; matrix: IMatrix; } @injectable() class HarmonyDataProvider extends EventEmitter { private _hubsByDevice: { [deviceName: string]: string } = {}; private _hubs: { [hubName: string]: HarmonyHub } = {}; // private _devicesByHub: { [hubName: string]: { [deviceName: string]: HarmonyDevice } } = {}; private _states: { [controlUnitName: string]: IActivityState | undefined; } = {}; private _matrix: IMatrix; constructor( @inject("IConfig") private _config: IConfig, @inject("log") private _log: Logging ) { super(); this._matrix = _config.Matrix; _config.Devices.forEach((deviceConfig: IDeviceConfig) => { this._hubsByDevice[deviceConfig.Name] = deviceConfig.Hub; }); // this._deviceConfigs = props.deviceConfigs; this.connect(_config.Hubs); this.emitInfo(); } // public get devicesByHub(): { [hubName: string]: { [deviceName: string]: HarmonyDevice } } { // return this._devicesByHub; // } public get hubs(): { [hubName: string]: HarmonyHub } { return this._hubs; } /** * Power on all devices in an activity. */ public powerOn = async (controlUnitName: string, activity: IActivity) => { //Only power on if not alread on let currentActivity = this._states[controlUnitName] ? this._states[controlUnitName]!.currentActivity : undefined; if (!currentActivity) { await this.startActivity(controlUnitName, activity); } }; /** * Power off all devices in an activity that aren't being used. */ 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.DeviceSetupList.map( (value: IDeviceSetupItem): HarmonyDevice => { return this.getDeviceFromName(value.DeviceName); } ); //Resolve device conflicts with other controlUnits devicesToTurnOff = this.sanitizeDeviceList( devicesToTurnOff, controlUnitName ); //Turn off devices devicesToTurnOff.forEach(async (device: HarmonyDevice) => { if (device) { await device.powerOff(); } }); this._states[controlUnitName] = undefined; }; /** * Start an activity */ public startActivity = async ( controlUnitName: string, activity: IActivity ) => { this._log.info( `Starting activity ${activity.DisplayName} for controlUnit: ${controlUnitName}` ); let lastActivity: IActivity | undefined = undefined; if (this._states[controlUnitName]) { lastActivity = this._states[controlUnitName]!.currentActivity; } //Build potential list of devices to to turn on let devicesToTurnOn: Array = activity.DeviceSetupList.map( (value: IDeviceSetupItem): HarmonyDevice => { return this.getDeviceFromName(value.DeviceName); } ); //Resolve device conflicts with other controlUnits devicesToTurnOn = this.sanitizeDeviceList(devicesToTurnOn, controlUnitName); //Turn on devices await Promise.all( devicesToTurnOn.map(async (device: HarmonyDevice) => { if (device && device.name) { if (!device.on) { this._log.info(`Turning on device ${device.name}`); await device.powerOn(); } } }) ); //Assign correct input await Promise.all( activity.DeviceSetupList.map(async (value: IDeviceSetupItem) => { let device: HarmonyDevice = this.getDeviceFromName(value.DeviceName); if (device && device.supportsCommand(`Input${value.Input}`)) { await device.sendCommand(`Input${value.Input}`); } }) ); if (activity.UseMatrix) { //get input and output let input: IInput = this._matrix.Inputs.filter( (e) => e.InputDevice === activity.ControlDevice )[0]; let output: IOutput = this._matrix.Outputs.filter( (e) => e.OutputDevice === activity.OutputDevice )[0]; let inputCommandName: string = `In ${input.InputNumber}`; let outputCommandName: string = `Out ${output.OutputLetter}`; let matrixDevice: HarmonyDevice = this.getDeviceFromName( this._matrix.DeviceName ); //Route hdmi if ( matrixDevice.supportsCommand(inputCommandName) && matrixDevice.supportsCommand(outputCommandName) ) { await matrixDevice.sendCommand(outputCommandName); await matrixDevice.sendCommand(inputCommandName); await matrixDevice.sendCommand(outputCommandName); await matrixDevice.sendCommand(inputCommandName); } } //Build potential list of devices to turn off if (lastActivity) { let devicesToTurnOff: Array = lastActivity.DeviceSetupList.map( (value: IDeviceSetupItem): HarmonyDevice => { return this.getDeviceFromName(value.DeviceName); } ); //remove devices that will be used for next activity from list //delete array[index] is stupid because it just nulls out the index. But now i have to deal with nulls devicesToTurnOff.forEach((device: HarmonyDevice, index: number) => { if ( device && device.name && activity.DeviceSetupList.some((e) => { return e && e.DeviceName === device.name; }) ) { delete devicesToTurnOff[index]; } }); //Resolve device conflicts with other controlUnits devicesToTurnOff = this.sanitizeDeviceList( devicesToTurnOff, controlUnitName ); this._log.info( `Sanatized devices to turn off: ${JSON.stringify( devicesToTurnOff.map((e) => (e ? e.name : "")) )}` ); await Promise.all( //Turn off devices devicesToTurnOff.map(async (device: HarmonyDevice) => { if (device) { if (device.on) { this._log.info(`Turning off device ${device.name}`); await device.powerOff(); } } }) ); } //Assign current activity this._states[controlUnitName] = { currentActivity: activity }; }; /** * Turn the volume up for the current running activity. */ public volumeUp = async (controlUnitName: string) => { let volumeUpCommand: string = "Volume Up"; if (this._states[controlUnitName]) { let volumeDevice: HarmonyDevice = this.getDeviceFromName( this._states[controlUnitName]!.currentActivity.VolumeDevice ); await volumeDevice.sendCommand(volumeUpCommand); } }; /** * Volume down for current running activity. */ public volumeDown = async (controlUnitName: string) => { let volumeDownCommand: string = "Volume Down"; if (this._states[controlUnitName]) { let volumeDevice: HarmonyDevice = this.getDeviceFromName( this._states[controlUnitName]!.currentActivity.VolumeDevice ); await volumeDevice.sendCommand(volumeDownCommand); } }; /** * Send key press for current activity. * * @param controlUnitName The name of the control unit to act on. * @param key The key to send. */ public sendKeyPress = async (controlUnitName: string, key: any) => { if (this._states[controlUnitName]) { let commandName: string = ""; let device: HarmonyDevice = this.getDeviceFromName( this._states[controlUnitName]!.currentActivity.ControlDevice ); switch (key) { case RemoteKey.ARROW_UP: { commandName = "Direction Up"; break; } case RemoteKey.ARROW_DOWN: { commandName = "Direction Down"; break; } case RemoteKey.ARROW_LEFT: { commandName = "Direction Left"; break; } case RemoteKey.ARROW_RIGHT: { commandName = "Direction Right"; break; } case RemoteKey.SELECT: { commandName = "Select"; break; } case RemoteKey.PLAY_PAUSE: { commandName = "Pause"; break; } case RemoteKey.INFORMATION: { commandName = "Menu"; break; } case RemoteKey.BACK: { commandName = "Back"; break; } case RemoteKey.EXIT: { commandName = "Back"; break; } } await device.sendCommand(commandName); } }; /** * Return if a control unit is active * @param controlUnitName */ public getIsActive(controlUnitName: string): IActivity | undefined { return this._states[controlUnitName] ? this._states[controlUnitName]!.currentActivity : undefined; } /** * Get the IDevice by name. * @param deviceName The device to retrieve. */ public getDeviceFromName(deviceName: string): HarmonyDevice { let device: HarmonyDevice | undefined; try { device = this._hubs[this._hubsByDevice[deviceName]].getDeviceByName(deviceName); } catch (err) { this._log.info(`Error retrieving device from hub: ${err}`); } return device!; } // /** // * Gets device button commands // * @param deviceCommandName The device command name // * @param deviceName The device name // */ // public getCommand(deviceCommandName: string, deviceName: string): ICommand | undefined { // const device: HarmonyDevice = this.getDeviceFromName(deviceName); // if (device && device.supportsCommand(deviceCommandName)) { // return device.getCommand(deviceCommandName); // } else { // return undefined; // } // } private connect = async (hubs: Array) => { let readyCount = 0; await Promise.all( hubs.map(async (hub: IHub): Promise => { const newHarmonyHub = new HarmonyHub(hub.Name, hub.Ip, this._log.info); this._hubs[hub.Name] = newHarmonyHub; newHarmonyHub.on("Ready", () => { readyCount++; if (readyCount === Object.keys(this._hubs).length) { this.emit("Ready"); } }); await newHarmonyHub.initialize(); }) ); }; /** * 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.DeviceSetupList.forEach( (value: IDeviceSetupItem) => { //there are devices to remove if (devicesToTurnOn.some((e) => e && e.name === value.DeviceName)) { let deviceToRemove: HarmonyDevice = devicesToTurnOn.filter( (i) => i.name === value.DeviceName )[0]; delete devicesToTurnOn[devicesToTurnOn.indexOf(deviceToRemove)]; } } ); } } return devicesToTurnOn; } private emitInfo(): void { //Emit devices if requested this._log.info("All hubs connected"); if (this._config.EmitDevicesOnStartup) { const hubs = this.hubs; Object.values(hubs).forEach((hub: HarmonyHub) => { const deviceDictionary = hub.devices; this._log.info(`${hub.hubName}`); Object.values(deviceDictionary).forEach((device: HarmonyDevice) => { this._log.info(` ${device.name} : ${device.id}`); Object.keys(device.commands).forEach((command: string) => { this._log.info(` ${command}`); }); }); }); } } } export default HarmonyDataProvider;