UNPKG

homebridge-smartsystem

Version:

SmartServer (Proxy Websockets to TCP sockets, Smappee MQTT, Duotecno IP Nodes, Homekit interface)

316 lines (257 loc) 10.3 kB
import * as net from 'net'; import * as WebSocket from 'ws'; import { spawn } from 'child_process'; import { argv, cwd, execPath } from 'process'; // // [ device (TCP) ] ←→ [ Proxy = WebSocket client (ws) ] ←→ [ Cloud = 2 x WebSocket server (ws) ] ←→ [ browser client ] // export interface ProxyConfig { cloudServer: string; // cloud server address cloudPort: number; // cloud server port masterAddress: string; // local master address masterPort: number; // local master port uniqueId: string; // unique ID for the device (for now the no-ip address + port) debug?: boolean; // debug mode kind?: "pm2" | "gw" | "solo"; } export const kEmptyProxy: ProxyConfig = { cloudServer: "ws.duotecno.eu", cloudPort: 5098, masterAddress: "", masterPort: 5001, uniqueId: "", // no unique ID -> don't start the proxy debug: false, // default nodebug mode kind: "solo" // default running under PM2 or other process manager -> don't restart yourself } export function setProxyConfig(c?: ProxyConfig): ProxyConfig { // set the proxy config if (c) { config.cloudServer = c.cloudServer || kEmptyProxy.cloudServer; config.cloudPort = c.cloudPort || kEmptyProxy.cloudPort; config.masterAddress = c.masterAddress || kEmptyProxy.masterAddress; config.masterPort = c.masterPort || kEmptyProxy.masterPort; config.uniqueId = c.uniqueId || kEmptyProxy.uniqueId; config.debug = !!c.debug; config.kind = c.kind || kEmptyProxy.kind; } else { config = {...kEmptyProxy}; } return config; } let config: ProxyConfig; try { setProxyConfig(require('../config-proxy.json')) } catch (e) { setProxyConfig(); console.log("No config-proxy.json found, using default values from homebridge or other."); } const gCloudConnections: Array<Context> = []; const kDebug = config.debug || false; // enable debug mode from config ///////////// // Logging // ///////////// function debug(str: string) { if (kDebug) { console.log(`DEBUG: ${str}`); } } function warning(str: string) { console.warn(`WARNING: ${str}`); } function log(str: string) { console.log(`LOG: ${str}`); } function error(str: string) { console.error(`ERROR: **** ${str} ****`); } /////////////////////////////// // Start up the proxy server // /////////////////////////////// // if (config.kind != "gw") { // makeNewCloudConnection(config.masterAddress, config.masterPort, config.cloudServer, config.cloudPort, config.uniqueId); // } export function makeNewCloudConnection(master: string, masterPort: number, server: string, serverPort: number, uniqueId: string) { const cloudSocket = new WebSocket('ws://'+server+':'+serverPort+"/"+uniqueId); cloudSocket.on('open', () => { log(`[PROXY] → [CLOUD] Connected to Cloud at ${server}:${serverPort}`); const context = new Context(cloudSocket, master, masterPort, server, serverPort, uniqueId); gCloudConnections.push(context); log(`[PROXY] → [CLOUD] New free connection #${gCloudConnections.length} to the Cloud at ${server}:${serverPort}`); }); log(`[PROXY] → [CLOUD] Attempting to start a new free connection for ${config.uniqueId} to the Cloud at ${server}:${serverPort}`); } //////////////// // Restarting // //////////////// export function cleanStart(restart: boolean = false) { if (gCloudConnections.length) { gCloudConnections.forEach(context => { if (context.deviceSocket) { context.deviceSocket.end(); context.deviceSocket = null; } context.cloudSocket.close(); context.cloudSocket = null; }); gCloudConnections.splice(0, gCloudConnections.length); // clear the array log(`[PROXY] → [CLOUD] Cleaned up all cloud connections.`); } if (restart) { if (config.uniqueId) { makeNewCloudConnection(config.masterAddress, config.masterPort, config.cloudServer, config.cloudPort, config.uniqueId); } else { log(`[PROXY] → [CLOUD] Not restarting, no unique ID configured, not starting the proxy.`); } } else { log(`[PROXY] → [CLOUD] Not restarting, no new cloud connection, just cleaning up.`); } } export class Context { deviceSocket: net.Socket; master: string; masterPort: number; cloudSocket: WebSocket; server: string; serverPort: number; // unique identifier for this device-gateway combination uniqueId: string; constructor(cloudSocket: WebSocket, master: string, masterPort: number, server: string, serverPort: number, uniqueId: string) { this.deviceSocket = null; this.master = master; this.masterPort = masterPort; this.cloudSocket = cloudSocket; this.server = server; this.serverPort = serverPort; this.uniqueId = uniqueId; this.setupCloudSocket(); } setupCloudSocket() { // set up handlers for the cloud socket this.cloudSocket.on('message', message => { this.handleDataFromCloud(message as Buffer); }); this.cloudSocket.on('close', (code, reason) => { warning('[CLOUD] Connection closed: ' + code + ' ' + reason); if (this.deviceSocket) { this.deviceSocket.end(); this.deviceSocket = null; } }); this.cloudSocket.on('error', err => { error(`[CLOUD] Error: ${err.message}`); if (this.deviceSocket) { this.deviceSocket.end(); this.deviceSocket = null; } }); } handleDataFromCloud(data: Buffer) { debug(`[CLOUD] → [PROXY]: Data from Cloud: ${data.toString().slice(0, -1)}`); if (!this.deviceSocket) { // we either have a real client that wants to connect to the device, // or we receive a heartbeat from the cloud server if (this.isHeartbeatRequest(data)) { debug(`[CLOUD] → [PROXY]: Received heartbeat from Cloud, returning response.`); // respond to the heartbeat of the cloud server this.cloudSocket.send("[72,3]"); // command([Command.HeartbeatStatus, CommandMethod.Heartbeat.server]) } else { // There is incomming data, it's a fresh connection, so: a new client wants to connect to the device // 1) create a new (free) connection to the cloud server for the next client // 2) connect to the device and send the initial data log(`[PROXY] → [CLOUD] New client connection, creating new free connection for this proxy/device: ${this.uniqueId}.`); makeNewCloudConnection(this.master, this.masterPort, this.server, this.serverPort, this.uniqueId); this.makeDeviceConnection(this.master, this.masterPort, data); } } else if (this.deviceSocket.readyState === 'open') { this.deviceSocket.write(data); debug(`[PROXY] → [DEVICE] forwarding data to device: ${data.toString().slice(0, -1)}`); } else { warning(`[PROXY] → [DEVICE] Device socket is not open yet, waiting for connection... what to do with the data??`); } } makeDeviceConnection(address: string, port: number, data: Buffer) { log(`[PROXY] → [DEVICE] Attempting to connect to local device at ${address}:${port}`); if (!this.deviceSocket) { this.deviceSocket = new net.Socket(); // Connect to local device and send the data that came in from the cloud this.deviceSocket.connect(port, address, () => { log(`[PROXY] → [DEVICE] Connected to local device at ${address}:${port}`); this.setUpDeviceSocket(); log(`[PROXY] → [DEVICE] Sending initial message: ${data.toString().slice(0, -1)}`); this.deviceSocket.write(data); }); } else if (this.deviceSocket.readyState === 'open') { warning(`[PROXY] → [DEVICE] Device socket already open, sending data directly -- STRANGE !!`); this.deviceSocket.write(data); } else { error(`[PROXY] → [DEVICE] Device socket exists, but is not open yet !!`); } } setUpDeviceSocket() { this.deviceSocket.on('data', (data: Buffer) => { const message = data.toString('utf-8'); debug(`[DEVICE] → [PROXY] forwarding data to Cloud: ${message.slice(0, -1)}`); this.cloudSocket.send(message); }); this.deviceSocket.on('close', () => { log(`[DEVICE] → [PROXY] Device socket closed`); this.removeConnection() }); this.deviceSocket.on('error', (err) => { error(`[DEVICE] → [PROXY] Device socket error: ${err.message}`); this.removeConnection() }); } removeConnection() { this.deviceSocket = null; // remove this context and close the cloud socket const index = gCloudConnections.indexOf(this); if (index !== -1) { this.cloudSocket.close(); this.cloudSocket = null; gCloudConnections.splice(index, 1); log(`[PROXY] → [CLOUD] Closed socket to Cloud and removed context for device ${this.master}`); } if (gCloudConnections.length === 0) { log(`[PROXY] → [CLOUD] No more cloud connections, restarting proxy.`); // just to be sure: restart... if (config.kind === "pm2") { // PM2 should restart us. log(`[PROXY] → [CLOUD] PM2 should restart us, exiting now.`); process.exit(); } else if (config.kind === "solo"){ log(`[PROXY] → [CLOUD] Running solo, restarting proxy.`); this.restart(); } else { log(`[PROXY] → [CLOUD] Running under gateway/homebridge, not restarting... not sure what to do here.`); } } } /////////////// // Utilities // /////////////// restart() { console.log('🔄 Restarting...'); spawn(execPath, argv.slice(1), { cwd: cwd(), detached: true, stdio: 'inherit', }); process.exit(); } // check if it's a heartbeat request from the server isHeartbeatRequest(data: Buffer): boolean { // heartbeatKind = (poll = 1, poll_old = 2, serve = 3) if (typeof data === 'string') { return data === "[215,3]"; } else { return ((data[0] === 91) && // [ (data[1] === 50) && // 2 (data[2] === 49) && // 1 (data[3] === 53) && // 5 (data[4] === 44) && // , (data[5] === 51) && // 3 (data[6] === 93)); // ] } } }