UNPKG

hap-homematic

Version:

provides a homekit bridge to the ccu

1,358 lines (1,256 loc) 50.1 kB
/* * File: Server.js * Project: hap-homematic * File Created: Saturday, 7th March 2020 12:47:00 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. * ========================================================================== */ const uuid = require('hap-nodejs').uuid const Bridge = require('hap-nodejs').Bridge const BridgeMock = require('./BridgeMock.js') const HAP = require('hap-nodejs') const Accessory = require('hap-nodejs').Accessory const Service = require('hap-nodejs').Service const Characteristic = require('hap-nodejs').Characteristic const path = require('path') const fs = require('fs') const childProcess = require('child_process') const crypto = require('crypto') const HomeMaticCCU = require(path.join(__dirname, 'HomeMaticCCU.js')) const HomeMaticTestCCU = require(path.join(__dirname, 'HomeMaticTestCCU.js')) const ApplianceType = { Device: 0, Variable: 1, Program: 2, Special: 3 } class Server { constructor(log, configurationPath, ccuHost) { this.log = log this.ccuHost = ccuHost if (configurationPath) { this.log.info('using configuration at %s', configurationPath) this._configurationPath = configurationPath this._hapCache = path.join(configurationPath, 'persist') this._publishedAccessories = {} this._variableAccessories = [] // used to äÄöÖüÜ fetch all at the key event this._specialAccessories = [] this._configuration = {} this.isTestMode = false this.gatoHomeBridge = { hap: { Characteristic: Characteristic, Service: Service }, user: { storagePath: () => { return this._configurationPath } } } } else { this._configuration = {} this.gatoHomeBridge = { hap: { Characteristic: Characteristic, Service: Service }, user: { storagePath: () => { return null } } } } } async init(dryRun) { let self = this this.dryRun = dryRun this.log.info('[Server] settings loading ...') await this.loadSettings() this.log.info('[Server] settings loading is done ; continuing init') this.log.info('[Server] Using config from %s', this._hapCache) HAP.init(this._hapCache) this.launchUIConfigurationServer() if (this.ccuHost !== undefined) { this._configuration.ccuIP = this.ccuHost } this._configuration.storagePath = this._configurationPath this._ccu = new HomeMaticCCU(this.log, this._configuration) // read the restart count and increment it var rcCounter = 0 let rcFile = path.join(this._hapCache, 'restartCounter.json') if (fs.existsSync(rcFile)) { rcCounter = JSON.parse(fs.readFileSync(rcFile)) } rcCounter = rcCounter + 1 this.log.info('[Server] current restart count is %s', rcCounter) if (rcCounter >= 5) { this.log.info('[Server] current restart count is >= 5 disable monitoring') // disable monitoring cause somethin is seriously wrong this._ccu.configuration.enableMonitoring = false // remove the flag this._ccu._removemonitconfig() } try { fs.writeFileSync(rcFile, JSON.stringify(rcCounter)) } catch (e) { } this._ccu.dryRun = this.dryRun if (this.dryRun) { this.continueInitialization() } else { this.checkRegaAlive() } // setup a Timer for 1hour to remove the rcFile if there is one // this will also reset the timer setTimeout(() => { try { self.log.info('[Server] seems we are stable since 1 hour remove the restart counter') if (fs.existsSync(rcFile)) { fs.unlinkSync(rcFile) // also resetup the monitoring self._ccu.processMonitoring() } } catch (e) { self.log.error('[Server] unable to reset restart counter or enable monitoring') } }, 60 * 60 * 1000) } reset() { // this will remove the current config let self = this let files = ['config.json', 'rooms.json', 'devices.json', 'programs.json', 'variables.json', 'persist'] files.forEach(file => { let configFileName = path.join(self._configurationPath, file) if (fs.existsSync(configFileName)) { if (fs.lstatSync(configFileName).isDirectory()) { self.deleteFolderRecursive(configFileName) } else { fs.unlinkSync(configFileName) } } }) } deleteFolderRecursive(pathname) { let self = this if (fs.existsSync(pathname)) { fs.readdirSync(pathname).forEach((file, index) => { const curPath = path.join(pathname, file) if (fs.lstatSync(curPath).isDirectory()) { // recurse self.deleteFolderRecursive(curPath) } else { // delete file fs.unlinkSync(curPath) } }) fs.rmdirSync(pathname) } }; async checkRegaAlive() { // wait until the ccu is alive this.log.info('[Server] Checking rega connection') let self = this let ccuIsRegaAlive = false try { ccuIsRegaAlive = await this._ccu.pingRega() } catch (e) { self.log.error('Unable to ping rega') ccuIsRegaAlive = false } if (ccuIsRegaAlive) { this.log.info('[Server] Rega is alive going ahead') this.continueInitialization() } else { // wait 2 secondz setTimeout(() => { self.log.warn('[Server] Rega is out of home .. try again in 10 seconds') self.checkRegaAlive() }, 10000) } } getConfig(key) { return this._configuration[key] } async continueInitialization() { let self = this this.log.info('[Server] continueInitialization') await this.connectCCU() this.log.info('[Server] Bridge Connections are up for %s bridges', this._bridges.length) this._ccu.on('devicelistchanged', async () => { // rebuild the service list self.log.debug('[Server] CCU DeviceList just changed') await self._buildCompatibleObjectList() }) await self.publishServiceTable() } async simulate(simPath, simData) { // Load the simfile let self = this if (simPath !== undefined) { this.log.info('Simulation starts') this._configurationPath = simPath let simfile = path.join(simPath, 'devices.json') if (fs.existsSync(simfile)) { let oDb = JSON.parse(fs.readFileSync(simfile)) let serviceList = await this.buildServiceList() let supportedChannelList = Object.keys(serviceList) let devAdrList = [] let devList = [] if (oDb.devices) { oDb.devices.map(device => { device.channels.map(channel => { // first check DEVICETYPE:CHANNELTYPE if (supportedChannelList.indexOf(device.type + ':' + channel.type) > -1) { channel.isSuported = true if (devAdrList.indexOf(device.address) === -1) { devAdrList.push(device.address) devList.push(device) } } if (supportedChannelList.indexOf(channel.type) > -1) { channel.isSuported = true if (devAdrList.indexOf(device.address) === -1) { devList.push(device) devAdrList.push(device.address) } } }) }) } this.log.info('[Server] list of supported devices') devList.map(device => { self.log.info('Device : %s | %s', device.name, device.address) device.channels.map(channel => { if (channel.isSuported === true) { self.log.info('Channel : %s | %s', device.name, channel.address) } }) }) } else { this.log.error('File not found %s', simfile) } this.log.info('Simulation status end will power up the system in test mode') await this.loadSettings() this.launchUIConfigurationServer() await this.publishServiceTable() this._ccu = new HomeMaticCCU(this.log, this._configuration) await this._ccu.loadDatabases(simPath, true) // Start CCU in testMOde this.log.info('[Server] Bridge Connections skipped as we are in test mode') this._bridges = [] this._publishedAccessories = {} await self._buildCompatibleObjectList() this.publishAccessoriesToConfigurationService() this.publishBridgeInfosToConfigurationService() return } if (simData) { // this is for unit tests if ((simData.config) && (simData.devices)) { this.isTestMode = true this._configuration = simData.config this.log.debug('[Server] Powering Test CCU') this._ccu = new HomeMaticTestCCU(this.log, this._configuration) this._ccu.init() this._ccu.setDummyDevices(simData.devices) if (simData.values) { this._ccu.setDummyValues(simData.values) } this.log.debug('[Server] Bridge Connections skipped as we are in test mode') this._bridges = [] this._publishedAccessories = {} await self._buildCompatibleObjectList() if (simData.mappings) { this.log.debug('[Server] TestMode Mapping : %s', JSON.stringify(simData.mappings)) } this._configuration.mappings = simData.mappings || {} // this.serviceConfig = await this.buildServiceList() await this.publishServiceTable() this.buildMappings(this._configuration.mappings, this.serviceConfig) this.log.debug('[Server] power up mockup bridge as we are in test mode') let channels = [] // create a empty mapping if not set by the test this._configuration.channels.map(channelAddress => { let channel = {} // generate a dummy channel objectservice used for simData.devices.map(simDevice => { simDevice.channels.map(simChannel => { if (simChannel.address === channelAddress) { channel = simChannel channel.dtype = simDevice.type } }) }) channels.push(channel) }) // create a test Instance b6589fc6-ab0d-4c82-8f12-099d1c2d40ab is default id this.powerUpBridges({ 'b6589fc6-ab0d-4c82-8f12-099d1c2d40ab': { 'name': 'test', 'user': '11:22:33:44:55:66', 'pincode': '123-456-789' } }, channels, [], [], []) this.log.debug('[Server] test mode setup done') } } } powerUpBridges(lInstances, channelsToMap, variables, programs, special) { let self = this this.currentPortNum = 9877 // Check Instances to power up Promise.all( Object.keys(lInstances).map(instanceID => { self.log.debug('[Server] loading instance %s with name %s', instanceID, JSON.stringify(lInstances[instanceID])) let instanceData = lInstances[instanceID] || { pin: self.generatePin(), name: 'default' } self.log.debug('[Server] instance will run on port %s', self.currentPortNum) let bridge = self.loadInstance(instanceID, instanceData, channelsToMap, variables, programs, special, self.currentPortNum) self._bridges.push(bridge) self.currentPortNum = self.currentPortNum + 1 }) ) } saveSettings(settings) { let configFile = path.join(this._configurationPath, 'config.json') fs.writeFileSync(configFile, JSON.stringify(settings, ' ', 1)) } /** creates the default homekit instance */ createDefaultInstance() { let defInst = uuid.generate('0') let instances = {} instances[defInst] = { name: 'default', user: this.randomMac(), pin: this.generatePin() } this.saveSettings({ instances: instances }) this._configuration.instances = instances return this._configuration.instances } async connectCCU() { let self = this this._bridges = [] this.log.debug('[Server] preparing interface communication to ccu') if (!this.dryRun) { await this._ccu.prepareConnections() } await this._ccu.loadDatabases(this._configurationPath) let lInstances = this._configuration.instances if (lInstances === undefined) { lInstances = this.createDefaultInstance() } this.log.debug('[Server] instances to load : %s', JSON.stringify(lInstances)) let channelsToMap = [] self.log.debug('[Server] mapping requested channels') if (this._configuration.channels) { Promise.all(this._configuration.channels.map(chaddress => { let channel = self._ccu.getChannelByAddress(chaddress) if (channel) { channelsToMap.push(channel) } })) } self.log.debug('[Server] setup used interfaces for %s mapped channels', channelsToMap.length) channelsToMap.map((channel) => { let oInterface = self._ccu.getInterfaceWithID(channel.intf) if (oInterface) { oInterface.inUse = true } else { self.log.warn('[Server] interface %s for channel %s not found', channel.intf, channel.name) } }) self.log.debug('[Server] mapping variables') let variablesToMap = [] if (this._configuration.variables) { Promise.all(this._configuration.variables.map(variable => { // create a fake channel address so the splitter will work variablesToMap.push({ name: variable, address: variable + ':0' }) })) } self.log.debug('[Server] mapping requested variables %s found', variablesToMap.length) let programsToMap = [] if (this._configuration.programs) { Promise.all(this._configuration.programs.map(program => { // create a fake channel address so the splitter will work programsToMap.push({ name: program, address: program + ':0' }) })) } let specialToMap = [] if (this._configuration.special) { Promise.all(this._configuration.special.map(special => { // create a fake channel address so the splitter will work specialToMap.push({ name: special, address: special + ':0' }) })) } self.log.debug('[Server] mapping requested programs %s found', programsToMap.length) self.log.debug('[Server] asking rpc manager to power up the interface connections') await this._ccu.prepareInterfaces() self.log.debug('[Server] prepare cache') await this._ccu.prefillCache() self.log.debug('[Server] prepare object push') if ((channelsToMap.length > 0) || (variablesToMap.length > 0) || (programsToMap.length > 0) || (specialToMap.length > 0)) { this.powerUpBridges(lInstances, channelsToMap, variablesToMap, programsToMap, specialToMap) this.log.info('[Server] publishing accessories') this.publishAccessoriesToConfigurationService() this.publishBridgeInfosToConfigurationService() this.publishVirtualKeys() // Setup the Variable Update Event if (this._configuration.VariableUpdateEvent) { this._ccu.registerAddressForEventProcessingAtAccessory(this._configuration.VariableUpdateEvent, () => { // loop thru all ccu variable events self._ccu.updateRegisteredVariables() // this is the new method // self._variableAccessories.map(variableAccessory => { // variableAccessory.updateVariable() // }) }) } } else { this.log.warn('[Server] no channels to map in configuration found') // Power up the instances in dry mode this.powerUpBridges(lInstances, [], [], [], []) // ok we will need this for wizzard start this.publishAccessoriesToConfigurationService() this.publishBridgeInfosToConfigurationService() this.publishVirtualKeys() } this._buildCompatibleObjectList() this.reloadMode = false // we are done with the initialization so update the databases this._ccu.updateDatabases(this._configurationPath) // send all dps in use to the ccu this._ccu.processEventDatapoints() this.log.debug('[Server] initial DC fetch') await this._ccu.getCCUDutyCycle() this.log.debug('[Server] rebuild settings') this.rebuildClassSettings() } randomMac() { var mac = '12:34:56' for (var i = 0; i < 6; i++) { if (i % 2 === 0) mac += ':' mac += Math.floor(Math.random() * 16).toString(16) } return mac } generatePin() { let code = Math.floor(10000000 + Math.random() * 90000000) + '' code = code.split('') code.splice(3, 0, '-') code.splice(6, 0, '-') code = code.join('') return code } generateSetupID() { const chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ' const bytes = crypto.randomBytes(4) let setupID = '' for (var i = 0; i < 4; i++) { var index = bytes.readUInt8(i) % 26 setupID += chars.charAt(index) } return setupID } loadInstance(instId, instanceData, ccuDevices, variables, programs, special) { let self = this self.log.debug('Saved Pin Code is %s', instanceData.pincode) let pin = instanceData.pincode || this.generatePin() let clearedName = instanceData.name.replace(/[.:#_()-]/g, ' ') // Issue #73 let instanceName = 'HomeMatic_' + clearedName || 'HomeMatic_' + self.pad(instId, 2) let hkfname = 'HomeMatic ' + clearedName || 'HomeMatic ' + self.pad(instId, 2) let username = instanceData.user || self.randomMac() let setupID = instanceData.setupID || self.generateSetupID() var bridge if (this.isTestMode) { self.log.debug('[Server] powering up test bridge : %s with userid %s and pinCode ', hkfname, username, pin) bridge = new BridgeMock(hkfname, instId) } else { self.log.info('[Server] powering up bridge : %s with id %s , userid %s and pinCode ', hkfname, instId, username, pin) bridge = new Bridge(hkfname, instId) } // load Channels and Build accessories self.log.debug('[Server] loading devices for instance %s', instId) var hazObjects = true if (ccuDevices) { if (!self._loadAccessories(ccuDevices, instId, bridge, instanceData.publishDevices)) { hazObjects = false } } // load variables and Build accessories self.log.debug('[Server] loading variables for instance %s', instId) if (!self._loadVariables(variables, instId, bridge, instanceData.publishDevices)) { hazObjects = false } self.log.debug('[Server] loading programs for instance %s', instId) if (!self._loadPrograms(programs, instId, bridge, instanceData.publishDevices)) { hazObjects = false } self.log.debug('[Server] loading special devices for instance %s', instId) if (!self._loadSpecialDevices(special, instId, bridge, instanceData.publishDevices)) { hazObjects = false } if (!hazObjects) { this.log.debug('[Server] no devices in %s', instanceName) } let info = bridge.getService(Service.AccessoryInformation) bridge.instanceId = instId bridge.instanceName = hkfname bridge.roomId = instanceData.roomId || 0 info.setCharacteristic(Characteristic.Manufacturer, 'github.com/thkl') info.setCharacteristic(Characteristic.Model, `HAPHomematic`) info.setCharacteristic(Characteristic.SerialNumber, hkfname) info.setCharacteristic(Characteristic.FirmwareRevision, self.getVersion()) var publishInfo = { username: username, port: this.currentPortNum, pincode: pin, setupID: setupID, category: Accessory.Categories.BRIDGE, mdns: { multicast: true } } bridge.port = this.currentPortNum // Do not publish the bridge in test mode if (!this.isTestMode) { bridge.publish(publishInfo, false) } bridge.on('listening', port => { self.log.info('[Server] hap-homematic instance %s (%s) is running on port %s.', instanceName, username, port) }) bridge.on('identify', (paired, callback) => { self.log.info('[Server] identify %s %s', bridge.instanceId, paired ? '-paired-' : '-not paired-') callback() }) if (self.isTestMode) { self.log.debug('[Server] Your Bridge ID %s PinCode is %s', instanceName, publishInfo.pincode) } else { self.log.info('[Server] Your Bridge ID %s PinCode is %s', instanceName, publishInfo.pincode) } return bridge } getVersion() { let packageFile = path.join(__dirname, '..', 'package.json') this.log.debug('[Server] Check Version from %s', packageFile) if (fs.existsSync(packageFile)) { try { let packageData = JSON.parse(fs.readFileSync(packageFile)) this.log.debug('[Server] version is %s', packageData.version) return packageData.version } catch (e) { return 'no version found' } } else { return 'no version found' } } async loadSettings() { let self = this return new Promise((resolve, reject) => { let configFileName = path.join(this._configurationPath, 'config.json') this.log.debug('[Server] will load config from %s', configFileName) if (fs.existsSync(configFileName)) { try { let config = JSON.parse(fs.readFileSync(configFileName).toString()) // todo Validate the stuff if ((config) && (config.special)) { var validatedConfig = config this.log.info('[Server] validating special config') // make sure we have special devices only once var validatedSpecial = [] config.special.map(specialDevice => { if (validatedSpecial.indexOf(specialDevice) === -1) { validatedSpecial.push(specialDevice) } }) validatedConfig.special = validatedSpecial this._configuration = validatedConfig } else { this._configuration = config } } catch (e) { console.log(e) this.log.error('[Server] JSON Error in configuration file') let confgBackup = path.join(this._configurationPath, 'config_' + new Date().getTime() + '.backup') this.log.info('[Server] will start with a clean config. Your old file will be saved as %s maybe u are able to fix this', confgBackup) fs.renameSync(configFileName, confgBackup) fs.writeFileSync(configFileName, JSON.stringify({})) this._configuration = {} resolve() } } else { this.log.warn('[Server] configuration not found %s', configFileName) } this.log.info('[Server] config loading completed; start building servicetable') // load dynamic classes this.buildServiceList().then((cfg) => { self.serviceConfig = cfg self.log.info('[Server] service loading completed; publishing services') self.publishServiceTable() self.buildMappings(self._configuration.mappings, self.serviceConfig) resolve() }) }) } buildMappings(mappings, serviceConfig) { // merge service mappings from user settings with internal config if (mappings === undefined) { mappings = {} } Object.keys(serviceConfig).map(key => { // create a new List for this type if not yet into the list if (mappings[key] === undefined) { mappings[key] = [] } // add all services for this type serviceConfig[key].map(serviceName => { if (mappings[key].indexOf(serviceName) === -1) { mappings[key].push(serviceName) } }) }) return mappings } async publishServiceTable() { // this.log.debug('[Server] current Service table %s', JSON.stringify(this.serviceConfig)) if (this.configUI) { try { this.configUI.send({ topic: 'services', services: this.serviceConfig }) } catch (e) { this.log.error(e) } } else { this.log.debug('[Server] skipping service publishing to config server cause there is no config server (yet)') this.configpushwasSkipped = true } } async rebuildClassSettings() { let self = this self.log.debug('[Server] rebuild configuration as requested by enviroment change') await Promise.all(Object.keys(this.serviceConfig).map((classType) => { let clazzes = self.serviceConfig[classType] clazzes.map(async (serviceItem) => { let test = require(path.join(__dirname, 'services', serviceItem.serviceClazz)) let settings = await test.configurationItems(self._ccu) serviceItem.settings = settings }) }) ) this.publishServiceTable() } buildServiceList() { this.log.debug('[Server] build up service list') let self = this var serviceConfig = {} // this will loop thru all HomeMatic*Accessory.js files and get the ChannelTypes let sPath = path.join(__dirname, 'services') return new Promise((resolve, reject) => { fs.readdir(sPath, async (err, items) => { if (err) { self.log.eror('[Server] error while reading the service classes %s', err) reject(err) } self.log.debug('[Server] start loading services') await Promise.all( items.map(async (item) => { if (item.match(/HomeMatic.*Accessory.js/)) { self.log.debug('[Server] processing service file %s', item) let test = require(path.join(__dirname, 'services', item)) let serviceName = test.name let channelTypes = test.channelTypes() let priority = test.getPriority() let serviceDescription = test.serviceDescription() let filterDevice = test.filterDevice() let settings = await test.configurationItems(self._ccu) if ((channelTypes) && (channelTypes.length > 0)) { channelTypes.map(channelType => { if (serviceConfig[channelType] === undefined) { serviceConfig[channelType] = [] } serviceConfig[channelType].push({ serviceClazz: serviceName, settings: settings, priority: priority, description: serviceDescription, filterDevice: filterDevice }) }) self.log.debug('[Server] processing service file %s is done', item) } else { self.log.warn('[Server] WARNING There is no Channeltype declaration in %s::channelTypes', serviceName) } } else { self.log.debug('[Server] --- service file %s did not match HomeMatic*Accessory.js', item) } }) ) self.log.debug('[Server] sorting services via priority list') // sort the list by prioriy Object.keys(serviceConfig).map(key => { var list = serviceConfig[key] serviceConfig[key] = list.sort((a, b) => (a.priority > b.priority) ? 1 : -1) }) self.log.debug('[Server] done loading services') resolve(serviceConfig) }) }) } pad(num, size) { var s = num + '' while (s.length < size) s = '0' + s return s } saveAccessories() { let self = this Object.keys(self._publishedAccessories).forEach((advertiseAddress) => { let accessory = self._publishedAccessories[advertiseAddress] let fn = path.join(self._configurationPath, advertiseAddress + '.json') fs.writeFileSync(fn, JSON.stringify(accessory._dictionaryPresentation())) }) } addAccessory(accessory, bridge, publishToBridge) { let bridgeID = accessory.instanceID let acUID = accessory.getUUID() if (this._publishedAccessories[bridgeID + '_' + acUID] === undefined) { this.log.debug('[Server] adding accessory %s', accessory.getName()) this._publishedAccessories[bridgeID + '_' + acUID] = accessory } else { this.log.warn('[Server] warning a accessory with the same uuid %s as one added before', bridgeID + '_' + acUID) } // we add the accessory when the bridge is set to publish devices or its a non bridgedAccessory if ((publishToBridge === true) || (accessory.isBridgedAccessory() === false)) { if (accessory.isBridgedAccessory() === true) { this.log.debug('[Server] %s is a bridged accessory', accessory.getName()) try { bridge.addBridgedAccessory(accessory.getHomeKitAccessory()) bridge.hasPublishedDevices = true } catch (e) { this.log.error('[Server] unable to add accessory %s', e.stack) return false } } else { this.log.debug('[Server] %s is a single accessory on port %s', accessory.getName(), this.currentPortNum) accessory.publishSingleAccessory(this.currentPortNum) this.currentPortNum = this.currentPortNum + 1 } accessory.isPublished = true } return true } publishAccessoriesToConfigurationService() { var deviceData = [] var variableData = [] var programData = [] var specialDev = [] let self = this Object.keys(self._publishedAccessories).forEach((advertiseAddress) => { let accessory = self._publishedAccessories[advertiseAddress] // Sort the Appliances into diff arrays for WebUI switch (accessory.applianceType) { case ApplianceType.Variable: variableData.push(accessory._dictionaryPresentation()) break case ApplianceType.Program: programData.push(accessory._dictionaryPresentation()) break case ApplianceType.Special: specialDev.push(accessory._dictionaryPresentation()) break case ApplianceType.Device: deviceData.push(accessory._dictionaryPresentation()) break } }) if (this.configUI) { let message = { topic: 'serverdata', accessories: deviceData, variables: variableData, programs: programData, special: specialDev, variableTrigger: this._configuration.VariableUpdateEvent, autoUpdateVarTriggerHelper: this._configuration.autoUpdateVarTriggerHelper, rooms: this._ccu.getRooms(), logfile: this.log.getLogFile() } try { this.configUI.send(message) } catch (e) { this.log.error(e) } } } publishBridgeInfosToConfigurationService() { var data = [] this.log.debug('[Server] publish bridge info') this._bridges.map(bridge => { let bAcInfo = bridge._accessoryInfo data.push({ 'id': bridge.instanceId, 'user': bAcInfo.username, 'displayName': bAcInfo.displayName, 'pincode': bAcInfo.pincode, 'hasPublishedDevices': bridge.hasPublishedDevices, 'setupID': bAcInfo.setupID, 'roomId': bridge.roomId, 'port': bridge.port, 'setupURI': bridge.setupURI() }) }) if (this.configUI) { try { this.configUI.send({ topic: 'bridges', bridges: data }) } catch (e) { this.log.error(e) } } } publishVirtualKeys() { let result = [] let self = this this._ccu.getCCUDevices().map((device) => { if (((device.type === 'HmIP-RCV-50') || (device.type === 'HM-RCV-50')) && (device.channels) && (device.channels.length > 0)) { let usedInterface = self._ccu.getInterfaceWithID(device.channels[0].intf) device.ifName = (usedInterface) ? usedInterface.name : 'unknown' result.push(device) } }) if (this.configUI) { try { this.configUI.send({ topic: 'virtualKeys', virtualKeys: result }) } catch (e) { this.log.error(e) } } } shutdownBridgeInstances() { let self = this if (this._bridges) { this._bridges.map(bridge => { self.log.info('[Server] shutting down Homekit bridge %s', bridge.instanceName) bridge.unpublish() }) } Object.keys(self._publishedAccessories).forEach((advertiseAddress) => { self._publishedAccessories[advertiseAddress].getHomeKitAccessory().unpublish() self._publishedAccessories[advertiseAddress].shutdown() }) this._bridges = [] this._publishedAccessories = {} this._variableAccessories = [] this._specialAccessories = [] } _removeDevice(uuid) { let self = this let accessory = self._publishedAccessories[uuid] if (accessory) { accessory.removeData() } } sendSystemConfiguration() { } shutdown() { this.log.info('[Server] shutting down all homekit instances') this.shutdownBridgeInstances() setTimeout(() => { process.exit() }, 2000) this._ccu.shutdown() this.log.info('[Server] bye') } launchUIConfigurationServer() { let self = this process.env.UIX_CONFIG_PATH = this._configurationPath process.env.UIX_DEBUG = this.log.isDebugEnabled() this.configUI = childProcess.fork(path.resolve(__dirname, 'configurationsrv'), null, { env: process.env }) this.log.info('Spawning configuration service with PID', this.configUI.pid) this.configUI.on('message', (message) => { self._handleIncommingIPCMessage(message) }) } async _reloadAppliances() { if (this.reloadMode === true) { this.log.warn('[Server] skip reload currently running') return } this.reloadMode = true this.log.info('[Server] reloading all appliances') let self = this this.shutdownBridgeInstances() await this._ccu.disconnectInterfaces() setTimeout(() => { self.loadSettings() self.connectCCU() }, 1000) } _handleIncommingIPCMessage(message) { if ((message) && (message.topic)) { switch (message.topic) { case 'reloadApplicances': this._reloadAppliances() break case 'createTrigger': this._ccu.updateCCUVarTrigger(this._configuration.VariableUpdateEvent) break case 'refreshCache': this._ccu.updateDatabases(this._configurationPath) this._ccu.updateDeviceDatabase() break // tell the device to remove its history and persistent data case 'remove': this._removeDevice(message.uuid) // reload all stuff this._reloadAppliances() break case 'debug': this.log.setDebugEnabled(message.debug) if (this.configUI) { try { this.configUI.send({ topic: 'debug', debug: this.log.isDebugEnabled() }) } catch (e) { this.log.error(e) } } break case 'cfghello': this.log.debug('[Server] configuration server is alive') if (this.configpushwasSkipped === true) { this.log.debug('[Server] publish service table again since we have config server now') this.publishServiceTable() } break default: break } } } _findServiceClass(channel) { // First try settings address this.log.debug('[Server] try find serviceclazz for %s', channel.address) let mappings = this._configuration.mappings var sClass if (mappings) { sClass = mappings[channel.address] // Channel:Address Mapping is an object if (sClass) { if ((typeof sClass === 'object') && (sClass.Service)) { this.log.debug('[Server] service %s found thru address %s', sClass.Service, channel.address) return sClass.Service } } } sClass = mappings[channel.dtype + ':' + channel.type] if (sClass) { this.log.debug('[Server] mapping found %s:%s', channel.dtype, channel.type) if (typeof sClass === 'object') { if (Array.isArray(sClass)) { if (sClass[0].serviceClazz !== undefined) { this.log.debug('[Server] service %s found thru devicetype:channeltype %s:%s', sClass[0].serviceClazz, channel.dtype, channel.type) return sClass[0].serviceClazz } } else { if (sClass.serviceClazz !== undefined) { this.log.debug('[Server] service %s found thru devicetype:channeltype %s:%s', sClass.serviceClazz, channel.dtype, channel.type) return sClass.serviceClazz } } } } var sClassList = mappings[channel.type] if (sClassList) { this.log.debug('[Server] service %s found thru channeltype using the first service found %s', sClassList[0].serviceClazz, channel.type) return sClassList[0].serviceClazz } this.log.debug('[Server] nothing found for %s %s:%s', channel.address, channel.dtype, channel.type) return undefined } async _buildCompatibleObjectList() { // loop thru all devices an channels and try to find services this.log.debug('[Server] build compatible device list') let self = this if (!this.serviceConfig) { this.serviceConfig = await this.buildServiceList() } this.log.debug('[Server] .. services fetched') let supportedChannelList = Object.keys(this.serviceConfig) this.log.debug('[Server] .. %s supported channeltypes', supportedChannelList.length) this._compatibleDevices = [] var devAdrList = [] // this is only for indication that we add the device just once if (this._ccu.getCCUDevices()) { this.log.debug('[Server] got %s devices from your ccu', this._ccu.getCCUDevices().length) this._ccu.getCCUDevices().map(device => { device.channels.map(channel => { // first check DEVICETYPE:CHANNELTYPE if (supportedChannelList.indexOf(device.type + ':' + channel.type) > -1) { channel.isSuported = true if (devAdrList.indexOf(device.address) === -1) { devAdrList.push(device.address) self._compatibleDevices.push(device) } } if (supportedChannelList.indexOf(channel.type) > -1) { // get the device filter and make sure the given device is not in the list let item = self.serviceConfig[channel.type] item.map(sClass => { if (sClass.filterDevice.indexOf(device.type) === -1) { channel.isSuported = true if (devAdrList.indexOf(device.address) === -1) { devAdrList.push(device.address) self._compatibleDevices.push(device) } } }) } }) }) } else { this.log.error('[Server] unable to generate service list. CCU Devicelist is empty') } this.log.debug('[Server] ... devicelist completed') this._ccuVariables = this._ccu.getVariables() // Loop thru all variables an select the boolean ones this._ccuVariables.map(variable => { if (variable) { if (((variable.valuetype === 2) && (variable.subtype === 2)) || ((variable.valuetype === 4) && (variable.subtype === 0)) || ((variable.valuetype === 16) && (variable.subtype === 29)) ) { variable.isCompatible = true } else { variable.isCompatible = false } } }) this.log.debug('[Server] ..variables completed') // send the list to the configuration service if (this.configUI) { try { this.log.debug('[Server] send List %', this._compatibleDevices.length) this.configUI.send({ topic: 'compatibleObjects', devices: this._compatibleDevices, variables: this._ccuVariables, programs: this._ccu.getPrograms() }) } catch (e) { this.log.error(e) } } // oh boy ! if (this.isTestMode) { this.log.debug('[Server] %s compatible devices found', devAdrList.length) } else { this.log.info('[Server] %s compatible devices found', devAdrList.length) } } _loadVariables(variables, forInstanceID, bridge, publish) { let self = this let hazObjects = false Promise.all( variables.map(variable => { // Varaibles use there own Clazz let varObject = self._ccu.variableWithName(variable.name) if (varObject) { var instanceToAdd = uuid.generate('0') // default is Instance 0 let settings = self._configuration.mappings[variable.address] if ((settings !== undefined) && (settings.instance !== undefined)) { instanceToAdd = settings.instance } let sClass = (settings) ? (settings.Service || 'HomeMaticVariableAccessory') : 'HomeMaticVariableAccessory' let clazZFile = path.join(__dirname, 'services', sClass + '.js') // make sure we only initialize the channels for this instance if (self._deviceHazInstance(instanceToAdd, forInstanceID)) { self.log.debug('[Server] try adding variable %s to bridge %s using service %s', variable.name, forInstanceID, sClass) let Appliance = require(clazZFile) if (settings === undefined) { settings = {} } let accessory = new Appliance(variable, 'Variable', self, settings) // save the variable accessory.variable = varObject accessory.nameInCCU = varObject.name accessory.init() accessory.instanceID = forInstanceID accessory.serviceClass = sClass accessory.settings = settings accessory.applianceType = ApplianceType.Variable this.addAccessory(accessory, bridge, publish) this._variableAccessories.push(accessory) hazObjects = true } } else { self.log.warn('[Server] variable with name %s not found at ccu', variable.name) } }) ) return hazObjects } _loadSpecialDevices(special, forInstanceID, bridge, publish) { let self = this let hazObjects = false self.log.debug('[Server] start adding special devices') Promise.all( special.map(spDevice => { // Varaibles use there own Clazz var instanceToAdd = uuid.generate('0') // default is Instance 0 let settings = self._configuration.mappings[spDevice.address] if ((settings !== undefined) && (settings.instance !== undefined)) { instanceToAdd = settings.instance self.log.debug('[Server] setup %s to add to bridge %s completed', settings.name, forInstanceID) } else { self.log.warn('[Server] did not find settings for special device with id %s', spDevice.address) } if (settings) { let sClass = settings.Service let clazZFile = path.join(__dirname, 'services', sClass + '.js') if (self._deviceHazInstance(instanceToAdd, forInstanceID)) { let Appliance = require(clazZFile) if (settings === undefined) { settings = {} } let accessory = new Appliance(spDevice, 'Special', self, settings) accessory.init() accessory.instanceID = forInstanceID accessory.serviceClass = sClass accessory.settings = settings accessory.applianceType = ApplianceType.Special self.log.debug('[Server] try adding special device %s to bridge %s', settings.name, forInstanceID) this.addAccessory(accessory, bridge, publish) hazObjects = true } } else { self.log.debug('[Server] looks like there are no settings for %s', special) } }) ) return hazObjects } _loadPrograms(programs, forInstanceID, bridge, publish) { let self = this let hazObjects = false let sClass = 'HomeMaticProgramAccessory' let clazZFile = path.join(__dirname, 'services', sClass + '.js') Promise.all( programs.map(program => { // Varaibles use there own Clazz var instanceToAdd = uuid.generate('0') // default is Instance 0 let settings = self._configuration.mappings[program.address] if ((settings !== undefined) && (settings.instance !== undefined)) { instanceToAdd = settings.instance } if (self._deviceHazInstance(instanceToAdd, forInstanceID)) { self.log.debug('[Server] try adding %s to bridge %s', program.name, forInstanceID) let Appliance = require(clazZFile) if (settings === undefined) { settings = {} } let accessory = new Appliance(program, 'Program', self, settings) accessory.applianceType = ApplianceType.Program accessory.nameInCCU = program.name accessory.init() accessory.instanceID = forInstanceID accessory.serviceClass = sClass accessory.settings = settings this.addAccessory(accessory, bridge, publish) hazObjects = true } }) ) return hazObjects } _deviceHazInstance(instances, search) { if (typeof instances === 'string') { return (instances === search) } else { return (instances.indexOf(search) > -1) } } _loadAccessories(channelList, forInstanceID, bridge, publish) { let self = this var hazObjects = false Promise.all( channelList.map(channel => { let settings = self._configuration.mappings[channel.address] var instanceToAdd = uuid.generate('0') // default is Instance 0 // if there are settings ... use instances from settings if ((settings !== undefined) && (settings.instance !== undefined)) { instanceToAdd = settings.instance } // make sure we only initialize the channels for this instance if (self._deviceHazInstance(instanceToAdd, forInstanceID)) { let sClass = self._findServiceClass(channel) if (sClass) { self.log.debug('[Server] try adding %s to bridge %s', channel.name, forInstanceID) let oInterface = self._ccu.getInterfaceWithID(channel.intf) if (oInterface) { oInterface.inUse = true if (!self.isTestMode) { self.log.info('[Server] service used for %s is %s', channel.name, sClass) } else { self.log.debug('[Server-TestMode] service used for %s is %s', channel.name, sClass) } // Init the service class and create a appliance object try { let clazZFile = path.join(__dirname, 'services', sClass + '.js') if (fs.existsSync(clazZFile)) { let Appliance = require(clazZFile) let channelTypes = Appliance.channelTypes() let filterDevice = Appliance.filterDevice() if (((channelTypes.indexOf(channel.type) > -1) || (channelTypes.indexOf(channel.dtype + ':' + channel.type) > -1)) && // additional check the device type should not been in the filterDevice list (filterDevice.indexOf(channel.dtype) === -1)) { let accessory = new Appliance(channel, oInterface.name, self, settings) accessory.init() accessory.instanceID = forInstanceID accessory.serviceClass = sClass accessory.settings = settings accessory.applianceType = ApplianceType.Device // add the accessory to the bridge if (publish === true) { self.log.info('[Server] add %s to bridge instance %s', channel.name, forInstanceID) } if (self.addAccessory(accessory, bridge, publish)) { hazObjects = true } else { self.log.error('[Server] unable to initialize %s see previous error', channel.name) } } else { self.log.error('[Server] unable to initialize %s', channel.name) self.log.error('[Server] requested ServiceClass %s did not provide a service for %s', sClass, channel.type) } } else { self.log.error('Unable to initialize %s file %s was not found.', channel.name, clazZFile) } } catch (e) { self.log.error('Unable to initialize %s Error is %s', channel.name, e.stack) } } else { self.log.warn('[Server] Interface %s not found in ccu manager', channel.intf) } } else { self.log.warn('[Server] there is no known service for %s:%s on channel %s', channel.dtype, channel.type, channel.address) } } else { // not my instance } }) )