import { IActivity } from "../Models/Config/IActivity"; import { IDeviceSetupItem } from "../Models/Config/IDeviceSetupItem"; import { IInput, IMatrix, IOutput } from "../Models/Config/IMatrix"; import { RemoteKey } from '../Accessories/ControlUnit'; import { EventEmitter } from "events"; import { ICommand } from '../Models/IDevice'; import { IHub } from "../Models/Config/IHub"; import { IDeviceConfig } from "../Models/Config/IDeviceConfig"; import { HarmonyDevice } from "../Models/HarmonyDevice"; import { HarmonyHub } from "../Models/HarmonyHub"; let Characteristic: HAPNodeJS.Characteristic; const Harmony = require("harmony-websocket"); interface IActivityState { currentActivity: IActivity } interface IHarmonyDataProviderProps { hubs: Array; deviceConfigs: Array; log: any, matrix: IMatrix } class HarmonyDataProvider extends EventEmitter { private _log: any; 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(props: IHarmonyDataProviderProps) { super(); this._log = props.log; this._matrix = props.matrix; props.deviceConfigs.forEach((deviceConfig: IDeviceConfig) => { this._hubsByDevice[deviceConfig.Name] = deviceConfig.Hub; }); // this._deviceConfigs = props.deviceConfigs; this.connect(props.hubs); } // 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(`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(`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(`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(`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(`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); 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; } } export default HarmonyDataProvider;