UNPKG

hap-homematic

Version:

provides a homekit bridge to the ccu

501 lines (454 loc) 17.3 kB
/* * File: HomeMaticRPC.js * Project: hap-homematic * File Created: Saturday, 7th March 2020 9:01:46 pm * Author: Thomas Kluge (th.kluge@me.com) * ----- * The MIT License (MIT) * * Copyright (c) Thomas Kluge <th.kluge@me.com> (https://github.com/thkl) * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. * ========================================================================== */ 'use strict' const xmlrpc = require('homematic-xmlrpc') const binrpc = require('binrpc') const EventEmitter = require('events') class HomeMaticRPCClient { constructor(ifName, sysID, hostIP, hostPort, path, log) { this.log = log this.port = hostPort this.host = hostIP this.ifName = ifName this.sysID = sysID this.isRunning = false if (this.ifName.indexOf('CUxD') > -1) { this.log.debug('[RPC] CuxD Extra ....') this.client = binrpc.createClient({ host: hostIP, port: hostPort, path: path, queueMaxLength: 100 }) this.protocol = 'xmlrpc_bin://' } else { this.client = xmlrpc.createClient({ host: hostIP, port: hostPort, path: path, queueMaxLength: 100 }) this.protocol = 'http://' } } init(localIP, listeningPort) { let self = this this.localIP = localIP this.listeningPort = listeningPort this.log.debug('[RPC] CCU RPC Init Call on %s port %s for interface %s local server port %s', this.host, this.port, this.ifName, listeningPort) var command = this.protocol + this.localIP + ':' + this.listeningPort this.ifId = this.sysID + '_' + this.ifName this.log.debug('[RPC] init parameter is %s %s', command, this.ifId) try { this.client.methodCall('init', [command, this.ifId], (error, value) => { self.log.debug('[RPC] CCU Response for init at %s with command %s,%s ...Value (%s) Error : (%s)', self.ifName, command, self.ifId, JSON.stringify(value), error) self.ping() self.isRunning = true }) } catch (e) { } } stop() { let self = this return new Promise((resolve, reject) => { self.log.debug('[RPC] disconnecting interface %s', self.ifName) try { self.client.methodCall('init', [self.protocol + self.localIP + ':' + self.listeningPort], (error, value) => { self.isRunning = false if ((error !== undefined) && (error !== null)) { self.log.error('[RPC] Error while disconnecting interface %s Error : %s', self.ifName, error) reject(error) } else { self.log.debug('[RPC] interface %s disconnected', self.ifName) resolve() } }) } catch (e) { resolve() } }) } sendRPCommand(command, parameters) { let self = this return new Promise((resolve, reject) => { self.client.methodCall(command, parameters, (error, value) => { if (error) { self.log.error('[RPC] Error while sending command %s to interface %s Error : %s', command, self.ifName, error) reject(error) } else { // self.log.debug('[RPC] interface %s returns %s', self.ifName, value) resolve(value) } }) }) } reportValueUsage(listDps) { let self = this self.log.debug('[RPC] Report Usage to %s', self.ifName) Object.keys(listDps).map((dpName) => { // Split into address and datapointname let parts = dpName.split('.') // part0 is the interface (we do not need this) let adr = parts[1] let dpn = parts[2] let cnt = listDps[dpName] self.log.debug('[RPC] Report %s time Usage of %s.%s', cnt, adr, dpn) self.sendRPCommand('reportValueUsage', [adr, dpn, cnt]) }) } ping() { this.lastMessage = Math.floor((new Date()).getTime() / 1000) } } class HomeMaticRPC extends EventEmitter { constructor(ccumanager, port) { super() this.log = ccumanager.log this.ccumanager = ccumanager this.server = undefined this.client = undefined this.stopped = false this.localIP = undefined this.bindIP = undefined this.listeningPort = port this.lastMessage = 0 this.watchDogTimer = undefined this.rpc = undefined this.rpcInit = undefined this.pathname = '/' this.resetInterfaces() this.watchDogTimeout = 300 this.localIP = this.getIPAddress() } async init(watchDogTimeout) { if (watchDogTimeout !== undefined) { this.log.debug('Setup Watchdog to %s', watchDogTimeout) this.watchDogTimeout = parseInt(watchDogTimeout) } this.server = await this.initServer(xmlrpc, this.listeningPort) } initServer(module, port) { let self = this var server this.log.debug('[RPC] creating rpc server on port %s', port) return new Promise((resolve, reject) => { this.isPortTaken(port, (error, inUse) => { if (error === null) { if (inUse === false) { server = module.createServer({ host: '0.0.0.0', // listen to all port: port }) server.on('NotFound', (method, params) => { // self.log.debug("Method %s does not exist. - %s",method, JSON.stringify(params)); }) server.on('system.listMethods', (err, params, callback) => { // if (self.stopped === false) { let iface = self.interfaceForEventMessage(params) if (iface !== undefined) { self.log.debug("[RPC] Method call params for 'system.listMethods': %s (%s)", JSON.stringify(params), err) } else { self.log.error('[RPC] unable to find Interface for %s', params) } // } else { // self.log.error('[RPC] Modul is not running ignore call listMethods') // } callback(null, ['event', 'system.listMethods', 'system.multicall']) }) server.on('listDevices', (err, params, callback) => { // if (self.stopped === false) { let iface = self.interfaceForEventMessage(params) if (iface !== undefined) { self.log.debug('[RPC] <- listDevices on %s - Zero Reply (%s)', iface.ifName, err) } // } else { // self.log.error('[RPC] Modul is not running ignore call listDevices') // } callback(null, []) }) server.on('newDevices', (err, params, callback) => { // if (self.stopped === false) { let iface = self.interfaceForEventMessage(params) if (iface !== undefined) { if ((iface.isRunning === true) && (iface.reconnecting === false)) { self.log.debug('[RPC] <- newDevices on %s. Emit this for the ccu to requery rega (%s)', iface.ifName, err) self.emit('newDevices', {}) } } // we are not intrested in new devices cause we will fetch them at launch // } callback(null, []) }) server.on('event', (err, params, callback) => { // if (self.stopped === false) { if (!err) { let iface = self.interfaceForEventMessage(params) if ((iface !== undefined) && (iface.isRunning === true)) { iface.ping() self.handleEvent(iface, 'event', params) } else { self.log.error('[RPC] event unable to find Interface for %s', params) } } // } callback(err, []) }) server.on('system.multicall', (err, params, callback) => { if (!err) { params.map((events) => { try { events.map((event) => { let iface = self.interfaceForEventMessage(event.params) if ((iface !== undefined) && (iface.isRunning === true)) { iface.ping() self.handleEvent(iface, event.methodName, event.params) } else { self.log.error('[RPC] multiCall unable to find Interface for %s', JSON.stringify(event.params)) } }) } catch (err) { } }) } callback(null) }) self.log.info('[RPC] server for all interfaces is listening on port %s.', port) resolve(server) } else { self.log.error('****************************************************************************************************************************') self.log.error('* Sorry the local port %s on your system is in use. Please make sure, self no other instance of this plugin is running.', port) self.log.error('* you may change the initial port with the config setting for local_port in your config.json ') self.log.error('* giving up ... the homematic plugin is not able to listen for ccu events on %s until you fix this. ') self.log.error('****************************************************************************************************************************') reject(new Error('port in use error')) } } else { self.log.error('* Error while checking ports') reject(new Error('port check error')) } }) }) } interfaceForEventMessage(params) { var result if ((params) && (params.length > 0)) { let ifTest = params[0] this.interfaces.map(iface => { if (iface.ifId === ifTest) { result = iface } // this is the cuxd extra handling cause cuxd is not rega compliant and returns alwas CUxD instead of the interface identifier from the init call if ((ifTest === 'CUxD') && ((iface.ifName.indexOf('CUxD') > -1))) { result = iface } }) } return result } clientFromName(ifId) { var result this.interfaces.map(iface => { if (iface.ifName === ifId) { result = iface } }) return result } connectedInterfaces() { return this.interfaces } handleEvent(iface, method, params) { let self = this if ((method === 'event') && (params !== undefined)) { let ifName = iface.ifName let channel = ifName + params[1] let datapoint = params[2] let value = params[3] let rgx = /([a-zA-Z0-9-]{1,}).([a-zA-Z0-9-_]{1,}):([0-9]{1,}).([a-zA-Z0-9-_]{1,})/g let parts = rgx.exec(channel + '.' + datapoint) if ((parts) && (parts.length > 4)) { let idx = parts[1] let address = parts[2] let chidx = parts[3] let evadr = idx + '.' + address + ':' + chidx + '.' + datapoint self.log.debug('[RPC] event for %s.%s with value %s', channel, datapoint, value) self.emit('event', { address: evadr, value: value }) } } } addInterface(ifName, hostIP, hostPort, path) { if (!ifName.endsWith('.')) { ifName = ifName + '.' } // PowerUP rpc bin if needed if ((ifName.indexOf('CUxD') > -1) && (!(this.binServer))) { this.log.debug('[RPC] open extra Connector for CuxD') this.binServer = this.initServer(binrpc, this.listeningPort + 1) this.log.debug('[RPC] Connector for CuxD is done') } this.log.debug('[RPC] adding Interface %s Host %s Port %s Path %s', ifName, hostIP, hostPort, path) let client = new HomeMaticRPCClient(ifName, 'HAP', hostIP, hostPort, path, this.log) this.interfaces.push(client) } connect() { let self = this this.log.debug('[RPC] Connecting all interfaces (%s interfaces found)', this.interfaces.length) this.interfaces.map(iface => { let port = self.listeningPort if (iface.ifName.indexOf('CUxD') > -1) { port = port + 1 } self.log.debug('[RPC] init interface %s for connection to port %s', iface.ifName, port) let localIp = self.localIP // Check if the hostIP is equal to the ccuIP so if they are , we can switch to 127.0.0.1 // https://github.com/thkl/hap-homematic/issues/437 if ((iface.host === this.localIP) || (iface.host === '127.0.0.1')) { localIp = '127.0.0.1' self.log.debug('[RPC] looks like hap is running local so switch to 127.0.0.1') } else { self.log.debug('[RPC] remote ccu %s %s', iface.host, this.localIP) } iface.init(localIp, port) iface.serverPort = port }) if ((this.watchDogTimeout !== undefined) && (this.watchDogTimeout > 0)) { this.log.info('[RPC] init watchdog %s seconds', this.watchDogTimeout) this.ccuWatchDog() } else { this.log.info('[RPC] skipped watchdog (%s)', this.watchDogTimeout) } this.stopping = false } ccuWatchDog() { var self = this this.interfaces.map(iface => { if (iface.lastMessage !== undefined) { var now = Math.floor((new Date()).getTime() / 1000) var timeDiff = now - iface.lastMessage if (timeDiff > self.watchDogTimeout) { self.log.debug('[RPC] Watchdog Trigger - Reinit Connection for %s after idle time of %s seconds', iface.ifName, timeDiff) self.lastMessage = now iface.reconnecting = true try { iface.stop() } catch (e) { self.log.error('[RPC] error while watchdog interface stop; will try to reconnect') } try { iface.init(self.localIP, iface.serverPort) } catch (e) { self.log.error('[RPC] error while watchdog reconnecting') } setTimeout(() => { iface.reconnecting = false }, 2000) } } }) var recall = () => { self.ccuWatchDog() } this.watchDogTimer = setTimeout(recall, 10000) } disconnectInterfaces() { let self = this if (this.stopped) { return } clearTimeout(this.watchDogTimer) this.stopped = true return Promise.all(self.interfaces.map(async (iface) => { try { await iface.stop() } catch (e) { // we are disconnecting .. 's tät wurschd sein } }) ) } sendInterfaceCommand(ifId, command, parameters) { let self = this this.log.debug('[RPC] sendInterfaceCommand %s %s', ifId, command) return new Promise((resolve, reject) => { let hmRpcClient = self.clientFromName(ifId + '.') if (hmRpcClient) { self.log.debug('[RPC] interface found going ahead') hmRpcClient.sendRPCommand(command, parameters).then(result => { resolve(result) }).catch(error => reject(error)) } else { self.log.debug('[RPC] interface %s NOT found ', ifId) } }) } resetInterfaces() { this.log.debug('[RPC] reseting all interface connections') this.interfaces = [] this.stopped = false } async stop() { let self = this await this.disconnectInterfaces() this.log.debug('[RPC] closing eventserver') this.server.close(() => { self.log.debug('[RPC] eventserver removed') }) if (this.binServer) { this.binServer.close() } } getIPAddress() { const os = require('os') var interfaces = os.networkInterfaces() for (var devName in interfaces) { var iface = interfaces[devName] for (var i = 0; i < iface.length; i++) { var alias = iface[i] if (alias.family === 'IPv4' && alias.address !== '127.0.0.1' && !alias.internal && (alias.address.indexOf('169.254.') === -1)) { return alias.address } } } return '0.0.0.0' } // checks if the port is in use // https://gist.github.com/timoxley/1689041 isPortTaken(port, fn) { var net = require('net') var tester = net.createServer().once('error', (err) => { if (err.code !== 'EADDRINUSE') return fn(err) fn(null, true) }) .once('listening', () => { tester.once('close', () => { fn(null, false) }) .close() }).listen(port) } } module.exports = HomeMaticRPC