import { Server } from '@hapi/hapi'; import * as net from 'net'; import * as fse from 'fs-extra'; import { join as pathJoin } from 'path'; import { IInputAdapterManager, ILoopBoxInputRequest, IInputAdapter, ILoopBoxAdapterState, ILoopBoxIntent } from 'loopbox-types'; import { forget, bind, pjson } from 'loopbox-utils'; declare module '@hapi/hapi' { interface ServerOptionsApp { inputAdapterManager?: IInputAdapterManager; } } const pkg = pjson(); export interface ILutronInputRequest { fromKeypad: boolean; command: string; } export const inputAdapterPlugin = { name: pkg.loopbox_inputadapter_lutron.domain, register: async (server: Server, options: any): Promise => { server.log(['InputAdapter-Lutron', 'info'], 'lutronInputAdapter register'); const lutronInputAdapter = new LutronInputAdapter(server, options); server.settings.app.inputAdapterManager.scheme(lutronInputAdapter); lutronInputAdapter.initialized = await lutronInputAdapter.initialize(); } }; export class LutronInputAdapter implements IInputAdapter { public initialized = false; private tcpServer: net.Server; private server; private options; private settings; private state: ILoopBoxAdapterState; private commandToNLMap: any; constructor(server: any, options: any) { this.server = server; this.options = options; this.settings = pkg.loopbox_inputadapter_lutron; this.state = server.settings.app.state; } public get id(): string { return this.settings.domain; } public get scheme(): string { return 'lutron'; } public async initialize(): Promise { let result = true; try { await this.startListener(); } catch (ex) { this.server.log(['InputAdapter-Lutron', 'error'], `initialize failed: ${ex.message}`); result = false; } return result; } public async preAdapt(requestId: string, userId: string, lutronInputAdapterRequest: any): Promise { if (!this.commandToNLMap) { this.commandToNLMap = await fse.readJson(pathJoin(this.state.getPluginPath(this.id), 'commandToNLMap.json')); } const nlCommand = this.commandToNLMap[lutronInputAdapterRequest.command].nlCommand; this.server.log(['InputAdapter-Lutron', 'info'], `NL map for command: ${lutronInputAdapterRequest.command} - "${nlCommand}"`); if (nlCommand) { return { scheme: 'lutron', requestId, processorId: this.id, userId, query: nlCommand, context: { fromKeypad: lutronInputAdapterRequest.fromKeypad, command: lutronInputAdapterRequest.command } }; } else { throw new Error(`No NL map found for command: ${lutronInputAdapterRequest.command}`); } } // @ts-ignore (loopBoxIntent, originalPayload) public async postAdapt(loopBoxIntent: ILoopBoxIntent, originalPayload: any): Promise { this.server.log(['InputAdapter-Lutron', 'info'], 'postAdapt (nothing to return)'); return {}; } private async startListener(): Promise { try { this.tcpServer = net.createServer(this.clientConnectionListener); this.tcpServer.listen(this.options.listenerPort, () => { const serverInfo = this.tcpServer.address(); this.server.log(['InputAdapter-Lutron', 'info'], `TCP server listening on address : ${JSON.stringify(serverInfo)}`); this.tcpServer.on('close', () => { this.server.log(['InputAdapter-Lutron', 'info'], 'TCP server closed'); }); this.tcpServer.on('error', (error) => { this.server.log(['InputAdapter-Lutron', 'error'], `TCP server error: ${error.message}`); }); }); } catch (ex) { this.server.log(['InputAdapter-Lutron', 'error'], `Error creating TCP listener: ${ex.message}`); } } @bind private clientConnectionListener(conn: net.Socket) { this.server.log(['InputAdapter-Lutron', 'info'], `Client connection on: ${conn.remoteAddress}:${conn.remotePort}`); conn.on('data', (data) => { try { const lutronCommand = data.toString().trim(); if (lutronCommand) { this.server.log(['InputAdapter-Lutron', 'info'], `Client data received: ${lutronCommand}`); const lutonInputRequest: ILutronInputRequest = { fromKeypad: true, command: lutronCommand }; forget(this.handleLutronCommand, lutonInputRequest); } } catch (ex) { this.server.log(['InputAdapter-Lutron', 'info'], `Error processing client data: ${ex.message}`); } }); conn.on('end', () => { this.server.log(['InputAdapter-Lutron', 'info'], `Client disconnected`); }); conn.on('error', (error) => { this.server.log(['InputAdapter-Lutron', 'error'], `Client connection error: ${error.message}`); }); } @bind private async handleLutronCommand(lutonInputRequest: ILutronInputRequest): Promise { try { await this.server.inject({ method: 'POST', url: `/api/v1/input/hook/lutron`, headers: { 'Content-Type': 'application/json', Authorization: this.options.loopBoxAuth }, payload: lutonInputRequest }); } catch (ex) { this.server.log(['InputAdapter-Lutron', 'error'], `Error processing command: ${ex.message}`); } } }