UNPKG

hap-homematic

Version:

provides a homekit bridge to the ccu

1,244 lines (1,107 loc) 42.4 kB
/* * File: HomeMaticAccessory.js * Project: hap-homematic * File Created: Saturday, 7th March 2020 1:19:40 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 os = require('os') const util = require('util') const fs = require('fs') const uuid = require('hap-nodejs').uuid const Accessory = require('hap-nodejs').Accessory const Service = require('hap-nodejs').Service const Characteristic = require('hap-nodejs').Characteristic const EveHomeKitTypes = require(path.join(__dirname, 'EveHomeKitTypes.js')) const HomeMaticAddress = require(path.join(__dirname, '..', 'HomeMaticAddress.js')) const EventEmitter = require('events') const moment = require('moment') // Abstract Super Class class HomeMaticAccessory extends EventEmitter { constructor(channel, sInterface, server, settings = {}) { super() this.runsInTestMode = server.isTestMode this._server = server let serial = channel.address this._settings = settings this.log = server.log this.isPublished = false this._serial = serial.split(':').slice(0, 1)[0] this._channelnumber = serial.split(':').slice(1, 2)[0] this._ccuType = channel.type this._deviceType = channel.dtype this._deviceName = channel.dname this._ccu = server._ccu this._ccuChannelId = channel.id this._interf = sInterface this._access = channel.access this._persistentValues = {} if (!this.runsInTestMode) { this._persistentStore = path.join(this._server._configurationPath, os.hostname() + '_' + this._serial + '_' + this._channelnumber + '.pstore') } if (settings.name) { this._name = settings.name } else { // Check if the Channel Name is like the Address äö let cDefaultName = this._deviceType + ' ' + this._serial + ':' + this._channelnumber let idx = cDefaultName.indexOf(channel.name) this.debugLog('Checking defaultname %s vs %s -> %s', cDefaultName, channel.name, idx) if (idx === -1) { this.debugLog('will use the chanel name') this._name = channel.name.replace(/[.:#_()]/g, ' ') } else { this.debugLog('will use the device name') if (this._deviceName) { this._name = this._deviceName.replace(/[.:#_()]/g, ' ') } else { // giving up this._name = this._serial } } } this._accessoryUUID = this.generateUUID(this._ccuType + ':' + this._name) } /** * initialize the accessory */ init() { this.debugLog('creating homekit accessory %s', this.getName()) // make this overridable this.createHomeKitAccessory() // this is only a dummy so fakegato will work this.gatoHomeBridge = this._server.gatoHomeBridge // the eve HomeKitType Lib expects this structure this.eve = new EveHomeKitTypes(this.gatoHomeBridge.hap) this.loadPersistentValues() this.services = [] this._configureInformationService() this.initialQuery = true this.serviceSettings = this.initServiceSettings(Characteristic) this.initAccessoryService(Service) this.publishServices(Service, Characteristic) this.monitorReachability() } initAccessoryService(Service) { // Stub } createHomeKitAccessory() { let self = this this.debugLog('publishing services for %s', this.getName()) this.homeKitAccessory = new Accessory(this._name, this._accessoryUUID) this.homeKitAccessory.on('identify', (paired, callback) => self.identify(paired, callback) ) this.homeKitAccessory.log = this.log } isBridgedAccessory() { return true } generateUUID(key) { return uuid.generate(key) } address() { return this._serial + ':' + this._channelnumber } isReadOnly() { let roChannel = this.deviceServiceSettings('roChannel') let result = this.isChannelReadOnly(roChannel) this.debugLog('check RO %s is %s', roChannel, result) return result } /* this will add the current classname and serial to the debug log output */ debugLog() { let msg = '[' + this.constructor.name + '] ' + this._serial + ' ' let logMsg = util.format.apply(util, Array.prototype.slice.call(arguments)) this.log.debug(msg + logMsg) } warnLog() { let msg = '[' + this.constructor.name + '] ' + this._serial + ' ' let logMsg = util.format.apply(util, Array.prototype.slice.call(arguments)) this.log.warn(msg + logMsg) } errorLog() { let msg = '[' + this.constructor.name + '] ' + this._serial + ' ' let logMsg = util.format.apply(util, Array.prototype.slice.call(arguments)) this.log.error(msg + logMsg) } isChannelReadOnly(channelNum) { if (channelNum === undefined) { this.debugLog('access level is %s', this._access) if (this._access) { return (this._access) ? parseInt(this._access) !== 255 : true } else { return false } } else { // get the channel from CCU let channel = this._ccu.getChannelByAddress(this._serial + ':' + channelNum) if (channel) { this.debugLog('check channel %s access level is %s', channelNum, channel.access) if (channel.access) { return (channel.access) ? parseInt(channel.access) !== 255 : true } else { return false } } } return true } /** * this will return the HomeKit Accessory */ getHomeKitAccessory() { return this.homeKitAccessory } getManufacturer() { return 'HAP-Homematic By Thkl' } getName() { return this._name } getDeviceSettings(key, returnEmpty = false) { if (key === undefined) { if (this._settings) { return this._settings.settings || {} } return {} } else { if ((this._settings) && (this._settings.settings)) { let tmp = this._settings.settings[key] if ((tmp === '') && (returnEmpty === true)) { return tmp } else { return tmp } } else { return undefined } } } /** * this is a stub; extended classes should implement this to create the homekit services and alle the magic * @param {*} Service * @param {*} Characteristic */ publishServices(Service, Characteristic) { this.log.warn('[Generic] u should override this to create your accessory') } getPublishInfo() { return {} } getPort() { return this.port } publishSingleAccessory(port) { this.port = port this.homeKitAccessory.port = this.getPort() this.homeKitAccessory.publish(this.getPublishInfo(), false) } getUUID() { return this._accessoryUUID } removeData() { // we do not have persistent data in test mode if (!this.runsInTestMode) { // clean eve history if (this.loggingService) { this.debugLog('removing history data') this.loggingService.cleanPersist() } // remove persistent store if (fs.existsSync(this._persistentStore)) { this.debugLog('removing persistent data') fs.unlinkSync(this._persistentStore) } } } /** * shuts down the accessory this will be called from the server on reload and shutdown * override this to clear all the timers */ shutdown() { clearTimeout(this.setDelayTimer) } /** * this will return a value for key for a specified device type. if there are no specified settings * will be used * @param {key used by settings} key */ deviceServiceSettings(key, subkey) { if (this.serviceSettings === undefined) { this.log.warn('[Generic] no serviceSettings defined') return undefined } // first try the channel Type - HmIP-BLA:CONTACT let oDeviceSettings = this.serviceSettings[this._deviceType + ':' + this._ccuType] // if not the channel type - CONTACT if (oDeviceSettings === undefined) { oDeviceSettings = this.serviceSettings[this._ccuType] } // if not the device type - HmIP-BLA if (oDeviceSettings === undefined) { oDeviceSettings = this.serviceSettings[this._deviceType] } // if not use the * if (oDeviceSettings === undefined) { oDeviceSettings = this.serviceSettings['*'] } if ((subkey === undefined) || (subkey === null)) { if (oDeviceSettings !== undefined) { return oDeviceSettings[key] } else { this.log.debug('[Generic] no key %s found in %s', key, JSON.stringify(oDeviceSettings)) return undefined } } else { if (oDeviceSettings[subkey] !== undefined) { return oDeviceSettings[subkey][key] } else { this.log.debug('[Generic] no key %s for subkey section %s found in %s', key, subkey, JSON.stringify(oDeviceSettings)) return undefined } } } deviceServiceSettingsFromTemplate(key, subkey, templateSettings) { let tmp = this.deviceServiceSettings(key, subkey) if (templateSettings) { Object.keys(templateSettings).map((tKey) => { tmp = tmp.replace('%' + tKey + '%', templateSettings[tKey]) }) } return tmp } initServiceSettings() { return {} } /** * returns the Service with name ... from the homekit accessor * if there is none , the service will be created * @param {*} name */ getService(serviceType, name = this._name, forceAdd = false, subtype = '', makeTestFailed = false) { let service = this.homeKitAccessory.getService(serviceType, name, subtype) if ((!service) || (forceAdd === true)) { if (makeTestFailed) { this.log.warn('[Generic] add Service will fail') return null } service = this.homeKitAccessory.addService(serviceType, name, serviceType.UUID, subtype) } if (subtype !== '') { // this is a dirty fix .. but hey it works service.subtype = subtype } var nameCharacteristic = service.getCharacteristic(Characteristic.Name) || service.addCharacteristic(Characteristic.Name) nameCharacteristic.setValue(name) return service } addService(service) { if (this.homeKitAccessory.getService(service) === undefined) { this.homeKitAccessory.addService(service) } return service } // maps the value depending on the servicesettings getDataPointResultMapping(type, subkey, value, mapTable = 'mapping', reverse = false) { let settings = this.deviceServiceSettings(type, subkey) let mappingtable = settings[mapTable] let testValue = value if ((typeof settings === 'object') && (mappingtable)) { this.debugLog('Mapping - Table found') if (settings.number) { // change the value into boolean testValue = parseInt(value) } if (settings[mapTable]) { if (settings.boolean) { // change the value into string testValue = this.isTrue(value) testValue = testValue ? 'true' : 'false' this.debugLog('mapping boolean to string %s', JSON.stringify(testValue)) } if (reverse === true) { var rResult Object.keys(mappingtable).map(key => { if (mappingtable[key] === testValue) { rResult = key } }) return rResult } else { if (mappingtable[testValue] !== undefined) { this.debugLog('mapping result found ...') return mappingtable[testValue] } else { this.debugLog('no value in mappingtable %s returning input', JSON.stringify(mappingtable)) return value } } } else { this.debugLog('no mapping table return input') return value } } return value } /** * return a datapoint name from settings matrix * @param {*} type * @param {*} subkey */ getDataPointNameFromSettings(type, subkey) { let result = this.deviceServiceSettings(type, subkey) if (!result) { return undefined } if (typeof result === 'string') { return result } else { return result.name } } getDataPointValueFromSettings(type, subkey) { let result = this.deviceServiceSettings(type, subkey) if (!result) { return undefined } if (typeof result === 'string') { return result } else { return result.value } } /** * gets called by the identify event .. * you may override this to let your device do blinkenlights * @param {*} paired * @param {*} callback */ identify(paired, callback) { this.log.info('[Generic] identifying %s. paired %s', this._name, paired) if (callback) { callback() } } /** * sets a value at the ccu with a delay * @param {*} address * @param {*} newValue * @param {*} delay */ setValueDelayed(address, newValue, delay = 100) { clearTimeout(this.setDelayTimer) let self = this this.setDelayTimer = setTimeout(() => { self.setValue(address, newValue) }, delay) } /** * sets value to a datapoint at the ccu * @param {*} address * @param {*} newValue */ setValue(address, newValue) { let self = this var adr = address if (typeof address === 'string') { adr = self.buildAddress(address) } if ((adr) && (adr.address())) { return this._ccu.setValue(adr.address(), newValue) } else { return false } } /** * sets a Datapoint Value based on the device configuration mask * @param {*} settingsKey * @param {*} subkey * @param {*} newValue */ setValueForDataPointNameWithSettingsKey(settingsKey, subkey, newValue) { let realDataPointName = this.getDataPointNameFromSettings(settingsKey, subkey) return this.setValue(realDataPointName, newValue) } getValue(address, ignoreCache) { let self = this let adr = self.buildAddress(address) this.debugLog('getValue %s (%s)', adr.address(), ignoreCache) if (adr.variable()) { return this._ccu.getVariableValue(adr.variable()) } else { if ((adr) && (adr.address())) { return this._ccu.getValue(adr.address(), ignoreCache) } else { return false } } } /** * gets a datapoint value based on the device configuration mask * @param {*} settingsKey * @param {*} subkey * @param {*} ignoreCache */ getValueForDataPointNameWithSettingsKey(settingsKey, subkey, ignoreCache) { let realDataPointName = this.getDataPointNameFromSettings(settingsKey, subkey) return this.getValue(realDataPointName, ignoreCache) } /** * adds a eve logging service to the accessory * @param {*} type * @param {*} disableTimer */ enableLoggingService(type, disableTimer) { // make sure the loggin is only once enabled if (this.loggingService !== undefined) { this.log.error('LoggingService can only enabled once') return } // do no record history if the flag is set. saving calls will be ignored if (this._server.getConfig('disableHistory') === true) { this.debugLog('Skip Logging Service for %s because its disabled', this._name) return } if (this.runsInTestMode === true) { this.debugLog('Skip Logging Service for %s because of testmode', this._name) } else { if (['weather', 'energy', 'room', 'door', 'motion', 'switch', 'thermo', 'aqua'].indexOf(type) === -1) { this.log.warn('[Generic] logging type %s is not available', type) return } else { this.debugLog('enable logging service %s for %s', type, this._name) } if (disableTimer === undefined) { disableTimer = false } // make the gato cache run on a usb stick let cachePath = this._server.getConfig('cache') if (cachePath !== undefined) { cachePath = path.join(cachePath, 'evehistory') } else { cachePath = this._server._configurationPath } if (!fs.existsSync(cachePath)) { fs.mkdirSync(cachePath, true) } var FakeGatoHistoryService = require('fakegato-history')(this.gatoHomeBridge) var hostname = os.hostname() let filename = hostname + '_' + this._serial + '_' + this._channelnumber + '_persist.json' this.loggingService = new FakeGatoHistoryService(type, this.homeKitAccessory, { storage: 'fs', filename: filename, path: cachePath, disableTimer: disableTimer, length: 1000 }) this.debugLog('Log Service for %s with type %s added timer disabled %s', this._name, type, disableTimer) this.services.push(this.loggingService) } } /** * adds the eve reset statistics to the service * @param {*} callback will be called when a reset was perfomed */ addResetStatistics(service, resetCallback) { if ((this.runsInTestMode === false) && (service !== undefined)) { this.debugLog('adding Reset to %s', this._name) let self = this this.lastReset = this.getPersistentValue('lastReset', undefined) if (this.lastReset === undefined) { // Set to now let epoch = moment('2001-01-01T00:00:00Z').unix() this.lastReset = moment().unix() - epoch this.savePersistentValue('lastReset', this.lastReset) } service.addOptionalCharacteristic(this.eve.Characteristic.ResetTotal) this.resetCharacteristic = service.getCharacteristic(this.eve.Characteristic.ResetTotal) this.resetCharacteristic.on('set', (value, setCallback) => { self.debugLog('will perform a reset for %s', self._name) // only reset if its not equal the reset time we know if (value !== self.lastReset) { self.lastReset = value self.savePersistentValue('lastReset', self.lastReset) if (resetCallback) { self.debugLog('calling reset function of %s', self._name) resetCallback() } if (self.loggingService) { self.loggingService.cleanPersist() } } else { self.debugLog('set ResetTotal called %s its equal the last reset time so ignore', value) } if (setCallback) { setCallback() } }) this.resetCharacteristic.on('get', (callback) => { self.debugLog('get lastReset called for %s will report %s', self._name, self.lastReset) callback(null, self.lastReset) }) this.resetCharacteristic.updateValue(this.lastReset, null) self.debugLog('reset Statistics added for %s', self._name) } else { if (!this.runsInTestMode) { this.log.warn('[Generic] unable to add reset to %s', this._name) if (this.loggingService === undefined) { this.log.warn('Please add the logging service before calling addResetStatistics') } } } } /** * adds a log entry * @param {[type]} data {key:value} * @return {[type]} [description] */ addLogEntry(data) { // check if loggin is enabled if ((this.loggingService !== undefined) && (data !== undefined)) { data.time = moment().unix() // check if the last logentry was just recently and is the same as the previous var logChanges = true // there is a previous logentry, let's compare... if (this.lastLogEntry !== undefined) { this.debugLog('addLogEntry lastLogEntry is available') logChanges = false // compare data var self = this Object.keys(data).forEach(key => { if (key === 'time') { return } // log changes if values differ if (data[key] !== self.lastLogEntry[key]) { self.debugLog('lastLogEntry is different') logChanges = true } }) // log changes if last log entry is older than 7 minutes, // homematic usually sends updates evry 120-180 seconds if ((data.time - self.lastLogEntry.time) > 7 * 60) { logChanges = true } } if (logChanges) { this.debugLog('Saving log data for %s: %s', this._name, JSON.stringify(data)) this.loggingService.addEntry(data) this.lastLogEntry = data } else { this.debugLog('Log did not change %s', this._name) } } } addLastActivationService(service) { if ((service !== undefined) && (this.loggingService !== undefined)) { let self = this service.addOptionalCharacteristic(this.eve.Characteristic.LastActivation) this.lastActivationService = service.getCharacteristic(this.eve.Characteristic.LastActivation) this.lastActivationService.on('get', (callback) => { callback(null, self.lastActivation) }) this.lastActivation = this.getPersistentValue('lastActivation') if (this.lastActivation === undefined) { this.lastActivation = this.loggingService.getInitialTime() this.savePersistentValue('lastActivation', this.lastActivation) } this.lastActivationService.updateValue(this.lastActivation, null) } } updateLastActivation() { if (this.lastActivationService !== undefined) { let firstLog = this.loggingService.getInitialTime() this.lastActivation = moment().unix() - firstLog this.lastActivationService.updateValue(this.lastActivation, null) this.savePersistentValue('lastActivation', this.lastActivation) } } loadPersistentValues() { if (!this.runsInTestMode) { if (fs.existsSync(this._persistentStore)) { this._persistentValues = JSON.parse(fs.readFileSync(this._persistentStore).toString()) } else { this._persistentValues = {} } } } async addTamperedCharacteristic(rootService, channel = 0) { let self = this if (rootService !== undefined) { if (rootService.testCharacteristic(Characteristic.StatusTampered)) { this.tamperedCharacteristic = rootService.getCharacteristic(Characteristic.StatusTampered) } else { // not added by default -> create it this.debugLog('added Tampered to %s', this.name) rootService.addOptionalCharacteristic(Characteristic.StatusTampered) this.tamperedCharacteristic = rootService.getCharacteristic(Characteristic.StatusTampered) } if (channel !== undefined) { // figure out which sabotage datapoint to use if (await this._ccu.hazDatapoint(this.buildAddress(channel + '.SABOTAGE'))) { this.tamperedCharacteristic.on('get', async callback => { callback(null, self.isTrue(await self.getValue(channel + '.SABOTAGE', true))) }) this.registerAddressForEventProcessingAtAccessory(this.buildAddress(channel + '.SABOTAGE'), (newValue) => { self.tamperedCharacteristic.updateValue(self.isTrue(newValue), null) }) return } if (await this._ccu.hazDatapoint(this.buildAddress(channel + '.ERROR_SABOTAGE'))) { this.tamperedCharacteristic.on('get', async callback => { callback(null, self.isTrue(await self.getValue(channel + '.ERROR_SABOTAGE', true))) }) this.registerAddressForEventProcessingAtAccessory(this.buildAddress(channel + '.ERROR_SABOTAGE'), (newValue) => { self.tamperedCharacteristic.updateValue(self.isTrue(newValue), null) }) } } } } async addLowBatCharacteristic(channel = 0) { // check if we have LOWBAT or LOW_BAT let hazLowBat = await this._ccu.hazDatapoint(this.buildAddress(channel + '.LOWBAT')) let hazLowBat2 = await this._ccu.hazDatapoint(this.buildAddress(channel + '.LOW_BAT')) if ((!hazLowBat) && (!hazLowBat2)) { return } let voltage = this.deviceServiceSettings('voltage') if (voltage) { let voltgeDP = this.buildAddress(channel + '.OPERATING_VOLTAGE') this.debugLog('check for operating voltage datapoint %s', voltgeDP.address()) let hazVoltage = await this._ccu.hazDatapoint(voltgeDP) if (hazVoltage) { this.debugLog('found Voltage %s and Datapoint is here', voltage) this.addHmIPBatteryLevelStatus(undefined, undefined, voltage) } else { this.debugLog('found Voltage %s but Datapoint is not here', voltage) this.addHMLowBatCharacteristic(channel) } } else { this.addHMLowBatCharacteristic(channel) } } async addHMLowBatCharacteristic(channel = 0) { let self = this this.isBatLow = false if (this.batteryService === undefined) { this.batteryService = this.getService(Service.BatteryService) } if (this.batLevel === undefined) { this.batLevel = this.batteryService.getCharacteristic(Characteristic.BatteryLevel) .on('get', (callback) => { callback(null, self.isBatLow ? 0 : 100) }) } if (this.lowBatCharacteristic === undefined) { this.lowBatCharacteristic = this.batteryService.getCharacteristic(Characteristic.StatusLowBattery) if (channel !== undefined) { this.debugLog('adding LOW Batt stuff for %s', this._serial) if (await this._ccu.hazDatapoint(this.buildAddress(channel + '.LOWBAT'))) { this.debugLog('register LOWBAT Event for %s', this._serial) this.lowBatCharacteristic.on('get', async callback => { self.isBatLow = self.isTrue(await self.getValue(channel + '.LOWBAT', true)) self.batLevel.updateValue(self.isBatLow ? 0 : 100, null) let hk = (self.isBatLow === true) ? Characteristic.StatusLowBattery.BATTERY_LEVEL_LOW : Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL callback(null, hk) }) this.registerAddressForEventProcessingAtAccessory(this.buildAddress(channel + '.LOWBAT'), (newValue) => { self.isBatLow = self.isTrue(self.isTrue(newValue)) self.batLevel.updateValue(self.isBatLow ? 0 : 100, null) let hk = (self.isBatLow === true) ? Characteristic.StatusLowBattery.BATTERY_LEVEL_LOW : Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL self.debugLog('LOWBAT Event for %s : %s HK Value %s', self._serial, newValue, hk) self.lowBatCharacteristic.updateValue(hk, null) }) return } if (await this._ccu.hazDatapoint(this.buildAddress(channel + '.LOW_BAT'))) { this.debugLog('register LOW_BAT Event for %s', this._serial) this.lowBatCharacteristic.on('get', async callback => { self.isBatLow = self.isTrue(await self.getValue(channel + '.LOW_BAT', true)) self.batLevel.updateValue(self.isBatLow ? 0 : 100, null) let hk = (self.isBatLow === true) ? Characteristic.StatusLowBattery.BATTERY_LEVEL_LOW : Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL callback(null, hk) }) this.registerAddressForEventProcessingAtAccessory(this.buildAddress(channel + '.LOW_BAT'), (newValue) => { self.isBatLow = self.isTrue(self.isTrue(newValue)) self.batLevel.updateValue(self.isBatLow ? 0 : 100, null) let hk = (self.isBatLow === true) ? Characteristic.StatusLowBattery.BATTERY_LEVEL_LOW : Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL self.debugLog('LOW_BAT Event for %s : %s HK Value %s', self._serial, newValue, hk) self.lowBatCharacteristic.updateValue(hk, null) }) } } } } addHmIPBatteryLevelStatus(dpName = '0.OPERATING_VOLTAGE', dplowBat = '0.LOW_BAT', maxVoltage = 3.0) { let self = this if (this.batteryService === undefined) { this.debugLog('adding hmIP Battery service') this.batteryService = this.getService(Service.BatteryService) } if (this.levelCharacteristic === undefined) { this.levelCharacteristic = this.batteryService.getCharacteristic(Characteristic.BatteryLevel) .on('get', (callback) => { self.getValue(dpName, true).then(value => { let level = parseFloat(value) / (parseFloat(maxVoltage) / 100) self.debugLog('get battery level %s', level) callback(null, level) }) }) } if (this.lowLevelCharacteristic === undefined) { this.lowLevelCharacteristic = this.batteryService.getCharacteristic(Characteristic.StatusLowBattery) .on('get', (callback) => { self.getValue(dplowBat, true).then(value => { let hk = (self.isTrue(value) === true) ? Characteristic.StatusLowBattery.BATTERY_LEVEL_LOW : Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL callback(null, hk) }) }) this.batteryService.getCharacteristic(Characteristic.ChargingState) .on('get', (callback) => { if (callback) callback(null, false) }) this.registerAddressForEventProcessingAtAccessory(this.buildAddress(dplowBat), (newValue) => { let hk = (self.isTrue(newValue) === true) ? Characteristic.StatusLowBattery.BATTERY_LEVEL_LOW : Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL self.lowLevelCharacteristic.updateValue(hk, null) }) this.registerAddressForEventProcessingAtAccessory(this.buildAddress(dpName), (newValue) => { let level = parseFloat(newValue) / (parseFloat(maxVoltage) / 100) self.debugLog('voltage is %s level %s', newValue, level) if (level > 100) { level = 100 } self.updateCharacteristic(self.levelCharacteristic, level) }) } } addFaultCharacteristic(rootService, hmDatapoint = '0.STICKY_UNREACH', mapping = (value) => { return self.isTrue(value) }) { let self = this var fault = rootService.getCharacteristic(Characteristic.StatusFault) if (fault !== undefined) { this.faultCharacteristic = fault } else { // not added by default -> create it this.debugLog('added Fault to %s', this.name) rootService.addOptionalCharacteristic(Characteristic.StatusFault) this.faultCharacteristic = rootService.getCharacteristic(Characteristic.StatusFault) } this.faultCharacteristic.on('get', callback => { self.getValue(hmDatapoint, true).then(value => { callback(null, mapping(value)) }) }) this.registerAddressForEventProcessingAtAccessory(this.buildAddress(hmDatapoint), (newValue) => { self.faultCharacteristic.updateValue(mapping(newValue), null) }) } monitorReachability() { let self = this this.registerAddressForEventProcessingAtAccessory(this.buildAddress('0.UNREACH'), (newValue) => { try { self.homeKitAccessory.updateReachability(!self.isTrue(newValue)) } catch (e) { } }) } addStateBasedCharacteristic(service, characteristic, getStateCallback) { let self = this if (service !== undefined) { service.addOptionalCharacteristic(characteristic) let result = service.getCharacteristic(characteristic) .on('get', (callback) => { self.debugLog('getTimesOpened will report %s', getStateCallback()) callback(null, getStateCallback()) }) result.setValue(getStateCallback()) return result } } getPersistentValue(key, defaultValue) { if (this._persistentValues[key]) { return this._persistentValues[key] } else { return defaultValue } } savePersistentValue(key, value) { this._persistentValues[key] = value if (!this.runsInTestMode) { fs.writeFileSync(this._persistentStore, JSON.stringify(this._persistentValues)) } } registerAddressForEventProcessingAtAccessory(address, callback) { if (typeof callback !== 'function') { console.log(callback) this.log.warn('callback is not a function') } if ((address) && ((typeof address.address === 'function'))) { // this will register var changes like normal events // the variable will be checked on a trigger event if (address.variable()) { this._ccu.registerVariableForEventProcessingAtAccessory(address.variable(), callback) } else { this._ccu.registerAddressForEventProcessingAtAccessory(address.address(), callback) } } else { this.log.error('[Generic] unable to register %s invalid object or null found', address) } } registerAddressWithSettingsKeyForEventProcessingAtAccessory(key, subkey, callback) { let adrForKey = this.getDataPointNameFromSettings(key, subkey) this.debugLog('Register Datapoint %s for events', adrForKey) if (adrForKey) { let fullAdr = this.buildAddress(adrForKey) this.debugLog('full addr is %s', fullAdr) this.registerAddressForEventProcessingAtAccessory(fullAdr, callback) } else { this.log.warn('[Generic] no Datapoint for event registering found in %s %s', key, subkey) } } buildAddress(dp) { this.debugLog('buildAddress %s', dp) if ((dp) && (typeof dp === 'string')) { // check its a variable let vr = this.isVariable(dp) if (vr) { this.debugLog('seems to be a variable') return new HomeMaticAddress(null, null, null, null, dp) } var pos = dp.indexOf('.') if (pos === -1) { this.debugLog('seems to be a single datapoint') let result = new HomeMaticAddress(this._interf, this._serial, this._channelnumber, dp) return result } 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(dp) if ((parts) && (parts.length > 4)) { let intf = parts[1] let address = parts[2] let chidx = parts[3] let dpn = parts[4] this.debugLog('try I.A:C.D Format |I:%s|A:%s|C:%s|D:%s', intf, address, chidx, dpn) return new HomeMaticAddress(intf, address, chidx, dpn) } else { // try format channel.dp let rgx = /([0-9]{1,}).([a-zA-Z0-9-_]{1,})/g let parts = rgx.exec(dp) if ((parts) && (parts.length === 3)) { let chidx = parts[1] let dpn = parts[2] this.debugLog('match C.D Format |I:%s|A:%s|C:%s|D:%s', this._interf, this._serial, chidx, dpn) return new HomeMaticAddress(this._interf, this._serial, chidx, dpn) } } } else { this.log.error('[Generic] unable create HM Address from undefined Input %s', dp) throw new Error('unable to generate unable create HM Address from undefined Input') } } isTrue(value) { var result = false if ((typeof value === 'string') && (value.toLocaleLowerCase() === 'true')) { result = true } if ((typeof value === 'string') && (value.toLocaleLowerCase() === '1')) { result = true } if ((typeof value === 'number') && (value === 1)) { result = true } if ((typeof value === 'boolean') && (value === true)) { result = true } return result } didMatch(v1, v2) { if (typeof v1 === typeof v2) { return (v1 === v2) } if (((typeof v1 === 'number') && (typeof v2 === 'string')) || ((typeof v1 === 'string') && (typeof v2 === 'number'))) { return parseFloat(v1) === parseFloat(v2) } if ((typeof v1 === 'boolean') && (typeof v2 === 'string')) { if (v1 === true) { return (v2.toLocaleLowerCase() === 'true') } if (v1 === false) { return (v2.toLocaleLowerCase() === 'false') } } if ((typeof v2 === 'boolean') && (typeof v1 === 'string')) { if (v2 === true) { return (v1.toLocaleLowerCase() === 'true') } if (v2 === false) { return (v1.toLocaleLowerCase() === 'false') } } if ((typeof v1 === 'boolean') && (typeof v2 === 'number')) { return (((v1 === true) && (v2 === 1)) || ((v1 === false) && (v2 === 0))) } if ((typeof v2 === 'boolean') && (typeof v1 === 'number')) { return (((v2 === true) && (v1 === 1)) || ((v2 === false) && (v1 === 0))) } return false } updateCharacteristic(characteristic, newValue, force = false, delay = 0) { if ((characteristic) && ((characteristic.value !== newValue) || (force === true))) { this.debugLog('updating %s to %s (%s)', characteristic.displayName, newValue, force) //prechecking the margins if ((characteristic.props) && (characteristic.props.minValue)) { if (newValue < characteristic.props.minValue) { newValue = characteristic.props.minValue } if (newValue > characteristic.props.maxValue) { newValue = characteristic.props.maxValue } } if (delay > 0) { setTimeout(() => { characteristic.updateValue(newValue) }, delay) } else { characteristic.updateValue(newValue) } } else { if (characteristic) { this.debugLog('skipp update %s cause the value stayed the same %s (force is %s)', characteristic.displayName, newValue, force) } } } isVariable(datapointAddress) { return (this._ccu.variableWithName(datapointAddress) !== undefined) } // validate a datapoint address isDatapointAddressValid(datapointAddress, acceptNull) { this.debugLog('validate datapoint %s we %s accept nul', datapointAddress, acceptNull ? 'do' : 'do not') if ((datapointAddress !== undefined) && (datapointAddress.length > 1)) { let parts = datapointAddress.split('.') if ((parts.length > 1) && (parts[0] === 'V')) { // seems to be a variable return true } // check we have 3 parts interface.address.name if (parts.length !== 3) { this.log.error('[Generic] %s is invalid not 3 parts', datapointAddress) return false } // check the address has a : if (parts[1].indexOf(':') === -1) { this.log.error('[Generic] %s is invalid %s does not contain a :', datapointAddress, parts[1]) return false } return true } else { // dp is undefined .. check if this is valid if (acceptNull === false) { this.log.error('[Generic] null is not a valid datapoint') } return acceptNull } } parseConfigurationJSON(strJSON, defValue) { try { return JSON.parse(strJSON) } catch (e) { return defValue } } /** ************* Configuration Stuff */ static channelTypes() { return ['ABSTRACT'] } static configurationItems() { return {} } static validate(configurationItem) { return false } static serviceDescription() { return 'You should never see this' } // Servicelist will sort on that static getPriority() { return 0 } static filterDevice() { return [] } /** ****** serialization */ _configureInformationService() { let informationService = this.getService(Service.AccessoryInformation) informationService.setCharacteristic(Characteristic.Name, this._name) informationService.setCharacteristic(Characteristic.Manufacturer, this.getManufacturer()) informationService.setCharacteristic(Characteristic.Model, this._ccuType || 'Generic') informationService.setCharacteristic(Characteristic.SerialNumber, os.hostname() + '_' + this._serial) } _dictionaryPresentation() { var result = {} result.displayName = this.displayName result.UUID = this.getUUID() var services = [] var linkedServices = {} for (var index in this.services) { var service = this.services[index] var servicePresentation = {} servicePresentation.displayName = service.displayName servicePresentation.UUID = service.UUID servicePresentation.subtype = service.subtype var linkedServicesPresentation = [] for (var linkedServiceIdx in service.linkedServices) { var linkedService = service.linkedServices[linkedServiceIdx] linkedServicesPresentation.push(linkedService.UUID + (linkedServices.subtype || '')) } linkedServices[service.UUID + (service.subtype || '')] = linkedServicesPresentation var characteristics = [] for (var cIndex in service.characteristics) { var characteristic = service.characteristics[cIndex] var characteristicPresentation = {} characteristicPresentation.displayName = characteristic.displayName characteristicPresentation.UUID = characteristic.UUID characteristicPresentation.props = characteristic.props characteristicPresentation.value = characteristic.value characteristicPresentation.eventOnlyCharacteristic = characteristic.eventOnlyCharacteristic characteristics.push(characteristicPresentation) } servicePresentation.characteristics = characteristics services.push(servicePresentation) } result.linkedServices = linkedServices result.services = services result.name = this._name result.interface = this._interf result.serial = this._serial result.channel = this._channelnumber result.type = this._ccuType result.instanceID = this.instanceID result.serviceClass = this.serviceClass result.settings = this.settings result.isPublished = this.isPublished result.nameInCCU = this.nameInCCU return result } } module.exports = HomeMaticAccessory