hap-homematic
Version:
provides a homekit bridge to the ccu
1,131 lines (1,032 loc) • 44 kB
JavaScript
/*
* File: HomeMaticCCU.js
* Project: hap-homematic
* File Created: Saturday, 7th March 2020 2:20:39 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 path = require('path')
const fs = require('fs')
const Rega = require(path.join(__dirname, 'HomeMaticRegaRequest.js'))
const HomeMaticRPC = require(path.join(__dirname, 'HomeMaticRPC.js'))
const url = require('url')
const EventEmitter = require('events')
const http = require('http')
class HomeMaticCCU extends EventEmitter {
constructor(log, configuration) {
super()
this.log = log
this.interfaces = {}
this.sectionChannelIds = []
this.cachedValues = {}
this.eventCallbacks = {}
this.variableCallbacks = {}
this.configuration = configuration
this.eventDataPoints = []
this.ccuIP = configuration.ccuIP || '127.0.0.1'
this.ccuDCDevices = {}
this.aggroCache = configuration.forceCache
this.log.debug('[CCU] init CCU Connections for %s', this.ccuIP)
this.portRpl = { 'BidCos-RF': 2001, 'VirtualDevices': 9292, 'HmIP-RF': 2010 }
this.configurationPath = configuration.storagePath
}
init() {
}
prepareConnections() {
let self = this
this.log.debug('[CCU] preparing connections cleaning event table')
this.eventCallbacks = {}
return new Promise((resolve, reject) => {
self._fetchInterfaces().then(o => {
resolve()
})
})
}
async pingRega() {
this.log.info('[CCU] check Rega is alive')
return new Promise((resolve, reject) => {
let rega = new Rega(this.log, this.ccuIP)
try {
rega.script('Write("Pong");').then(result => {
resolve(result === 'Pong')
}).catch(e => {
reject(e)
})
} catch (e) {
resolve(e)
}
})
}
getChannelByAddress(chAddress) {
var result
let dl = this.getCCUDevices()
for (var di = 0; di < dl.length; di++) {
var device = dl[di]
if ((device.address) && (device.address.length > 1) && (chAddress.indexOf(device.address) > -1)) {
for (var ci = 0; ci < device.channels.length; ci++) {
var channel = device.channels[ci]
if (channel.address === chAddress) {
channel.dtype = device.type
channel.dname = device.name
result = channel
break
}
}
}
}
// removed message cause special devices will falsely trigger this error
// if (result === undefined) {
// this.log.error('[CCU] no channel found for %s', chAddress)
// }
return result
}
getCCUDevices() {
return this.unescapeDevices(this.devices || [])
}
getVariables() {
return this.variables || []
}
getPrograms() {
return this.programs || []
}
getRooms() {
return this.rooms || []
}
variableWithName(varName) {
var result
this.getVariables().map(variable => {
if (variable.name === varName) {
result = variable
}
})
return result
}
unescapeDevices(devices) {
let self = this
if (devices) {
try {
devices.forEach(device => {
device.name = unescape(device.name)
device.channels.forEach(channel => {
try {
channel.name = unescape(channel.name)
} catch (e) {
self.log.error('decoding error channel name %s', device.name)
}
})
})
} catch (e) {
self.log.error('decoding error for %s', JSON.stringify(devices))
}
}
return devices
}
unescapeVariables(variables) {
let self = this
if (variables) {
try {
variables.forEach(variable => {
variable.name = unescape(variable.name)
variable.dpInfo = unescape(variable.dpInfo)
variable.valuelist = unescape(variable.valuelist)
variable.unit = unescape(variable.unit)
})
} catch (e) {
self.log.error('decoding error for %s', JSON.stringify(variables))
}
}
return variables
}
async loadDatabases(configurationPath, dryRun = false) {
let self = this
this.configurationPath = configurationPath
this.log.info('[CCU] loading databases from %s', this.configurationPath)
this.devices = await this.loadObjectDatabase(path.join(configurationPath, 'devices.json'), 'devices', dryRun, async () => {
let fetchedDevices = await self.fetchAllDevices()
let result = self.unescapeDevices(fetchedDevices.devices)
self.devices = result
return result
})
this.log.info('[CCU] device database loaded %s devices found', (this.devices) ? this.devices.length : 0)
// since we are using uriencode in the script we have to unescape the names again
this.devices = this.unescapeDevices(this.devices)
let variables = await this.loadObjectDatabase(path.join(configurationPath, 'variables.json'), 'variables', dryRun, async () => {
let result = await self.fetchVariables()
return self.unescapeVariables(result);
})
this.variables = self.unescapeVariables(variables);
if (this.variables === undefined) {
this.variables = []
}
this.log.info('[CCU] variable database loaded %s variables found', (this.variables) ? this.variables.length : 0)
this.programs = await this.loadObjectDatabase(path.join(configurationPath, 'programs.json'), 'programs', dryRun, async () => {
let result = await self.fetchPrograms()
return result
})
if (this.programs === undefined) {
this.programs = []
}
this.log.info('[CCU] program database loaded %s programs found', (this.programs) ? this.programs.length : 0)
this.rooms = await this.loadObjectDatabase(path.join(configurationPath, 'rooms.json'), 'rooms', dryRun, async () => {
let result = await self.fetchRooms()
return result
})
if (this.rooms === undefined) {
this.rooms = []
}
this.log.info('[CCU] room database loaded %s rooms found', (this.rooms) ? this.rooms.length : 0)
}
loadObjectDatabase(databasePath, type, dryRun, fetchFunction) {
let self = this
this.log.info('[CCU] loading object database for %s from %s', type, databasePath)
return new Promise((resolve, reject) => {
if (!fs.existsSync(databasePath)) {
if (!dryRun) {
self.log.error('[CCU] databse not found get a new one')
self.updateObjectDatabase(databasePath, fetchFunction).then((result) => {
resolve(result[type])
})
} else {
self.log.warn('[CCU] test mode do not fetch anything')
resolve(undefined)
}
} else {
var oDb
try {
oDb = JSON.parse(fs.readFileSync(databasePath))
} catch (e) {
self.log.warn('[CCU] unable to parse cached database.')
}
if (oDb) {
let result = oDb[type]
if (result) {
self.log.info('[CCU] object database loaded %s objects found', result.length)
}
resolve(result)
} else {
if (!dryRun) {
self.log.error('[CCU] unable to load object database will get a new one')
self.updateObjectDatabase(databasePath, fetchFunction).then((result) => {
resolve(result[type])
})
} else {
self.log.warn('[CCU] test mode do not fetch anything')
resolve(undefined)
}
}
}
})
}
updateObjectDatabase(databasePath, fetchFunction) {
return new Promise((resolve, reject) => {
fetchFunction().then(oResult => {
fs.writeFile(databasePath, JSON.stringify(oResult, ' ', 2), () => {
resolve(oResult)
})
})
})
}
async updateDeviceDatabase(configuratonPath) {
if ((this.deviceDBUpdateRunning) || (this.dryRun)) {
this.log.debug('[CCU] updateDeviceDatabase is running skip other call')
return
}
let self = this
this.deviceDBUpdateRunning = true
var result = await this.updateObjectDatabase(path.join(this.configurationPath, 'devices.json'), async () => {
let result = await self.fetchAllDevices()
self.deviceDBUpdateRunning = false
return result
})
this.devices = result.devices
this.emit('devicelistchanged', null)
this.deviceDBUpdateRunning = false
}
async updateDatabases(databasePath) {
let self = this
var result
result = await this.updateObjectDatabase(path.join(databasePath, 'variables.json'), async () => {
let result = await self.fetchVariables()
return result
})
this.variables = self.unescapeVariables(result.variables)
result = await this.updateObjectDatabase(path.join(databasePath, 'programs.json'), async () => {
let result = await self.fetchPrograms()
return result
})
this.programs = result.programs
result = await this.updateObjectDatabase(path.join(databasePath, 'rooms.json'), async () => {
let result = await self.fetchRooms()
return result
})
this.rooms = result.rooms
}
async shutdown() {
this.log.debug('[CCU] shutdown')
await this.disconnectInterfaces()
this.log.debug('[CCU] shutdown completed')
}
async disconnectInterfaces() {
return new Promise(async (resolve, reject) => {
if (this.rpc) {
await this.rpc.disconnectInterfaces()
this.rpc.resetInterfaces()
this.rpc.removeAllListeners('event')
}
resolve()
})
}
fetchAllDevices() {
let self = this
return new Promise((resolve, reject) => {
this.eventaddresses = []
var script = '!devices\nstring sDeviceId;string sChannelId;boolean df = true;Write(\'{"devices":[\');foreach(sDeviceId, root.Devices().EnumIDs()){object oDevice = dom.GetObject(sDeviceId);if(oDevice){if(df) {df = false;} else { Write(\',\');}Write(\'{\');'
script = script + self._scriptPartForElement('id', 'sDeviceId', 'number', ',')
script = script + self._scriptPartForElement('name', 'oDevice.Name().UriEncode()', 'urlstring', ',')
script = script + self._scriptPartForElement('address', 'oDevice.Address()', 'string', ',')
script = script + self._scriptPartForElement('type', 'oDevice.HssType()', 'string', ',')
script = script + 'Write(\'"channels": [\');boolean bcf = true;foreach(sChannelId, oDevice.Channels().EnumIDs()){object oChannel = dom.GetObject(sChannelId);'
script = script + 'if(bcf) {bcf = false;} else {Write(\',\');}Write(\'{\');'
script = script + self._scriptPartForElement('id', 'sChannelId', 'number', ',')
script = script + self._scriptPartForElement('name', 'oChannel.Name().UriEncode()', 'urlstring', ',')
script = script + self._scriptPartForElement('intf', 'oDevice.Interface()', 'number', ',')
script = script + self._scriptPartForElement('address', 'oChannel.Address()', 'string', ',')
script = script + self._scriptPartForElement('type', 'oChannel.HssType()', 'string', ',')
script = script + self._scriptPartForElement('access', 'oChannel.UserAccessRights(iulOtherThanAdmin)', 'number')
script = script + 'Write(\'}\');}Write(\']}\');}}Write(\']\');'
script += 'var s = dom.GetObject("'
script += this.subsection
script += '");string cid;boolean sdf = true;if (s) {Write(\',"subsection":[\');foreach(cid, s.EnumUsedIDs()){ '
script += ' if(sdf) {sdf = false;}'
script += ' else { Write(\',\');}Write(cid);}Write(\']\');}'
script += 'Write(\'}\');'
let rega = new Rega(self.log, self.ccuIP)
rega.script(script).then(devices => {
let oDevices = self.parseResult(devices)
resolve(oDevices)
})
})
}
fetchVariables() {
let self = this
return new Promise((resolve, reject) => {
var script = '!variables\nstring varid;boolean df = true;Write(\'{"variables":[\');foreach(varid, dom.GetObject(ID_SYSTEM_VARIABLES).EnumIDs()){object ovar = dom.GetObject(varid);if(df) {df = false;} else { Write(\',\');}Write(\'{\')'
script = script + self._scriptPartForElement('id', 'varid', 'number', ',')
script = script + self._scriptPartForElement('name', 'ovar.Name().UriEncode()', 'urlstring', ',')
script = script + self._scriptPartForElement('dpInfo', 'ovar.DPInfo().UriEncode()', 'urlstring', ',')
script = script + self._scriptPartForElement('unerasable', 'ovar.Unerasable()', 'string', ',')
script = script + self._scriptPartForElement('valuetype', 'ovar.ValueType()', 'number', ',')
script = script + self._scriptPartForElement('subtype', 'ovar.ValueSubType()', 'number', ',')
script = script + self._scriptPartForElement('minvalue', 'ovar.ValueMin()', 'string', ',')
script = script + self._scriptPartForElement('maxvalue', 'ovar.ValueMax()', 'string', ',')
script = script + self._scriptPartForElement('valuelist', 'ovar.ValueList().UriEncode()', 'string', ',')
script = script + self._scriptPartForElement('unit', 'ovar.ValueUnit().UriEncode()', 'urlstring', '')
script = script + 'Write(\'}\');} Write(\']}\');'
let rega = new Rega(self.log, self.ccuIP)
rega.script(script).then(variables => {
let oVariables = self.parseResult(variables)
resolve(oVariables)
})
})
}
fetchPrograms() {
let self = this
return new Promise((resolve, reject) => {
var script = '!programs\nstring prgid;boolean df = true;Write(\'{"programs":[\');foreach(prgid, dom.GetObject(ID_PROGRAMS).EnumIDs()){object oprg = dom.GetObject(prgid);if(df) {df = false;} else { Write(\',\');}Write(\'{\')'
script = script + self._scriptPartForElement('id', 'prgid', 'number', ',')
script = script + self._scriptPartForElement('name', 'oprg.Name()', 'urlstring', ',')
script = script + self._scriptPartForElement('dpInfo', 'oprg.PrgInfo()', 'urlstring', '')
script = script + 'Write(\'}\');} Write(\']}\');'
let rega = new Rega(self.log, self.ccuIP)
rega.script(script).then(programs => {
let oPrograms = self.parseResult(programs)
resolve(oPrograms)
})
})
}
fetchRooms() {
let self = this
return new Promise((resolve, reject) => {
var script = '!rooms\nstring rid;boolean df = true;Write(\'{"rooms":[\');foreach(rid, dom.GetObject(ID_ROOMS).EnumIDs()){object oRoom = dom.GetObject(rid);if(df) {df = false;} else { Write(\',\');}Write(\'{\')'
script = script + self._scriptPartForElement('id', 'rid', 'number', ',')
script = script + self._scriptPartForElement('name', 'oRoom.Name()', 'urlstring', ',')
script = script + self._scriptPartForElement('channels', 'oRoom.EnumUsedIDs()', 'enumeration', '')
script = script + 'Write(\'}\');} Write(\']}\');'
let rega = new Rega(self.log, self.ccuIP)
rega.script(script).then(strRooms => {
let oRooms = self.parseResult(strRooms)
resolve(oRooms)
})
})
}
fetchDevices() {
let self = this
return new Promise((resolve, reject) => {
// first build a \t string from channel IDs
let cList = this.sectionChannelIds.join('\t')
var script = 'string cid;boolean df=true;'
script = script + 'string list = \'' + cList + '\';'
script = script + 'Write(\'{"channels":[\');foreach(cid, list){object oCh = dom.GetObject(cid);var did = oCh.Device(); if (did) {object oDe = dom.GetObject(did);} if(df) {df = false;} else { Write(\',\');}Write(\'{\')'
script = script + self._scriptPartForElement('id', 'cid', 'number', ',')
script = script + self._scriptPartForElement('name', 'oCh.Name()', 'string', ',')
script = script + self._scriptPartForElement('type', 'oCh.HssType()', 'string', ',')
script = script + self._scriptPartForElement('intf', 'oCh.Interface()', 'number', ',')
script = script + self._scriptPartForElement('dtype', 'oDe.HssType()', 'string', ',')
script = script + self._scriptPartForElement('dname', 'oDe.Name()', 'string', ',')
script = script + self._scriptPartForElement('address', 'oCh.Address()', 'string', ',')
script = script + self._scriptPartForElement('access', 'oChannel.UserAccessRights(iulOtherThanAdmin)', 'number', '')
script = script + 'Write(\'}\');} Write(\']}\');'
let rega = new Rega(self.log, self.ccuIP)
rega.script(script).then(channels => {
if (channels) {
let oChannels = self.parseResult(channels)
self.channels = oChannels.channels
resolve(self.channels)
} else {
reject(new Error('unable to fetch devices'))
}
})
})
}
/*
var script = 'Write("["");var i=dom.GetObject(41);if(i.State()>0){var s=dom.GetObject(ID_SERVICES);string sid;foreach(sid,s.EnumIDs()){var o=dom.GetObject(sid);if (o.AlState()==asOncoming){'
script = script + 'Write(o.ID()#'\\t'#o.Name()#'\\t'#o.Timestamp());}}}'
script = script + 'Write("]");
*/
parseResult(strJson) {
try {
return JSON.parse(strJson)
} catch (e) {
this.log.error('[CCU] Error while parsing json %s - (str is %s)', e, strJson)
return {}
}
}
hazDatapoint(dpName) {
let self = this
if (typeof dpName === 'object') {
dpName = dpName.address()
}
return new Promise((resolve, reject) => {
let script = 'Write(\'{"result":\');var x = dom.GetObject("' + dpName + '");if (x) {Write("true");}else{Write("false");}Write("}");'
let rega = new Rega(self.log, self.ccuIP)
rega.script(script).then(data => {
var hdp = false
try {
let obj = JSON.parse(data)
if ((obj) && (obj.result === true)) {
hdp = true
}
} catch (e) {
}
self.log.debug('[CCU] check HazDP %s result is %s', dpName, hdp)
resolve(hdp)
})
})
}
setValue(address, newValue) {
let self = this
return new Promise((resolve, reject) => {
let script = 'object o = dom.GetObject(\'' + address + '\');if (o){'
if (typeof newValue === 'string') {
script = script + 'o.State(\'' + newValue + '\');}'
} else {
script = script + 'o.State(' + newValue + ');}'
}
let rega = new Rega(self.log, self.ccuIP)
rega.script(script).then((r) => {
self.setCache(address, newValue)
resolve(r)
})
})
}
getValue(address, ignoreCache = false) {
var result
let self = this
return new Promise((resolve, reject) => {
if ((ignoreCache === false) || (self.aggroCache === true)) {
result = this.getCache(address)
}
if ((result === undefined) || (result === 'undefined')) {
self.log.debug('[CCU] ask Rega %s', address)
let script = 'object o = dom.GetObject(\'' + address + '\');if (o){Write(o.Value());}'
let rega = new Rega(self.log, self.ccuIP)
rega.script(script).then((regaResult) => {
self.setCache(address, regaResult)
self.fireEvent(address, regaResult)
resolve(regaResult)
})
} else {
self.log.debug('[CCU] %s return cached value %s', address, result)
resolve(result)
}
})
}
getVariableValue(variable) {
let self = this
return new Promise((resolve, reject) => {
let script = 'object o = dom.GetObject(ID_SYSTEM_VARIABLES).Get(\'' + variable + '\');if (o){Write(o.State());}'
let rega = new Rega(self.log, self.ccuIP)
rega.script(script).then((regaResult) => {
self.log.debug('[CCU] %s return variable value %s', variable, regaResult)
self.fireVariableEvent(variable, regaResult)
resolve(regaResult)
})
})
}
setVariable(variable, value) {
let self = this
return new Promise((resolve, reject) => {
let script = 'object o = dom.GetObject(ID_SYSTEM_VARIABLES).Get(\'' + variable + '\');if (o){Write(o.State(' + value + '));} else {Write(\'variable not found by rega\');}'
let rega = new Rega(self.log, self.ccuIP)
rega.script(script).then((regaResult) => {
resolve(regaResult)
})
})
}
runProgram(programName) {
let self = this
return new Promise((resolve, reject) => {
let script = 'object o = dom.GetObject(ID_PROGRAMS).Get(\'' + programName + '\');if (o){Write(o.ProgramExecute());} else {Write(\'program not found by rega\');}'
let rega = new Rega(self.log, self.ccuIP)
rega.script(script).then((regaResult) => {
if (regaResult.indexOf('program not found by') !== -1) {
self.log.error('Unable to launch %s program was not found by rega', programName)
}
resolve(regaResult)
})
})
}
setCache(address, newValue) {
this.cachedValues[address] = newValue
}
removeCache(address) {
this.cachedValues[address] = undefined
}
getCache(address) {
return this.cachedValues[address]
}
getInterfaceWithID(interfaceId) {
return this.interfaces[interfaceId]
}
getInterfaceWithName(interfaceName) {
var result
let self = this
Object.keys(this.interfaces).map(ifId => {
let oInteface = self.interfaces[ifId]
if (oInteface.name === interfaceName) {
result = oInteface
}
})
return result
}
sendInterfaceCommand(interfaceId, command, parameters) {
if (this.rpc) {
this.log.debug('[CCU] sendInterfaceCommand %s %s', interfaceId, command)
return this.rpc.sendInterfaceCommand(interfaceId, command, parameters)
}
}
async updateCCUVarTrigger(triggerDataPoint) {
let self = this
let varList = Object.keys(this.variableCallbacks)
this.log.debug('[CCU] updateCCUVarTrigger get datapoint and channel ids %s', triggerDataPoint)
var getIDMessage = 'object x = dom.GetObject(\'' + triggerDataPoint + '\');Write(\'{"DpId":\' # x.ID() # \',\');Write(\'"ChId":\' # x.Channel() # \'}\');'
let rega = new Rega(this.log, this.ccuIP)
let getResult = await rega.script(getIDMessage)
if (getResult) {
try {
let ids = JSON.parse(getResult)
let channelID = ids.ChId
let channelDpId = ids.DpId
let tmpProg = '_hap_autotrigger_'
// Core Block
var regaMessage = 'object oPTmp = dom.GetObject( ID_PROGRAMS );'
regaMessage = regaMessage + 'object program = dom.GetObject("' + tmpProg + '");'
regaMessage = regaMessage + 'if (program) {'
regaMessage = regaMessage + ' dom.DeleteObject(program);'
regaMessage = regaMessage + '}'
regaMessage = regaMessage + 'program = dom.CreateObject(OT_PROGRAM);'
regaMessage = regaMessage + 'program.PrgInfo("This program will autotrigger the variable updater for hap-homematic.");'
regaMessage = regaMessage + 'program.Name("' + tmpProg + '");'
regaMessage = regaMessage + 'boolean bF1 = oPTmp.Add(program.ID());'
regaMessage = regaMessage + 'object rule = program.Rule();'
regaMessage = regaMessage + 'object destn = rule.RuleDestination();'
regaMessage = regaMessage + 'object n_condition;'
regaMessage = regaMessage + 'if (rule.RuleConditions().Count()>0) {'
regaMessage = regaMessage + 'n_condition = rule.RuleConditions(0);'
regaMessage = regaMessage + '} else {'
regaMessage = regaMessage + 'n_condition = rule.RuleAddCondition();'
regaMessage = regaMessage + '}'
regaMessage = regaMessage + 'n_condition.CndOperatorType(2);'
regaMessage = regaMessage + 'object s_cond;'
// Destination Block
regaMessage = regaMessage + 'object dest = destn.DestAddSingle();'
regaMessage = regaMessage + 'dest.DestinationParam(ivtObjectId);'
regaMessage = regaMessage + 'dest.DestinationChannel(' + channelID + ');'
regaMessage = regaMessage + 'dest.DestinationDP(' + channelDpId + ');'
regaMessage = regaMessage + 'dest.DestinationValueType(ivtBinary);'
regaMessage = regaMessage + 'dest.DestinationValue(1);'
// loop thru all variables
varList.map((variable) => {
let oVar = self.variableWithName(variable)
self.log.debug('[CCU] add variable condition : %s', JSON.stringify(oVar))
if ((oVar) && (oVar.id)) {
if ((oVar.valuetype === 4) || (oVar.valuetype === 2) || (oVar.valuetype === 16)) { // ivtFloat - 4 ivtBinary - 2 ivtInteger - 16
let regaCheck = ''
switch (oVar.subtype) {
case 2: // istBool
case 6: // istAlarm
case 23: // istPresent
self.log.debug('[CCU] adding boolean checks')
regaCheck = regaCheck + this.getVariableCondition(oVar.id, 9, 0) // 9 >=
regaCheck = regaCheck + this.getVariableCondition(oVar.id, 9, 1)
break
case 29: // istEnum
// loop thru all values
self.log.debug('[CCU] adding choice checks')
if (oVar.valuelist) {
let sz = oVar.valuelist.split(';').length
for (let i = 0; i < sz; i++) {
regaCheck = regaCheck + this.getVariableCondition(oVar.id, undefined, i)
}
}
break
case 0: // istGeneric
self.log.debug('[CCU] adding generic number checks')
regaCheck = regaCheck + this.getVariableCondition(oVar.id, 9, oVar.minvalue) // 9 >=
break
}
regaMessage = regaMessage + regaCheck
} else {
regaMessage = regaMessage + this.getVariableCondition(oVar.id, undefined, '""')
}
}
})
// and send regaMessage
regaMessage = regaMessage + 'program.Active(true);'
regaMessage = regaMessage + 'dom.RTUpdate(0);'
await rega.script(regaMessage)
} catch (e) {
this.log.error(e)
}
} else {
this.log.debug('[CCU] updateCCUVarTrigger unable to get datapointids')
}
}
getVariableCondition(varId, conditionType, comparison) {
let regaMessage = ''
regaMessage = regaMessage + 's_cond = n_condition.CndAddSingle();'
regaMessage = regaMessage + 's_cond.OperatorType(2);'
if (conditionType) {
regaMessage = regaMessage + 's_cond.ConditionType(' + conditionType + ');'
}
regaMessage = regaMessage + 's_cond.ConditionType2(13);'
regaMessage = regaMessage + 's_cond.LeftValType(19);'
regaMessage = regaMessage + 's_cond.ConditionChannel(65535);'
regaMessage = regaMessage + 's_cond.LeftVal(' + varId + ');'
regaMessage = regaMessage + 's_cond.RightVal1(' + comparison + ');'
return regaMessage
}
async getCCUDutyCycle(onlyReturnDevices) {
// first get the BidCos-RF Interface
var result = {}
this.log.debug('[CCU] fetching dutycycle for BidCos-RF')
if (onlyReturnDevices) {
this.log.debug('[CCU] just return previously saved interfaces %s', JSON.stringify(this.ccuDCDevices))
return this.ccuDCDevices
}
// if there are no interface infos ... fetch them
if (Object.keys(this.interfaces).length === 0) {
await this._fetchInterfaces()
}
let bci = this.getInterfaceWithName('BidCos-RF')
if (bci) {
this.log.debug('[CCU] BidCos-RF found sending listBidcosInterfaces command')
let lbIResult = await this.sendInterfaceCommand(bci.name, 'listBidcosInterfaces', [])
this.log.debug('[CCU] BidCos-RF listBidcosInterfaces result %s', JSON.stringify(lbIResult))
if (lbIResult) {
lbIResult.map(hwIf => {
result[hwIf.ADDRESS] = hwIf.DUTY_CYCLE
})
}
this.ccuDCDevices = result
this.log.debug('[CCU] save DC Interfaces %s', JSON.stringify(this.ccuDCDevices))
}
return result
}
/**
* depending on debugging the monitoring service will be enabled if user has set the flag for that
*/
processMonitoring() { // if we are in debug remove the monitor
if (this.ccuIP === '127.0.0.1') {
if (this.log.isDebugEnabled()) {
this.log.info('[CCU] skip Monitoring as we are in debug')
this._removemonitconfig()
this.log.info('[CCU] monit config removed')
} else {
if ((this.configuration) && (this.configuration.enableMonitoring === true)) {
this.log.info('[CCU] enable Monitoring')
this._buildmonitconfig()
} else {
this.log.info('[CCU] disable Monitoring')
this._removemonitconfig()
}
}
} else {
this.log.info('ignore monitoring cause seems to be a remote ccu')
}
}
prepareInterfaces() {
let self = this
if (this.configuration.interfaceWatchdog === undefined) {
this.configuration.interfaceWatchdog = 300
}
self.log.debug('[CCU] creating eventserver (%s)', this.configuration.interfaceWatchdog)
// create an rpc server if not in use yet
if (!this.rpc) {
this.rpc = new HomeMaticRPC(this, 9875)
this.rpc.init(parseInt(this.configuration.interfaceWatchdog))
this.processMonitoring()
}
self.log.debug('[CCU] adding %s interfaces to rpc manager', Object.keys(this.interfaces).length)
Object.keys(this.interfaces).map(ifId => {
let oInteface = self.interfaces[ifId]
if (oInteface.inUse === true) {
let iUrl = oInteface.url.replace('xmlrpc://', 'http://').replace('xmlrpc_bin://', 'http://')
self.log.debug('If URL is %s', iUrl)
let oUrl = url.parse(iUrl)
let port = oUrl.port
let hostname = oUrl.hostname
self.log.debug('If Host %s Port %s Path %s', hostname, port, oUrl.pathname)
if (self.ccuIP !== '127.0.0.1') {
self.log.debug('[CCU] we are remote so change the host and port')
hostname = self.ccuIP
port = self.portRpl[oInteface.name] || port
} else {
self.log.debug('[CCU] local ccu so nothing will be replaced')
}
self.log.debug('[CCU] adding interface %s with id %s', oInteface.name, ifId)
self.rpc.addInterface(oInteface.name, hostname, port, oUrl.pathname)
} else {
self.log.info('[CCU] interface %s seems not to be in use', oInteface.name)
}
})
this.rpc.on('event', (event) => {
self.log.debug('[CCU] event %s', event.address)
self.setCache(event.address, event.value)
self.fireEvent(event.address, event.value)
})
this.rpc.on('newDevices', () => {
if (!self.dryRun) {
self.log.debug('[CCU] refresh device database on newDevices message from ccu')
self.updateDeviceDatabase()
}
})
this.rpc.connect()
}
fireEvent(address, value) {
let self = this
let cbList = this.eventCallbacks[address]
if (cbList) {
self.log.debug('[CCU] event %s will be handled by %s registered callback', address, cbList.length)
cbList.map(cb => {
cb(value)
})
}
}
fireVariableEvent(varName, value) {
let cbList = this.variableCallbacks[varName]
if (cbList) {
this.log.debug('[CCU] var Update Event %s will be handled by %s registered callback', varName, cbList.length)
cbList.map(cb => {
cb(value)
})
} else {
this.log.debug('[CCU] no registered event callbacks for %s', varName)
}
}
registerVariableForEventProcessingAtAccessory(varName, callback) {
if (typeof callback === 'function') {
this.log.debug('[CCU] register variable %s for events', varName)
if (this.variableCallbacks[varName] === undefined) {
this.variableCallbacks[varName] = []
}
this.variableCallbacks[varName].push(callback)
// also do a remote fetch for this address
this.getVariableValue(varName, true)
} else {
this.log.warn('[CCU] unable to register %s event %s is not a function ', varName, callback)
}
}
updateRegisteredVariables() {
let self = this
Object.keys(this.variableCallbacks).map((varName) => {
self.getVariableValue(varName, true)
})
}
registerAddressForEventProcessingAtAccessory(address, callback) {
this.log.debug('[CCU] register address %s for events', address)
if (this.eventCallbacks[address] === undefined) {
this.eventCallbacks[address] = []
}
this.eventCallbacks[address].push(callback)
// also do a remote fetch for this address
this.getValue(address, true)
}
prefillCache() {
let self = this
return new Promise((resolve, reject) => {
self.log.debug('[CCU] will prefill the cache with values from evdps');
let evdpFile = path.join(self.configurationPath, 'evdps.json')
if (fs.existsSync(evdpFile)) {
try {
let dpsCache = JSON.parse(fs.readFileSync(evdpFile))
let script = `boolean df = true;string dpn;object o; string dpList = '${dpsCache.join('\t')}';Write('{"states":{'); foreach(dpn,dpList){ o = dom.GetObject(dpn); if (o){ object v = o.Value();if (v) {if(df) {df = false;} else { Write(',');} Write('"' # dpn # '":');if (o.ValueType() != ivtString) { Write(o.Value()); } else { Write('"' #o.Value() # '"');}}}} Write('}}');`
let rega = new Rega(self.log, self.ccuIP)
rega.script(script).then(states => {
let jStates = JSON.parse(states);
if ((jStates) && (jStates.states)) {
Object.keys(jStates.states).forEach(dpId => {
self.setCache(dpId, jStates.states[dpId])
})
}
self.log.debug('[CCU] cache prefill is done');
resolve(jStates)
})
} catch (e) {
self.log.error(e)
resolve(undefined)
}
} else {
self.log.debug('evdps not found .. ')
resolve(undefined)
}
})
}
processEventDatapoints() {
let self = this
let dpsToAdd = []
let dpsToRemove = []
let dpsCache = []
this.log.debug('[CCU] register all new event datapoints via reportValueUsages')
// we will check all dps in this.eventCallbacks
let dps = Object.keys(this.eventCallbacks)
let evdpFile = path.join(this.configurationPath, 'evdps.json')
if (fs.existsSync(evdpFile)) {
try {
dpsCache = JSON.parse(fs.readFileSync(evdpFile))
} catch (e) { }
}
// build datapoints which will be removed from iterface
dpsCache.map((cachedItem) => {
if (dps.indexOf(cachedItem) === -1) {
dpsToRemove.push(cachedItem)
}
})
// build datapoints to add to the interface
dps.map((usedItem) => {
if (dpsCache.indexOf(usedItem) === -1) {
dpsToAdd.push(usedItem)
}
})
this.log.debug('[CCU] %s datapoints to add', dpsToAdd.length)
this.log.debug('[CCU] %s datapoints to remove', dpsToRemove.length)
// registration is done on every interface so we have to loop thru known intefaces
let ifList = this.rpc.connectedInterfaces()
ifList.map((ccuInterface) => {
let itemsInInterface = {}
dpsToAdd.map((dpName) => {
let idx = dpName.indexOf(ccuInterface.ifName)
if (idx === 0) {
itemsInInterface[dpName] = self.eventCallbacks[dpName].length || 1
}
})
// if there are new Datepoints .. register them
if (Object.keys(itemsInInterface).length > 0) {
self.log.debug('[CCU] register %s datapoints for interface %s', Object.keys(itemsInInterface).length, ccuInterface.ifName)
ccuInterface.reportValueUsage(itemsInInterface)
}
itemsInInterface = []
dpsToRemove.map((dpName) => {
let idx = dpName.indexOf(ccuInterface.ifName)
if (idx === 0) {
itemsInInterface[dpName] = 0
}
})
// if there are dps to remove do so
if (Object.keys(itemsInInterface).length > 0) {
self.log.debug('[CCU] UNregister %s datapoints from interface %s', Object.keys(itemsInInterface).length, ccuInterface.ifName)
ccuInterface.reportValueUsage(itemsInInterface)
}
})
// and save the cache to do this only on changed dps at next time
fs.writeFileSync(evdpFile, JSON.stringify(dps))
}
_fetchHomeKitSubSectionIDs(subsection) {
let self = this
return new Promise((resolve, reject) => {
var script = '!subsections\nWrite(\'{"subsection":[\');var s = dom.GetObject(ID_FUNCTIONS).Get("'
script += subsection
script += '");string cid;boolean sdf = true;if (s) {foreach(cid, s.EnumUsedIDs()){ '
script += ' if(sdf) {sdf = false;}'
script += ' else { Write(\',\');} Write(cid);}'
script += '} Write(\']}\');'
let rega = new Rega(self.log, self.ccuIP)
rega.script(script).then(section => {
if (section) {
let oSection = JSON.parse(section)
self.sectionChannelIds = oSection.subsection
resolve()
} else {
reject(new Error('unable to fetch section'))
}
})
})
}
_fetchInterfaces() {
let self = this
this.log.debug('[CCU] fetching Interfaces')
return new Promise((resolve, reject) => {
let rega = new Rega(self.log, self.ccuIP)
var script = '!interfaces\nstring sifId;boolean df = true;Write(\'{"interfaces":[\');foreach(sifId, root.Interfaces().EnumIDs()){object oIf = dom.GetObject(sifId);if ((oIf) && (oIf.TypeName()=="INTERFACE")) {if(df) {df = false;} else { Write(\',\');}Write(\'{\')'
script = script + self._scriptPartForElement('id', 'sifId', 'number', ',')
script = script + self._scriptPartForElement('name', 'oIf.Name()', 'string', ',')
script = script + self._scriptPartForElement('type', 'oIf.Type()', 'string', ',')
script = script + self._scriptPartForElement('typename', 'oIf.TypeName()', 'string', ',')
script = script + self._scriptPartForElement('info', 'oIf.InterfaceInfo()', 'string', ',')
script = script + self._scriptPartForElement('url', 'oIf.InterfaceUrl()', 'string')
script = script + 'Write(\'}\');}} Write(\']}\');'
rega.script(script).then(strInterfaces => {
if (strInterfaces) {
let interfaces = JSON.parse(strInterfaces)
interfaces.interfaces.map(oInterface => {
self.interfaces[oInterface.id] = oInterface
})
resolve()
} else {
reject(new Error('unable to fetch Interfaces'))
}
})
})
}
_scriptPartForElement(elementName, functionName, type, leadingComa = '') {
var result
if (type === 'urlstring') {
result = 'Write(\'"' + elementName + '": "\');'
result = result + 'WriteXML(' + functionName + ');'
result = result + 'Write(\'"' + leadingComa + '\');'
return result
} else
if (type === 'string') {
return 'Write(\'"' + elementName + '": "\' # ' + functionName + ' # \'"' + leadingComa + '\');'
} else if (type === 'number') {
return 'Write(\'"' + elementName + '": \' # ' + functionName + ' # \'' + leadingComa + '\');'
} else if (type === 'stringenumeration') {
result = 'Write(\'"' + elementName + '": [\');'
result = result + 'string idf;boolean tf = true;foreach(idf,' + functionName + '){if(tf){tf=false;} else { Write(\',\');}'
result = result + 'Write(\'"\' # idf # \'"\');}'
result = result + 'Write(\']\');'
return result
} else if (type === 'enumeration') {
result = 'Write(\'"' + elementName + '": [\');'
result = result + 'string idf;boolean tf = true;foreach(idf,' + functionName + '){if(tf){tf=false;} else { Write(\',\');}'
result = result + 'Write(\'\' # idf # \'\');}'
result = result + 'Write(\']\');'
return result
}
}
_loadTranslations(lang) {
this.translations = {}
let self = this
return new Promise((resolve, reject) => {
let url = 'http://' + this.ccuIP + '/webui/js/lang/' + lang + '/translate.lang.extension.js'
http.get(url, (res) => {
const { statusCode } = res
if (statusCode === 200) {
}
let rawData = ''
res.on('data', (chunk) => { rawData += chunk })
res.on('end', () => {
try {
const lines = rawData.split('\n')
lines.forEach(line => {
const match = line.match(/\s*"((func|room|sysVar)[^"]+)"\s*:\s*"([^"]+)"/)
if (match) {
let key = match[1]
let trns = unescape(match[3])
self.translations[key] = trns
}
})
resolve()
} catch (e) {
reject(e)
}
})
})
})
}
_removemonitconfig() {
if ((fs.existsSync('/usr/bin/monit')) && (fs.existsSync('/usr/local/etc/monit_hap-homematic.cfg'))) {
try {
fs.unlinkSync('/usr/local/etc/monit_hap-homematic.cfg')
const childprocess = require('child_process')
childprocess.execSync('/usr/bin/monit reload')
} catch (e) {
this.log.error(e)
}
}
}
_buildmonitconfig() {
// first check if /usr/bin/monit is there and check if we have a pid file
if (fs.existsSync('/usr/bin/monit')) {
if (fs.existsSync('/var/run/hap-homematic.pid')) {
let cfgFile = '/usr/local/etc/monit_hap-homematic.cfg'
if (!fs.existsSync(cfgFile)) {
try {
this.log.info('[CCU] ok looks like we have a monitoring daemon so i will create settings for this')
var config = '# Hap-HomeMatic Homekit engine daemon monitoring\n'
config = config + 'check process HapHomeMatic with pidfile /var/run/hap-homematic.pid\n'
config = config + ' group addons\n'
config = config + ' start = "/etc/config/rc.d/hap-homematic start"\n'
config = config + ' stop = "/etc/config/rc.d/hap-homematic stop"\n'
config = config + ' restart = "/etc/config/rc.d/hap-homematic restart"\n'
config = config + ' if not exist for 5 cycles then restart\n'
config = config + ' if failed host \'127.0.0.1\' port 9875 protocol http request "/" for 5 cycles then restart\n'
config = config + ' if 1 restart within 1 cycles then\n'
config = config + ' exec "/bin/triggerAlarm.tcl \'Hap-HomeMatic restarted\' WatchDog-Alarm"\n'
fs.writeFileSync(cfgFile, config)
// reload the monitor daemon
const childprocess = require('child_process')
childprocess.execSync('/usr/bin/monit reload')
} catch (e) {
this.log.error(e)
}
}
} else {
this._removemonitconfig() // remove the monitor config if there is no pid file so we do not get reloaded by the monitor
}
}
}
}
module.exports = HomeMaticCCU