homebridge-plc
Version:
Homebridge plugin for SIEMENS Step7 and compatible PLCs. (https://github.com/homebridge)
1,235 lines (1,116 loc) • 181 kB
JavaScript
/*
* (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