import { IActivity } from "../Models/Activity"; import { IDeviceSetupItem } from "../Models/DeviceSetupItem"; import { IInput, IMatrix, IOutput } from "../Models/Matrix"; import { RemoteKey } from '../Accessories/ControlUnit'; import { sleep } from '../Util/Sleep'; 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: IActivity } interface IHarmonyDataProviderProps { hubAddress: string, log: any, matrix: IMatrix } 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: IMatrix; 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.connected = true; }); this.harmony.on('close', () => { this.connected = false; }); this.connect(); } /** * 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): 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; } /** * 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): IDevice => { 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: IDevice) => { if (device && device.name && this.devices[device.name]) { if (!device.on) { this.log(`Turning on device ${device.name}`) await this.powerOnDevice(device); } } })); //Assign correct input await Promise.all( activity.DeviceSetupList.map(async (value: IDeviceSetupItem) => { let device: IDevice = this.getDeviceFromName(value.DeviceName); if (device && 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: 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: IDevice = this.getDeviceFromName(this.matrix.DeviceName); //Route hdmi if (matrixDevice.supportsCommand(inputCommandName) && matrixDevice.supportsCommand(outputCommandName)) { await this.sendCommand(matrixDevice.getCommand(outputCommandName)); await this.sendCommand(matrixDevice.getCommand(inputCommandName)); await this.sendCommand(matrixDevice.getCommand(outputCommandName)); await this.sendCommand(matrixDevice.getCommand(inputCommandName)); } } //Build potential list of devices to turn off if (lastActivity) { let devicesToTurnOff: Array = lastActivity.DeviceSetupList.map((value: IDeviceSetupItem): IDevice => { 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: IDevice, 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: IDevice) => { if (device) { if (device.on) { this.log(`Turning off device ${device.name}`) await this.powerOffDevice(device); } } }) ); } //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: IDevice = this.getDeviceFromName(this.states[controlUnitName]!.currentActivity.VolumeDevice); if (volumeDevice.supportsCommand(volumeUpCommand)) { this.sendCommand(volumeDevice.getCommand(volumeUpCommand)); } } } /** * Volume down for current running activity. */ public volumeDown = async (controlUnitName: string) => { let volumeDownCommand: string = "Volume Down" if (this.states[controlUnitName]) { let volumeDevice: IDevice = this.getDeviceFromName(this.states[controlUnitName]!.currentActivity.VolumeDevice); if (volumeDevice.supportsCommand(volumeDownCommand)) { this.sendCommand(volumeDevice.getCommand(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: IDevice = 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; } } if (device && device.supportsCommand(commandName)) { this.sendCommand(device.getCommand(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; } /** * Gets device button commands * @param deviceCommandName The device command name * @param deviceName The device name */ public getCommand(deviceCommandName: string, deviceName: string): string { const device: IDevice = this.getDeviceFromName(deviceName); if (device && device.supportsCommand(deviceCommandName)) { return device.getCommand(deviceCommandName); } else { return ""; } } /** * Send a command to the harmony hub. * @param command The command to send. */ public 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}`); } } /** * 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. */ private getDeviceFromName(deviceName: string): IDevice { return this.devices[deviceName]; } /** * 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: IDevice = devicesToTurnOn.filter(i => i.name === value.DeviceName)[0]; delete devicesToTurnOn[devicesToTurnOn.indexOf(deviceToRemove)]; } }); } } return devicesToTurnOn; } } export default HarmonyDataProvider;