import { Activity } from "../Models/Activity"; import { DeviceSetupItem } from "../Models/DeviceSetupItem"; import { Input, Matrix, Output } from "../Models/Matrix"; 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; const Harmony = require("harmony-websocket"); interface IActivityState { currentActivity: Activity } interface IHubDataProviderProps { harmonyHubAddress: string, neeoHubAddress: string, log: any, matrix: Matrix } interface INeeoMacro { roomKey: string, macroKey: string, deviceKey: string, } class HubDataProvider { //Harmony Fields private harmonyHubAddress: string; private harmonyConnected: boolean = false; //Neeo Fields private neeoHubAddress: string; //Common Fields private log: any; private devices: { [name: string]: IDevice; } = {}; private states: { [controlUnitName: string]: (IActivityState | undefined) } = {}; private matrix: Matrix; constructor(props: IHubDataProviderProps) { this.log = props.log; this.harmonyHubAddress = props.harmonyHubAddress; this.neeoHubAddress = props.neeoHubAddress; this.matrix = props.matrix; if (this.harmonyHubAddress) { try { 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}`) } } } /** * Power on all devices in an activity. */ public powerOn = async (controlUnitName: string, activity: Activity) => { //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.deviceSetupItems .map((value: DeviceSetupItem): IDevice => { return this.getDeviceFromName(value.deviceName, value.hubType); }); //Resolve device conflicts with other controlUnits devicesToTurnOff = this.sanitizeDeviceList(devicesToTurnOff, controlUnitName); await Promise.all( //Turn off devices devicesToTurnOff.map(async (device: IDevice) => { if (device) { await device.powerOff(); } }) ); this.states[controlUnitName] = undefined; } /** * Start an activity */ public startActivity = async (controlUnitName: string, activity: Activity) => { this.log(`Starting activity ${activity.displayName} for controlUnit: ${controlUnitName}`) let lastActivity: Activity | undefined = undefined; if (this.states[controlUnitName]) { lastActivity = this.states[controlUnitName]!.currentActivity; } //Build potential list of devices to to turn on let devicesToTurnOn: Array = activity.deviceSetupItems.map((value: DeviceSetupItem): IDevice => { return this.getDeviceFromName(value.deviceName, value.hubType); }); //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) { if (!device.on) { this.log(`Turning on device ${device.name}`) await device.powerOn(); } } })); //Assign correct input await Promise.all( activity.deviceSetupItems.map(async (value: DeviceSetupItem) => { let device: IDevice = this.getDeviceFromName(value.deviceName, value.hubType); if (device && device.supportsCommand(`Input ${value.input}`)) { let command: string = device.getCommand(`Input ${value.input}`); await device.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}`; let matrixDevice: IDevice = this.getDeviceFromName(this.matrix.deviceName, this.matrix.hubType); //Route hdmi if (matrixDevice.supportsCommand(inputCommandName) && matrixDevice.supportsCommand(outputCommandName)) { await matrixDevice.sendCommand(matrixDevice.getCommand(inputCommandName)); await matrixDevice.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, value.hubType); }); //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.deviceSetupItems.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 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" 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(currentActivity.volumeDeviceId, deviceSetupItem.hubType); if (volumeDevice.supportsCommand(volumeUpCommand)) { volumeDevice.sendCommand(volumeDevice.getCommand(volumeUpCommand)); } } } /** * Volume down for current running activity. */ 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(currentActivity.volumeDeviceId, deviceSetupItem.hubType); if (volumeDevice.supportsCommand(volumeDownCommand)) { volumeDevice.sendCommand(volumeDevice.getCommand(volumeDownCommand)); } } } /** * Return if a control unit is active * @param controlUnitName */ public getIsActive(controlUnitName: string): Activity | undefined { return this.states[controlUnitName] ? this.states[controlUnitName]!.currentActivity : undefined; } /** * 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.deviceSetupItems.forEach((value: DeviceSetupItem) => { //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; } /** * 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 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"; 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)) { device.sendCommand(device.getCommand(commandName)); } } } /** * Get the IDevice by name. * @param deviceName The device to retrieve. */ private getDeviceFromName(deviceName: string, hubType: hub): IDevice { return this.devices[`${hubType}-${deviceName}`]; } /** * Connect to harmony hub and receive device info. */ private connectHarmony = async (address: string): Promise => { let harmony: any = new Harmony(); //Listeners harmony.on('open', () => { this.harmonyConnected = true; }); harmony.on('close', () => { this.harmonyConnected = false; }); await harmony.connect(address); let self = this; setTimeout(async function () { if (self.harmonyConnected) { let devices: any = await 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 harmony.getDeviceCommands(dev.id); deviceCommands.forEach((command: any) => { commands[command.label] = command.action; }); self.devices[`Harmony-${dev.label}`] = { id: dev.id, name: dev.label, commands: commands, harmony: harmony, hubType: "Harmony", log: self.log, on: false, //Define device methods supportsCommand(commandName: string): boolean { let command = commands[commandName]; return (command) ? true : false; }, getCommand(commandName: string): string { return commands[commandName]; }, 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; } }, async sendCommand(command: string): Promise { 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}`); } } } })); self.log(`Harmony data provider ready`); } catch (err) { self.log(`ERROR - error connecting to harmony: ${err}`); } } }, 1000); } 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(powerOnCommand); this.on = true; } else if (this.supportsCommand(powerToggleCommand)) { await this.sendCommand(powerToggleCommand); this.on = true; } }, async powerOff(): Promise { let powerOffCommand: string = "Power Off"; let powerToggleCommand: string = "Power Toggle"; if (this.supportsCommand(powerOffCommand)) { await this.sendCommand(powerOffCommand); this.on = false; } else if (this.supportsCommand(powerToggleCommand)) { await this.sendCommand(powerToggleCommand); this.on = false; } } } } } } } export default HubDataProvider;