UNPKG

homebridge-plc

Version:

Homebridge plugin for SIEMENS Step7 and compatible PLCs. (https://github.com/homebridge)

1,235 lines (1,116 loc) 181 kB
/* * (c) 2020-2025 Feilner */ var PlatformAccessory, Service, Characteristic, UUIDGen; var snap7 = require('node-snap7'); var url = require('url'); var http = require('http'); // Exports module.exports = function(homebridge) { Service = homebridge.hap.Service; Characteristic = homebridge.hap.Characteristic; UUIDGen = homebridge.hap.uuid; PlatformAccessory = homebridge.platformAccessory; homebridge.registerPlatform('homebridge-plc', 'PLC', PLC_Platform); }; function PLC_Platform(log, config, api) { this.log = log; this.config = config; this.api = api; this.s7PlatformAccessories = []; this.S7Client = new snap7.S7Client(); this.isConnectOngoing = false; this.S7ClientConnect(); } PLC_Platform.prototype = { accessories: function(callback) { var log = this.log; if (typeof(this.config.defaultPollInterval) === 'undefined' || this.config.defaultPollInterval === null || this.config.defaultPollInterval < 1) { this.config.defaultPollInterval = 10; } log.info("Add PLC accessories..."); // Create accessory for each configuration for (let index = 0; index < this.config.accessories.length; index++) { var config = this.config.accessories[index]; var accessoryNumber = index + 1; var numberOfAccessories = this.config.accessories.length; if (!('name' in config)) { log.error("[" + String(accessoryNumber) + "/" + String(numberOfAccessories) + "] Missing name in config and was not added!"); continue; } var removedOptions = ['minValue', 'maxValue', 'minStep', 'minHumidityValue', 'maxHumidityValue', 'minHumidityStep', 'mapGetCurrent', 'mapGetTarget', 'mapSetTarget', 'invert', 'set_Secured', 'set_Unsecured', 'forceCurrentState', 'set_Deactivate', 'set_Off', 'mapSet', 'mapGet']; var removedOptionsLockMechanismBool = ['get_LockCurrentState', 'get_LockTargetState', 'set_LockTargetState', 'set_Secured', 'set_Unsecured']; var hasRemovedOption = false; removedOptions.forEach((item) => { if (item in config) { log.warn("[" + config.name + "] Parameter " + item + " was renamed, please update your config"); hasRemovedOption = true; } }); if (config.accessory == 'PLC_LockMechanismBool') { removedOptionsLockMechanismBool.forEach((item) => { if (item in config) { log.warn("[" + config.name + "] Parameter " + item + " was renamed, please update your config"); hasRemovedOption = true; } }); } if (hasRemovedOption) { log.error("[" + String(accessoryNumber) + "/" + String(numberOfAccessories) + "] " + config.name + " (" + config.accessory + ") needs update of config and was not added!"); } else if ('disable' in config && !config.disable) { log.warn("[" + String(accessoryNumber) + "/" + String(numberOfAccessories) + "] " + config.name + " (" + config.accessory + ") is disabled!"); } else { log.info("[" + String(accessoryNumber) + "/" + String(numberOfAccessories) + "] " + config.name + " (" + config.accessory + ")"); // Call accessory construction var accessory = new GenericPLCAccessory(this, config, accessoryNumber); this.s7PlatformAccessories.push(accessory); } } callback(this.s7PlatformAccessories); if (this.config.enablePolling) { log.info("Enable polling..."); setInterval(function(param) { this.pollLoop(this.s7PlatformAccessories); }.bind(this), 1000); } if (this.config.enablePush || this.config.enableControl) { this.port = this.config.port || 8888; this.api.on('didFinishLaunching', () => { if (this.config.enablePush && this.config.enableControl) { this.log.info('Enable push and control server...'); } else if (this.config.enablePush) { this.log.info('Enable push server...'); } else { this.log.info('Enable control server...'); } this.listener = http.createServer((req, res) => this.httpListener(req, res)); this.listener.listen(this.port); this.log.info('Listening on port ' + this.port); }); } log.info("Init done!"); }, pollLoop: function(s7PlatformAccessories) { s7PlatformAccessories.forEach((accessory) => { accessory.poll(); }); }, forwardHTTP: function(logprefix, url) { http.get(url, (resp) => { if (resp.statusCode !== 200) { this.log.error(logprefix + " Forward failed with HTTP status: " + resp.statusCode); return; } }).on('error', function(e) { this.log.error(logprefix + " Forward failed: " + e.message); }.bind(this)); }, httpListener: function(req, res) { var data = ''; var url = ''; if (req.method == 'POST') { req.on('data', (chunk) => { data += chunk; }); req.on('end', () => { this.log.info('Received POST and body data:'); this.log.info(data.toString()); }); } else if (req.method == 'PUT' || req.method == 'GET') { req.on('end', () => { url = require('url').parse(req.url, true); // Will parse parameters into query string if (this.config.enablePush && 'push' in url.query && 'db' in url.query && 'offset' in url.query && 'value' in url.query) { this.log.debug("[HTTP Push] (" + req.socket.remoteAddress + ") Received update for accessory: db:" + url.query.db + " offset:" + url.query.offset + " value:" + url.query.value); var db = parseInt(url.query.db); var offset = parseFloat(url.query.offset); var value = url.query.value; var offsetHandled = false; var dbHandled = false; this.s7PlatformAccessories.forEach((accessory) => { if (accessory.config.db == db) { dbHandled = true; offsetHandled = accessory.updatePush(offset, value) || offsetHandled; } }); if (typeof(this.config.mirror) != 'undefined' && this.config.mirror) { this.forwardHTTP("[HTTP Push]", this.config.mirror + req.url); } if (!dbHandled) { if (typeof(this.config.forward) != 'undefined' && this.config.forward) { this.forwardHTTP("[HTTP Push]", this.config.forward + req.url); } else { this.log.warn("[HTTP Push] (" + req.socket.remoteAddress + ") No accessory configured for db:" + url.query.db + " offset:" + url.query.offset + " value:" + url.query.value); } } else if (!offsetHandled) { this.log.warn("[HTTP Push] (" + req.socket.remoteAddress + ") Offset not configured for accessory db:" + url.query.db + " offset:" + url.query.offset + " value:" + url.query.value); } } else if (this.config.enableControl && 'control' in url.query && 'db' in url.query && 'offset' in url.query && 'value' in url.query) { this.log.debug("[HTTP Control] (" + req.socket.remoteAddress + ") Received control request for accessory: db:" + url.query.db + " offset:" + url.query.offset + " value:" + url.query.value); var db = parseInt(url.query.db); var offset = parseFloat(url.query.offset); var value = url.query.value; var offsetHandled = false; var dbHandled = false; this.s7PlatformAccessories.forEach((accessory) => { if (accessory.config.db == db) { dbHandled = true; offsetHandled = accessory.updateControl(offset, value) || offsetHandled; } }); if (!dbHandled) { if (typeof(this.config.forward) != 'undefined' && this.config.forward) { this.forwardHTTP("[HTTP Control]", this.config.forward + req.url); } else { this.log.warn("[HTTP Control] (" + req.socket.remoteAddress + ") No accessory configured for db:" + url.query.db + " offset:" + url.query.offset + " value:" + url.query.value); } } else if (!offsetHandled) { this.log.warn("[HTTP Control] (" + req.socket.remoteAddress + ") Offset not configured for accessory db:" + url.query.db + " offset:" + url.query.offset + " value:" + url.query.value); } } else { if (!this.config.enablePush && 'push' in url.query) { this.log.warn("[HTTP Push] (" + req.socket.remoteAddress + ") enablePush is not set in platform config!"); } else if (!this.config.enableControl && 'control' in url.query) { this.log.warn("[HTTP Control] (" + req.socket.remoteAddress + ") enableControl is not set in platform config!"); } else if (!('push' in url.query) && !('control' in url.query)) { this.log.warn("[HTTP Push/Control] (" + req.socket.remoteAddress + ") unknown operation: " + req.url); } else if (!('db' in url.query)) { this.log.warn("[HTTP Push/Control] (" + req.socket.remoteAddress + ") parameter db is missing in url: " + req.url); } else if (!('offset' in url.query)) { this.log.warn("[HTTP Push/Control] (" + req.socket.remoteAddress + ") parameter offset is missing in url: " + req.url); } else if (!('value' in url.query)) { this.log.warn("[HTTP Push/Control] (" + req.socket.remoteAddress + ") parameter value is missing in url: " + req.url); } } }); } res.writeHead(200, { 'Content-Type': 'text/plain' }); res.end(); }, mirrorGet: function(logprefix, parameter) { if ('mirror' in this.config && this.config.mirror) { var url = this.config.mirror + "/?push" + parameter; require('http').put(url, (resp) => { if (resp.statusCode !== 200) { this.log.error(logprefix, "Mirror failed (" + url + "): HTTP status " + resp.statusCode); return; } }).on('error', function(e) { this.log.error(logprefix, "Mirror failed (" + url + "): " + e.message); }.bind(this)); } return; }, // PLC connection check function S7ClientConnect: function() { let typeName = ["invalid", "PG-Communication", "OP-Communication"]; var log = this.log; var S7Client = this.S7Client; var ip = this.config.ip; var rack = this.config.rack; var slot = this.config.slot; var type = S7Client.CONNTYPE_PG; var rv = false; if ('communicationOP' in this.config && this.config.communicationOP) { type = S7Client.CONNTYPE_OP; } if (S7Client.Connected()) { rv = true; } else { log.info("Connecting to %s (%s:%s) %s", ip, rack, slot, typeName[type]); if (!this.isConnectOngoing) { this.isConnectOngoing = true; var ok = S7Client.SetConnectionType(type); if (ok) { ok = S7Client.ConnectTo(ip, rack, slot); this.isConnectOngoing = false; if (ok) { log.info("Connected to %s (%s:%s) %s", ip, rack, slot, typeName[type]); rv = true; } else { log.error("Connection to %s (%s:%s) failed", ip, rack, slot); } } else { this.isConnectOngoing = false; log.error("Set connection type to %s (%s:%s) %s failed", ip, rack, slot, typeName[type]); } } } return rv; } }; function GenericPLCAccessory(platform, config, accessoryNumber) { this.platform = platform; this.log = platform.log; this.name = config.name; var accessoryUUID = UUIDGen.generate(config.name + config.accessory); this.config = config; this.accessory = new PlatformAccessory(this.name, accessoryUUID); this.modFunctionGet = this.plain; this.modFunctionSet = this.plain; if ('enablePolling' in platform.config && platform.config.enablePolling && config.enablePolling) { this.pollActive = true; this.pollInterval = config.pollInterval || platform.config.defaultPollInterval; if (platform.config.distributePolling) { this.pollCounter = (accessoryNumber % this.pollInterval) + 1; } else { this.pollCounter = this.pollInterval; } this.log.debug("Polling enabled interval " + this.pollInterval + "s. First polling is done in " + this.pollCounter + "s"); } // INIT handling /////////////////////////////////////////////// // Lightbulb, Outlet, Switch //////////////////////////////////////////////////////////////// if (config.accessory == 'PLC_LightBulb' || config.accessory == 'PLC_Outlet' || config.accessory == 'PLC_Switch') { if (config.accessory == 'PLC_Outlet') { this.service = new Service.Outlet(this.name); } else if (config.accessory == 'PLC_Switch') { this.service = new Service.Switch(this.name); } else { this.service = new Service.Lightbulb(this.name); } this.accessory.addService(this.service); this.initOn(true); if (config.accessory == 'PLC_LightBulb') { if ('get_Brightness' in config) { this.service.getCharacteristic(Characteristic.Brightness) .on('get', function(callback) { this.getByte(callback, config.db, config.get_Brightness, 'get Brightness' ); }.bind(this)) .on('set', function(value, callback) { this.setByte(value, callback, config.db, config.set_Brightness, 'set Brightness' ); }.bind(this)) .setProps({ minValue: ('minBrightnessValue' in config) ? config.minBrightnessValue : 0, maxValue: ('maxBrightnessValue' in config) ? config.maxBrightnessValue : 100, minStep: ('minBrightnessStep' in config) ? config.minBrightnessStep : 1 }); } } } // INIT handling /////////////////////////////////////////////// // TemperatureSensor //////////////////////////////////////////////////////////////// else if (config.accessory == 'PLC_TemperatureSensor') { this.service = new Service.TemperatureSensor(this.name); this.accessory.addService(this.service); this.initCurrentTemperature(true); this.initStatusTampered(); this.initStatusLowBattery(); } // INIT handling /////////////////////////////////////////////// // HumiditySensor //////////////////////////////////////////////////////////////// else if (config.accessory == 'PLC_HumiditySensor') { this.service = new Service.HumiditySensor(this.name); this.accessory.addService(this.service); this.initCurrentRelativeHumidity(true); this.initStatusTampered(); this.initStatusLowBattery(); } // INIT handling /////////////////////////////////////////////// // Thermostat //////////////////////////////////////////////////////////////// else if (config.accessory == 'PLC_Thermostat') { this.service = new Service.Thermostat(this.name); this.accessory.addService(this.service); var informFunction = function(notUsed) { // Update target state and current state value. this.service.getCharacteristic(Characteristic.TargetHeatingCoolingState).handleGetRequest().then(value => { this.service.getCharacteristic(Characteristic.TargetHeatingCoolingState).updateValue(value); }).catch(err => { this.log.error("[" + this.name + "] Error during inform", err); }); this.service.getCharacteristic(Characteristic.CurrentHeatingCoolingState).handleGetRequest().then(value => { this.service.getCharacteristic(Characteristic.CurrentHeatingCoolingState).updateValue(value); }).catch(err => { this.log.error("[" + this.name + "] Error during inform", err); }); }.bind(this); if ('mapSetTargetHeatingCoolingState' in config && config.mapSetTargetHeatingCoolingState) { this.modFunctionSet = function(value) { return this.mapFunction(value, config.mapSetTargetHeatingCoolingState); }.bind(this); } if ('mapGetTargetHeatingCoolingState' in config && config.mapGetTargetHeatingCoolingState) { this.modFunctionGet = function(value) { return this.mapFunction(value, config.mapGetTargetHeatingCoolingState); }.bind(this); } this.modFunctionGetCurrent = this.plain; if ('mapGetCurrentHeatingCoolingState' in config && config.mapGetCurrentHeatingCoolingState) { this.modFunctionGetCurrent = function(value) { return this.mapFunction(value, config.mapGetCurrentHeatingCoolingState); }.bind(this); } if ('get_CurrentHeatingCoolingState' in config) { this.service.getCharacteristic(Characteristic.CurrentHeatingCoolingState) .on('get', function(callback) { this.getByte(callback, config.db, config.get_CurrentHeatingCoolingState, 'get CurrentHeatingCoolingState', this.modFunctionGetCurrent ); }.bind(this)); } else { this.service.getCharacteristic(Characteristic.CurrentHeatingCoolingState) .on('get', function(callback) { this.getDummy(callback, 1, // Currently return fixed value inactive=0, idle=1, heating=2, cooling=3 'get CurrentHeatingCoolingState' ); }.bind(this)); } if ('get_TargetHeatingCoolingState' in config) { this.service.getCharacteristic(Characteristic.TargetHeatingCoolingState) .on('get', function(callback) { this.getByte(callback, config.db, config.get_TargetHeatingCoolingState, 'get TargetHeatingCoolingState', this.modFunctionGet ); }.bind(this)) .on('set', function(value, callback) { this.setByte(value, callback, config.db, config.set_TargetHeatingCoolingState, 'set TargetHeatingCoolingState', informFunction, this.modFunctionSet ); }.bind(this)); } else { this.service.getCharacteristic(Characteristic.TargetHeatingCoolingState) .on('get', function(callback) { this.getDummy(callback, 3, // Currently return fixed value off=0, heat=1, cool=2, automatic=3 'get TargetHeatingCoolingState' ); }.bind(this)) .on('set', function(value, callback) { this.setDummy(value, callback, 'set TargetHeatingCoolingState', // Ignore set and return current fixed values informFunction ); }.bind(this)); } this.service.getCharacteristic(Characteristic.TemperatureDisplayUnits) .on('get', function(callback) { this.getDummy(callback, 0, // Currently return fixed value celsius=0, fahrenheit=1 'get TemperatureDisplayUnits' ); }.bind(this)) .on('set', function(value, callback) { this.setDummy(value, callback, 'set TemperatureDisplayUnits' ); }.bind(this)); this.initCurrentTemperature(true); if ('get_TargetTemperature' in config && 'set_TargetTemperature' in config) { this.service.getCharacteristic(Characteristic.TargetTemperature) .on('get', function(callback) { this.getReal(callback, config.db, config.get_TargetTemperature, 'get TargetTemperature' ); }.bind(this)) .on('set', function(value, callback) { this.setReal(value, callback, config.db, config.set_TargetTemperature, 'set TargetTemperature' ); }.bind(this)) .setProps({ minValue: ('minTargetTemperatureValue' in config) ? config.minTargetTemperatureValue : 10, maxValue: ('maxTargetTemperatureValue' in config) ? config.maxTargetTemperatureValue : 38, minStep: ('minTargetTemperatureStep' in config) ? config.minTargetTemperatureStep : 0.1 }); } else { this.log.error("Mandatory config get_TargetTemperature or set_TargetTemperature missing"); this.service.getCharacteristic(Characteristic.TargetTemperature) .on('get', function(callback) { this.getDummy(callback, 20, 'get TargetTemperature' ); }.bind(this)) .on('set', function(value, callback) { this.setDummy(value, callback, 'set TargetTemperature', // Ignore set and return current fixed values informFunction ); }.bind(this)); } this.initCurrentRelativeHumidity(false); if ('get_TargetRelativeHumidity' in config) { this.service.getCharacteristic(Characteristic.TargetRelativeHumidity) .on('get', function(callback) { this.getReal(callback, config.db, config.get_TargetRelativeHumidity, 'get TargetRelativeHumidity' ); }.bind(this)) .on('set', function(value, callback) { this.setReal(value, callback, config.db, config.set_TargetRelativeHumidity, 'set TargetRelativeHumidity' ); }.bind(this)) .setProps({ minValue: ('minTargetHumidityValue' in config) ? config.minTargetHumidityValue : 0, maxValue: ('maxTargetHumidityValue' in config) ? config.maxTargetHumidityValue : 100, minStep: ('minTargetHumidityStep' in config) ? config.minTargetHumidityStep : 1 }); } // This will generate a warning but will work anyway. this.initStatusTampered(); // This will generate a warning but will work anyway. this.initStatusLowBattery(); } // INIT handling /////////////////////////////////////////////// // Humidifier Dehumidifier //////////////////////////////////////////////////////////////// else if (config.accessory == 'PLC_HumidifierDehumidifier') { this.service = new Service.HumidifierDehumidifier(this.name); this.accessory.addService(this.service); var informFunction = function(notUsed) { // Update target state and current state value. this.service.getCharacteristic(Characteristic.TargetHumidifierDehumidifierState).handleGetRequest().then(value => { this.service.getCharacteristic(Characteristic.TargetHumidifierDehumidifierState).updateValue(value); }).catch(err => { this.log.error("[" + this.name + "] Error during inform", err); }); this.service.getCharacteristic(Characteristic.CurrentHumidifierDehumidifierState).handleGetRequest().then(value => { this.service.getCharacteristic(Characteristic.CurrentHumidifierDehumidifierState).updateValue(value); }).catch(err => { this.log.error("[" + this.name + "] Error during inform", err); }); }.bind(this); if ('mapSetTargetHumidifierDehumidifierState' in config && config.mapSetTargetHumidifierDehumidifierState) { this.modFunctionSet = function(value) { return this.mapFunction(value, config.mapSetTargetHumidifierDehumidifierState); }.bind(this); } if ('mapGetTargetHumidifierDehumidifierState' in config && config.mapGetTargetHumidifierDehumidifierState) { this.modFunctionGet = function(value) { return this.mapFunction(value, config.mapGetTargetHumidifierDehumidifierState); }.bind(this); } this.modFunctionGetCurrent = this.plain; if ('mapGetCurrentHumidifierDehumidifierState' in config && config.mapGetCurrentHumidifierDehumidifierState) { this.modFunctionGetCurrent = function(value) { return this.mapFunction(value, config.mapGetCurrentHumidifierDehumidifierState); }.bind(this); } if ('get_CurrentHumidifierDehumidifierState' in config) { this.service.getCharacteristic(Characteristic.CurrentHumidifierDehumidifierState) .on('get', function(callback) { this.getByte(callback, config.db, config.get_CurrentHumidifierDehumidifierState, 'get CurrentHumidifierDehumidifierState', this.modFunctionGetCurrent ); }.bind(this)); } else { this.service.getCharacteristic(Characteristic.CurrentHumidifierDehumidifierState) .on('get', function(callback) { this.getDummy(callback, 1, // Currently return fixed value inactive=0, idle=1, humidifying=2, dehumidifying=3 'get CurrentHumidifierDehumidifierState' ); }.bind(this)); } if ('get_TargetHumidifierDehumidifierState' in config) { this.service.getCharacteristic(Characteristic.TargetHumidifierDehumidifierState) .on('get', function(callback) { this.getByte(callback, config.db, config.get_TargetHumidifierDehumidifierState, 'get TargetHumidifierDehumidifierState', this.modFunctionGet ); }.bind(this)) .on('set', function(value, callback) { this.setByte(value, callback, config.db, config.set_TargetHumidifierDehumidifierState, 'set TargetHumidifierDehumidifierState', informFunction, this.modFunctionSet ); }.bind(this)); } else { this.service.getCharacteristic(Characteristic.TargetHumidifierDehumidifierState) .on('get', function(callback) { this.getDummy(callback, config.default_TargetHumidifierDehumidifierState || 0, // Currently return fixed value auto=0, humidifier=1, dehumidifier=2 'get TargetHeatingCoolingState' ); }.bind(this)) .on('set', function(value, callback) { this.setDummy(value, callback, 'set TargetHeatingCoolingState', // Ignore set and return current fixed values informFunction ); }.bind(this)); } if ('get_RelativeHumidityDehumidifierThreshold' in config) { this.service.getCharacteristic(Characteristic.RelativeHumidityDehumidifierThreshold) .on('get', function(callback) { this.getReal(callback, config.db, config.get_RelativeHumidityDehumidifierThreshold, 'get RelativeHumidityDehumidifierThreshold' ); }.bind(this)) .on('set', function(value, callback) { this.setReal(value, callback, config.db, config.set_RelativeHumidityDehumidifierThreshold, 'set RelativeHumidityDehumidifierThreshold' ); }.bind(this)); } if ('get_RelativeHumidityHumidifierThreshold' in config) { this.service.getCharacteristic(Characteristic.RelativeHumidityHumidifierThreshold) .on('get', function(callback) { this.getReal(callback, config.db, config.get_RelativeHumidityHumidifierThreshold, 'get RelativeHumidityHumidifierThreshold' ); }.bind(this)) .on('set', function(value, callback) { this.setReal(value, callback, config.db, config.set_RelativeHumidityHumidifierThreshold, 'set RelativeHumidityHumidifierThreshold' ); }.bind(this)); } if ('get_RotationSpeed' in config) { this.service.getCharacteristic(Characteristic.RotationSpeed) .on('get', function(callback) { this.getReal(callback, config.db, config.get_RotationSpeed, 'get RotationSpeed' ); }.bind(this)) .on('set', function(value, callback) { this.setReal(value, callback, config.db, config.set_RotationSpeed, 'set RotationSpeed' ); }.bind(this)); } else if ('get_RotationSpeedByte' in config) { this.service.getCharacteristic(Characteristic.RotationSpeed) .on('get', function(callback) { this.getByte(callback, config.db, config.get_RotationSpeedByte, 'get RotationSpeed' ); }.bind(this)) .on('set', function(value, callback) { this.setByte(value, callback, config.db, config.set_RotationSpeedByte, 'set RotationSpeed' ); }.bind(this)); } this.initCurrentRelativeHumidity(true); this.initActive(true); if ('get_SwingMode' in config) { this.service.getCharacteristic(Characteristic.SwingMode) .on('get', function(callback) { this.getByte(callback, config.db, config.get_SwingMode, 'get SwingMode' ); }.bind(this)) .on('set', function(value, callback) { this.setByte(value, callback, config.db, config.set_SwingMode, 'set SwingMode' ); }.bind(this)); } if ('get_WaterLevel' in config) { this.service.getCharacteristic(Characteristic.WaterLevel) .on('get', function(callback) { this.getReal(callback, config.db, config.get_WaterLevel, 'get WaterLevel' ); }.bind(this)); } // This will generate a warning but will work anyway. this.initStatusTampered(); // This will generate a warning but will work anyway. this.initStatusLowBattery(); } // INIT handling /////////////////////////////////////////////// // Window, WindowCovering and Door //////////////////////////////////////////////////////////////// else if (config.accessory == 'PLC_Window' || config.accessory == 'PLC_WindowCovering' || config.accessory == 'PLC_Door') { if (config.accessory == 'PLC_Window') { this.service = new Service.Window(this.name); } else if (config.accessory == 'PLC_WindowCovering') { this.service = new Service.WindowCovering(this.name); } else { this.service = new Service.Door(this.name); } this.accessory.addService(this.service); this.lastTargetPos = 0; this.modFunctionGetCurrent = this.plain; this.modFunctionGetTarget = this.plain; this.modFunctionSetTarget = this.plain; // Default do nothing after set of target position var informFunction = function(value) {}.bind(this); if ('forceCurrentPosition' in config && config.forceCurrentPosition) { informFunction = function(value) { this.service.getCharacteristic(Characteristic.CurrentPosition).updateValue(value); }.bind(this); } if ('enablePolling' in platform.config && platform.config.enablePolling) { if ('adaptivePolling' in config && config.adaptivePolling) { // High frequency polling during home app triggered movement this.adaptivePollActive = false; this.adaptivePollingInterval = config.adaptivePollingInterval || 1; this.pollCounter = this.adaptivePollingInterval; this.log.debug("Adaptive polling enabled interval " + this.adaptivePollingInterval + "s"); // When execution set save target position and enable polling with high frequency informFunction = function(value) { this.lastTargetPos = value; this.pollCounter = this.adaptivePollingInterval; this.adaptivePollActive = true; }.bind(this); } } if ('invertPosition' in config && config.invertPosition) { this.modFunctionGetCurrent = this.invert_0_100; this.modFunctionGetTarget = this.invert_0_100; this.modFunctionSetTarget = this.invert_0_100; } if ('mapGetCurrentPosition' in config && config.mapGetCurrentPosition) { this.modFunctionGetCurrent = function(value) { return this.mapFunction(value, config.mapGetCurrentPosition); }.bind(this); } if ('mapGetTargetPosition' in config && config.mapGetTargetPosition) { this.modFunctionGetTarget = function(value) { return this.mapFunction(value, config.mapGetTargetPosition); }.bind(this); } if ('mapSetTargetPosition' in config && config.mapSetTargetPosition) { this.modFunctionSetTarget = function(value) { return this.mapFunction(value, config.mapSetTargetPosition); }.bind(this); } // Create handlers for required characteristics this.service.getCharacteristic(Characteristic.CurrentPosition) .on('get', function(callback) { this.getByte(callback, config.db, config.get_CurrentPosition, 'get CurrentPosition', this.modFunctionGetCurrent ); }.bind(this)); if ('get_TargetPosition' in config) { // Windows or WindowCover can be electrically moved this.service.getCharacteristic(Characteristic.TargetPosition) .on('get', function(callback) { this.getByte(callback, config.db, config.get_TargetPosition, 'get TargetPosition', this.modFunctionGetTarget ); }.bind(this)) .on('set', function(value, callback) { this.setByte(value, callback, config.db, config.set_TargetPosition, 'set TargetPosition', informFunction, this.modFunctionSetTarget ); }.bind(this)); } else { // Not possible to give a target position; always use current position as target position. this.service.getCharacteristic(Characteristic.TargetPosition) .on('get', function(callback) { this.getByte(callback, config.db, config.get_CurrentPosition, // Always use current position as target position 'get CurrentPosition', this.modFunctionGetCurrent ); }.bind(this)) .on('set', function(value, callback) { this.setDummy(value, callback, 'set TargetPosition', function(value) { // Ignore new target value; instead get current value and use it as target position this.service.getCharacteristic(Characteristic.CurrentPosition).handleGetRequest().then(value => { this.service.getCharacteristic(Characteristic.TargetPosition).updateValue(value); }).catch(err => { this.log.error("[" + this.name + "] Error during inform", err); }); }.bind(this) ); }.bind(this)); } if ('get_PositionState' in config) { this.service.getCharacteristic(Characteristic.PositionState) .on('get', function(callback) { this.getByte(callback, config.db, config.get_PositionState, 'get PositionState' ); }.bind(this)); } else { this.service.getCharacteristic(Characteristic.PositionState) .on('get', function(callback) { this.getDummy(callback, 2, 'get PositionState' ); }.bind(this)); } if ('set_HoldPosition' in config) { this.service.getCharacteristic(Characteristic.HoldPosition) .on('set', function(value, callback) { this.setBit(value, callback, config.db, Math.floor(config.set_HoldPosition), Math.floor((config.set_HoldPosition * 10) % 10), 'set HoldPosition' ); }.bind(this)); } else { this.service.getCharacteristic(Characteristic.HoldPosition) .on('set', function(callback) { this.handleDummy(callback, 'set HoldPosition' ); }.bind(this)); } } // INIT handling /////////////////////////////////////////////// // OccupancySensor //////////////////////////////////////////////////////////////// else if (config.accessory == 'PLC_OccupancySensor') { this.service = new Service.OccupancySensor(this.name); this.accessory.addService(this.service); if ('invertOccupancy' in config && config.invertOccupancy) { this.modFunctionGet = this.invert_bit; } this.service.getCharacteristic(Characteristic.OccupancyDetected) .on('get', function(callback) { this.getBit(callback, config.db, Math.floor(config.get_OccupancyDetected), Math.floor((config.get_OccupancyDetected * 10) % 10), "get OccupancyDetected", this.modFunctionGet ); }.bind(this)); this.initStatusTampered(); this.initStatusLowBattery(); } // INIT handling /////////////////////////////////////////////// // MotionSensor //////////////////////////////////////////////////////////////// else if (config.accessory == 'PLC_MotionSensor') { this.service = new Service.MotionSensor(this.name); this.accessory.addService(this.service); if ('invertMotionDetected' in config && config.invertMotionDetected) { this.modFunctionGet = this.invert_bit; } this.service.getCharacteristic(Characteristic.MotionDetected) .on('get', function(callback) { this.getBit(callback, config.db, Math.floor(config.get_MotionDetected), Math.floor((config.get_MotionDetected * 10) % 10), "get MotionDetected", this.modFunctionGet ); }.bind(this)); this.initStatusTampered(); this.initStatusLowBattery(); } // INIT handling /////////////////////////////////////////////// // ContactSensor //////////////////////////////////////////////////////////////// else if (config.accessory == 'PLC_ContactSensor') { this.service = new Service.ContactSensor(this.name); this.accessory.addService(this.service); if ('invertContactSensorState' in config && config.invertContactSensorState) { this.modFunctionGet = this.invert_bit; } this.service.getCharacteristic(Characteristic.ContactSensorState) .on('get', function(callback) { this.getBit(callback, config.db, Math.floor(config.get_ContactSensorState), Math.floor((config.get_ContactSensorState * 10) % 10), "get get_ContactSensorState", this.modFunctionGet ); }.bind(this)); this.initStatusTampered(); this.initStatusLowBattery(); } // INIT handling /////////////////////////////////////////////// // LeakSensor //////////////////////////////////////////////////////////////// else if (config.accessory == 'PLC_LeakSensor') { this.service = new Service.LeakSensor(this.name); this.accessory.addService(this.service); if ('invertLeakDetected' in config && config.invertLeakDetected) { this.modFunctionGet = this.invert_bit; } this.service.getCharacteristic(Characteristic.LeakDetected) .on('get', function(callback) { this.getBit(callback, config.db, Math.floor(config.get_LeakDetected), Math.floor((config.get_LeakDetected * 10) % 10), "get LeakDetected", this.modFunctionGet ); }.bind(this)); this.initStatusTampered(); this.initStatusLowBattery(); } // INIT handling /////////////////////////////////////////////// // Faucet //////////////////////////////////////////////////////////////// else if (config.accessory == 'PLC_Faucet') { this.service = new Service.Faucet(this.name); this.accessory.addService(this.service); this.initActive(true); } // INIT handling /////////////////////////////////////////////// // Valve //////////////////////////////////////////////////////////////// else if (config.accessory == 'PLC_Valve') { this.service = new Service.Valve(this.name); this.accessory.addService(this.service); informFunction = function(value) { this.service.getCharacteristic(Characteristic.InUse).updateValue(value); }.bind(this); this.initActive(true, informFunction); this.service.getCharacteristic(Characteristic.InUse) .on('get', function(callback) { this.getBit(callback, config.db, Math.floor(config.get_Active), Math.floor((config.get_Active * 10) % 10), 'get InUse' ); }.bind(this)); if ('ValveType' in config) { this.service.getCharacteristic(Characteristic.ValveType) .on('get', function(callback) { this.getDummy(callback, config.ValveType, 'get ValveType' ); }.bind(this)); } if ('get_RemainingDuration' in config) { this.service.getCharacteristic(Characteristic.RemainingDuration) .on('get', function(callback) { this.getDInt(callback, config.db, config.get_RemainingDuration, "get RemainingDuration", this.s7time2int ); }.bind(this)); } if ('get_SetDuration' in config && 'set_SetDuration' in config) { this.service.getCharacteristic(Characteristic.SetDuration) .on('get', function(callback) { this.getDInt(callback, config.db, config.get_SetDuration, "get SetDuration", this.s7time2int ); }.bind(this)) .on('set', function(value, callback) { this.setDInt(value, callback, config.db, config.set_SetDuration, "set SetDuration", function(value) { this.service.getCharacteristic(Characteristic.RemainingDuration).updateValue(value); }.bind(this), this.int27time ); }.bind(this)); } } // INIT handling /////////////////////////////////////////////// // SecuritySystem //////////////////////////////////////////////////////////////// else if (config.accessory == 'PLC_SecuritySystem') { this.service = new Service.SecuritySystem(this.name); this.accessory.addService(this.service); this.modFunctionGetCurrent = this.plain; var informFunction = function(notUsed) { // Get the current target system state and update the value. this.service.getCharacteristic(Characteristic.SecuritySystemTargetState).handleGetRequest().then(value => { this.service.getCharacteristic(Characteristic.SecuritySystemTargetState).updateValue(value); }).catch(err => { this.log.error("[" + this.name + "] Error during inform", err); }); // Get the current system state and update the value. this.service.getCharacteristic(Characteristic.SecuritySystemCurrentState).handleGetRequest().then(value => { this.service.getCharacteristic(Characteristic.SecuritySystemCurrentState).updateValue(value); }).catch(err => { this.log.error("[" + this.name + "] Error during inform", err); }); }.bind(this); if ('mapSetSecuritySystemTargetState' in config && config.mapSetSecuritySystemTargetState) { this.modFunctionSet = function(value) { return this.mapFunction(value, config.mapSetSecuritySystemTargetState); }.bind(this); } if ('mapGetSecuritySystemTargetState' in config && config.mapGetSecuritySystemTargetState) { this.modFunctionGet = function(value) { return this.mapFunction(value, config.mapGetSecuritySystemTargetState); }.bind(this); } if ('mapGetSecuritySystemCurrentState' in config && config.mapGetSecuritySystemCurrentState) { this.modFunctionGetCurrent = function(value) { return this.mapFunction(value, config.mapGetSecuritySystemCurrentState); }.bind(this); } this.service.getCharacteristic(Characteristic.SecuritySystemCurrentState) .on('get', function(callback) { this.getByte(callback, config.db, config.get_SecuritySystemCurrentState, "get SecuritySystemCurrentState", this.modFunctionGetCurrent ); }.bind(this)); this.service.getCharacteristic(Characteristic.SecuritySystemTargetState) .on('get', function(callback) { this.getByte(callback, config.db, config.get_SecuritySystemTargetState, "get SecuritySystemTargetState", this.modFunctionGet ); }.bind(this)) .on('set', function(value, callback) { this.setByte(value, callback, config.db, config.set_SecuritySystemTargetState, "set SecuritySystemTargetState", informFunction, this.modFunctionSet ); }.bind(this)); } // INIT handling /////////////////////////////////////////////// // StatelessProgrammableSwitch, Doorbell //////////////////////////////////////////////////////////////// else if (config.accessory == 'PLC_StatelessProgrammableSwitch' || config.accessory == 'PLC_Doorbell') { if (config.accessory == 'PLC_StatelessProgrammableSwitch') { this.service = new Service.StatelessProgrammableSwitch(this.name); } else { this.service = new Service.Doorbell(this.name); } this.accessory.addService(this.service); this.service.getCharacteristic(Characteristic.ProgrammableSwitchEvent) .on('get', function(callback) { this.getByte(callback, config.db, config.get_ProgrammableSwitchEvent, "get ProgrammableSwitchEvent" ); }.bind(this)); if (config.accessory == 'PLC_StatelessProgrammableSwitch') { this.service.getCharacteristic(Characteristic.ServiceLabelIndex) .on('get', function(callback) { this.getDummy(callback, 1, "get ServiceLabelIndex" ); }.bind(this)); } } // INIT handling /////////////////////////////////////////////// // LockMechanism //////////////////////////////////////////////////////////////// else if (config.accessory == 'PLC_LockMechanism') { this.service = new Service.LockMechanism(this.name); this.accessory.addService(this.service); if ('forceCurrentLockState' in config && config.forceCurrentLockState) { var informFunction = function(value) { this.service.getCharacteristic(Characteristic.LockCurrentState).updateValue(value); }.bind(this); } if ('mapSet' in config && config.mapSet) { this.modFunctionSet = function(value) { return this.mapFunction(value, config.mapSet); }.bind(this); } if ('mapGet' in config && config.mapGet) { this.modFunctionGet = function(value) { return this.mapFunction(value, config.mapGet); }.bind(this); } this.service.getCharacteristic(Characteristic.LockCurrentState) .on('get', function(callback) { this.getByte(callback, config.db, config.get_LockCurrentState, "get LockCurrentState", this.modFunctionGet ); }.bind(this)); this.service.getCharacteristic(Characteristic.LockTargetState) .on('get', function(callback) { this.getByte(callback, config.db, config.get_LockTargetState, "get LockTargetState", this.modFunctionGet ); }.bind(this)) .on('set', function(value, callback) { this.setByte(value, callback, config.db, config.set_LockTargetState, "set LockTargetState", informFunction, this.modFunctionSet ); }.bind(this)); } // INIT handling /////////////////////////////////////////////// // LockMechanismBool //////////////////////////////////////////////////////////////// else if (config.accessory == 'PLC_LockMechanismBool') { this.service = new Service.LockMechanism(this.name); this.accessory.addService(this.service); if ('forceCurrentLockState' in config && config.forceCurrentLockState) { var informFunction = function(value) { this.service.getCharacteristic(Characteristic.LockCurrentState).updateValue(value); }.bind(this); } // Note the invert is inverted! To invert is normal behaviour. if (!('invertLockState' in config && config.invertLockState)) { this.modFunctionGet = this.invert_bit; this.modFunctionSet = this.invert_bit; } this.service.getCharacteristic(Characteristic.LockCurrentState) .on('get', function(callback) { this.getBit(callback, config.db, Math.floor(config.get_LockCurrentStateBool), Math.floor((config.get_LockCurrentStateBool * 10) % 10), "get LockCurrentState", this.modFunctionGet ); }.bind(this)); if ('set_LockTargetStateBool' in config) { this.service.getCharacteristic(Characteristic.LockTargetState) .on('get', function(callback) { this.getBit(callback, config.db, Math.floor(config.get_LockTargetStateBool), Math.floor((config.get_LockTargetStateBool * 10) % 10), "get LockTargetState", this.modFunctionGet ); }.bind(this)) .on('set', function(value, callback) { this.setBit(value, callback, config.db, Math.floor(config.set_LockTargetStateBool), Math.floor((config.set_LockTargetStateBool * 10) % 10), "set LockTargetState", informFunction, this.modFunction