Mit Bezug auf die letzte Frage zum Skript möchte ich euch nicht eine neuere Version des Skripts vorenthalten, welche dieses und weitere Probleme einfacher lösen kann. Unter dem Skript noch ein Beispiel, wie man die neue Logik-Funktion nutzen kann.
Achtung: Grundsätzlich sollte das Skript mit der alten Version kompatibel sein, allerdings werden die Geräte nun standardmäßig unter "0_userdata.0.virtualDevice" angelegt. Um dies rückgängig zu machen siehe im Skript Zeile 57.
Wie immer: vorher dem Update ein Backup machen.
/*
VirtualDevice v1.0
Structure of config:
{
name: 'name', //name of device
namespace: '', //create device within this namespace (device ID will be namespace.name)
common: {}, //(optional)
native: {}, //(optional)
copy: objectId, //(optional) ID of device or channel to copy common and native from
onCreate: function(device, callback) {} //called once on device creation
states: {
'stateA': { //State Id will be namespace.name.stateA
common: {}, //(optional)
native: {}, //(optional)
copy: stateId,
read: {
//(optional) states which should write to "stateA"
'stateId1': {
trigger: {ack: true, change: 'any'} //(optional) see https://github.com/ioBroker/ioBroker.javascript#on---subscribe-on-changes-or-updates-of-some-state
convert: function(val) {}, //(optional) function should return converted value
before: function(device, value, callback) {}, //(optional) called before writing new state. has to call callback or new value will not be written
delay: 0, //(optional) delay in ms before new value gets written
after: function(device, value) {}, //(optional) called after new value has been written
},
...
},
logic: { //(optional), default type: last
type, //can be one of: 'last' || 'number.sum' || 'number.max' || 'number.min' || 'number.average' || 'string.concat'
parameters //(optional) depends on type: number {decimals: 1} / string {separator: " "} /
},
write: {
//(optional) states which "stateA" should write to
'stateId1': {
convert: function(val) {}, //(optional) function should return converted value
before: function(device, value, callback) {}, //(optional) called before writing new state. has to call callback or new value will not be written
delay: 0, //(optional) delay in ms before new value gets written
after: function(device, value) {}, //(optional) called after new value has been written
},
...
},
},
...
}
}
*/
//generic virtual device
function VirtualDevice(config) {
//sanity check
if (typeof config !== 'object' || typeof config.namespace !== 'string' || typeof config.name !== 'string' || typeof config.states !== 'object') {
log('sanity check failed, no device created', 'warn');
return;
}
this.config = config;
this.globalNamespace = '0_userdata.0'; //old: 'javascript.' + instance + '.';
this.sandboxNamespace = 'virtualDevice.' + config.namespace + '.' + config.name;
this.namespace = this.globalNamespace + '.' + this.sandboxNamespace;
this.name = config.name;
this.data = {};
//create virtual device
log('creating virtual device ' + this.sandboxNamespace)
this.createDevice(function () {
this.createStates(function () {
log('created virtual device ' + this.namespace)
}.bind(this));
}.bind(this));
}
VirtualDevice.prototype.createDevice = function (callback) {
log('creating object for device ' + this.sandboxNamespace, 'debug');
//create device object
var obj = this.config.copy ? getObject(this.config.copy) : {common: {}, native: {}};
// @ts-ignore
delete obj.common.custom;
if (typeof this.config.common === 'object') {
obj.common = Object.assign(obj.common, this.config.common);
}
if (typeof this.config.native === 'object') {
obj.native = Object.assign(obj.native, this.config.native);
}
extendObject(this.namespace, {
type: 'device',
common: obj.common,
native: obj.native
}, function (err) {
if (err) {
log('could not create virtual device: ' + this.namespace, 'warn');
return;
}
log('created object for device ' + this.namespace, 'debug');
if (typeof this.config.onCreate === 'function') {
this.config.onCreate(this, callback);
} else {
callback();
}
}.bind(this));
}
VirtualDevice.prototype.createStates = function (callback) {
"use strict";
log('creating states for device ' + this.sandboxNamespace, 'debug');
var stateIds = Object.keys(this.config.states);
log('creating states ' + JSON.stringify(stateIds), 'debug');
var countCreated = 0;
for (var i = 0; i < stateIds.length; i++) {
let stateId = stateIds[i];
this.normalizeState(stateId);
var id = this.sandboxNamespace + '.' + stateId;
log('creating state ' + id, 'debug');
var obj = this.config.states[stateId].copy ? getObject(this.config.states[stateId].copy) : {
common: {},
native: {}
};
// @ts-ignore
delete obj.common.custom;
if (typeof this.config.states[stateId].common === 'object') {
obj.common = Object.assign(obj.common, this.config.states[stateId].common);
}
if (typeof this.config.states[stateId].native === 'object') {
obj.native = Object.assign(obj.native, this.config.states[stateId].native);
}
// @ts-ignore
if (!obj.common.type) {
// @ts-ignore
obj.common.type = 'mixed';
}
// @ts-ignore
if (!obj.common.role) {
// @ts-ignore
obj.common.role = 'state';
}
// @ts-ignore
if (!obj.common.name) {
// @ts-ignore
obj.common.name = stateId;
}
extendObject(this.globalNamespace + '.' + id, {
type: 'state',
common: obj.common,
native: obj.native
}, function (err) {
if (err) {
log('skipping creation of state ' + id, 'debug');
} else {
log('created state ' + id, 'debug');
}
setTimeout(() => {this.connectState(stateId)}, 1000);
countCreated++;
if (countCreated >= stateIds.length) {
log('created ' + countCreated + ' states for device ' + this.namespace, 'debug');
callback();
}
}.bind(this));
}
}
VirtualDevice.prototype.normalizeState = function (stateId) {
log('normalizing state ' + stateId, 'debug');
if (typeof this.config.states[stateId].read !== 'object') {
this.config.states[stateId].read = {};
}
if (typeof this.config.states[stateId].write !== 'object') {
this.config.states[stateId].write = {};
}
this.data[stateId] = {read: {}, write:{}, val: undefined};
var readIds = Object.keys(this.config.states[stateId].read);
for (var i = 0; i < readIds.length; i++) {
this.data[stateId].read[readIds[i]] = null;
var readId = this.config.states[stateId].read[readIds[i]];
if (typeof readId.before !== 'function') {
this.config.states[stateId].read[readIds[i]].before = function (device, value, callback) {
callback()
};
}
if (typeof readId.after !== 'function') {
this.config.states[stateId].read[readIds[i]].after = function (device, value) {
};
}
}
var writeIds = Object.keys(this.config.states[stateId].write);
for (i = 0; i < writeIds.length; i++) {
this.data[stateId].write[writeIds[i]] = null;
var writeId = this.config.states[stateId].write[writeIds[i]];
if (typeof writeId.before !== 'function') {
this.config.states[stateId].write[writeIds[i]].before = function (device, value, callback) {
callback()
};
}
if (typeof writeId.after !== 'function') {
this.config.states[stateId].write[writeIds[i]].after = function (device, value) {
};
}
}
log('normalized state ' + stateId, 'debug');
}
VirtualDevice.prototype.connectState = function (stateId) {
log('connecting state ' + stateId, 'debug');
let id = this.sandboxNamespace + '.' + stateId;
//subscribe to read ids
let readIds = Object.keys(this.config.states[stateId].read);
for (let i = 0; i < readIds.length; i++) {
//check if state exists
getObject(readIds[i], function (err, obj) {
// @ts-ignore
if (err || obj.type !== 'state') {
log('cannot connect to not existing state: ' + readIds[i] + '|' + err + '|' + JSON.stringify(obj), 'warn');
return;
}
var readObj = this.config.states[stateId].read[readIds[i]];
var trigger_temp = readObj.trigger || {change: 'any'};
trigger_temp.ack = true;
trigger_temp.id = readIds[i];
this.subRead(trigger_temp, readObj, stateId);
log('connected ' + readIds[i] + ' to ' + id, 'debug');
}.bind(this));
}
//subscribe to this state and write to write ids
var writeIds = Object.keys(this.config.states[stateId].write);
var trigger = {id: this.namespace + '.' + stateId, change: 'any', ack: false};
// @ts-ignore
on(trigger, function (obj) {
"use strict";
log('detected change of ' + stateId, 'debug');
for (var i = 0; i < writeIds.length; i++) {
let writeObj = this.config.states[stateId].write[writeIds[i]];
let val = obj.state.val;
try {
val = this.convertValue(obj.state.val, writeObj.convert);
} catch (e) {
log('device "' + this.name + '" caused an error in write convert function:' + e, 'warn');
continue;
}
let writeId = writeIds[i];
log('executing function before for ' + writeId, 'debug');
try {
writeObj.before(this, val, function (newVal, newDelay) {
if (newVal !== undefined && newVal !== null) val = newVal;
var delay = writeObj.delay;
if (newDelay !== undefined && newDelay !== null) delay = newDelay;
log('writing value ' + val + ' to ' + writeId + ' with delay ' + delay, 'debug');
this.data[stateId].val = val;
setStateDelayed(writeId, val, false, delay || 0, true, function () {
log('executing function after for ' + writeId, 'debug');
writeObj.after(this, val);
}.bind(this));
}.bind(this));
} catch (e) {
log('device "' + this.name + '" caused an error in a write function:' + e, 'warn');
continue;
}
}
}.bind(this));
log('connected ' + stateId + ' to ' + JSON.stringify(writeIds), 'debug');
}
VirtualDevice.prototype.subRead = function (trigger, readObj, stateId) {
//function to process read states
var onRead = function (obj) {
let val = obj.state.val;
try {
val = this.convertValue(obj.state.val, readObj.convert);
} catch (e) {
log('device "' + this.name + '" caused an error in read convert function:' + e, 'warn');
return;
}
log('executing function before for ' + trigger.id, 'debug');
try {
readObj.before(this, val, function (newVal, newDelay) {
if (newVal !== undefined && newVal !== null) val = newVal;
if (newDelay !== undefined && newDelay !== null) readObj.delay = newDelay;
log('reading value ' + val + ' into ' + this.namespace + '.' + stateId, 'debug');
this.data[stateId].read[trigger.id] = val;
let aggregatedVal = this.aggregateRead(stateId, trigger.id);
this.data[stateId].val = aggregatedVal;
setStateDelayed(this.namespace + '.' + stateId, aggregatedVal, true, readObj.delay || 0, true, function () {
log('executing function after for ' + trigger.id, 'debug');
readObj.after(this, aggregatedVal);
}.bind(this));
}.bind(this));
} catch (e) {
log('device "' + this.name + '" caused an error in a read function:' + e, 'warn');
return;
}
}.bind(this);
//subscribe to state
on(trigger, onRead);
//get state once initially
getState(trigger.id, function (err, state) {
if (!err && state) {
onRead({state: state});
}
});
}
VirtualDevice.prototype.convertValue = function (val, func) {
if (typeof func !== 'function') {
return val;
}
return func(val);
}
VirtualDevice.prototype.aggregateRead = function (stateId, readId) {
var aggregatedVal = this.data[stateId].read[readId];
if (!this.config.states[stateId].logic || !this.config.states[stateId].logic.type) {
return aggregatedVal;
}
switch(this.config.states[stateId].logic.type) {
case 'last':
//use value of state that triggered this function
aggregatedVal = this.data[stateId].read[readId];
if (this.config.states[stateId].logic.parameters && typeof this.config.states[stateId].logic.parameters.decimals === 'number') {
aggregatedVal = Number(aggregatedVal.toFixed(this.config.states[stateId].logic.parameters.decimals));
}
break;
case 'number.sum':
var values = Object.values(this.data[stateId].read).filter(value => typeof value === 'number');
aggregatedVal = values.reduce((accumulator, current) => accumulator + current);
if (this.config.states[stateId].logic.parameters && typeof this.config.states[stateId].logic.parameters.decimals === 'number') {
aggregatedVal = Number(aggregatedVal.toFixed(this.config.states[stateId].logic.parameters.decimals));
}
break;
case 'number.min':
var values = Object.values(this.data[stateId].read).filter(value => typeof value === 'number');
aggregatedVal = Math.min(...values);
if (this.config.states[stateId].logic.parameters && typeof this.config.states[stateId].logic.parameters.decimals === 'number') {
aggregatedVal = Number(aggregatedVal.toFixed(this.config.states[stateId].logic.parameters.decimals));
}
break;
case 'number.max':
var values = Object.values(this.data[stateId].read).filter(value => typeof value === 'number');
aggregatedVal = Math.max(...values);
if (this.config.states[stateId].logic.parameters && typeof this.config.states[stateId].logic.parameters.decimals === 'number') {
aggregatedVal = Number(aggregatedVal.toFixed(this.config.states[stateId].logic.parameters.decimals));
}
break;
case 'number.average':
var values = Object.values(this.data[stateId].read).filter(value => typeof value === 'number');
aggregatedVal = values.length ? values.reduce((accumulator, current) => accumulator + current) / values.length : 0;
if (this.config.states[stateId].logic.parameters && typeof this.config.states[stateId].logic.parameters.decimals === 'number') {
aggregatedVal = Number(aggregatedVal.toFixed(this.config.states[stateId].logic.parameters.decimals));
}
break;
case 'string.concat':
var values = Object.values(this.data[stateId].read).filter(value => typeof value === 'string');
var separator = " ";
if (this.config.states[stateId].logic.parameters && typeof this.config.states[stateId].logic.parameters.separator === 'string') {
separator = this.config.states[stateId].logic.parameters.separator;
}
aggregatedVal = values.join(separator);
break;
default:
//use value of state that triggered this function
aggregatedVal = this.data[stateId].read[readId];
break;
}
return aggregatedVal;
}
Als Beispiel für die neue Funktion bietet sich ein Gruppierungsgerät an, das die Werte mehrerer Geräte zusammenfasst.
In diesem Fall habe ich meine Thermostate zusammengefasst. valveOpen zeigt z.B. die Anzahl der Ventile mit einem Wert > 0 an, maxTemp zeigt die höchste Temperatur an usw.
new VirtualDevice({
"namespace": "Heizung.Gruppe",
"name": "alle",
"states": {
"valveOpen": {
"common": {
"type": "number",
"min": 0,
"def": 0,
"read": true,
"write": false,
"unit": "Valves",
"role": "value.counter"
},
"read": {
"0_userdata.0.virtualDevice.Heizung.Wohnzimmer.valve": {
"convert": function(value) {
return value ? 1 : 0
}
},
"0_userdata.0.virtualDevice.Heizung.Badezimmer.valve": {
"convert": function(value) {
return value ? 1 : 0
}
},
"0_userdata.0.virtualDevice.Heizung.Schlafzimmer.valve": {
"convert": function(value) {
return value ? 1 : 0
}
}
},
"logic": {
"type": "number.sum"
}
},
"valveClosed": {
"common": {
"type": "number",
"min": 0,
"def": 0,
"read": true,
"write": false,
"unit": "Valves",
"role": "value.counter"
},
"read": {
"0_userdata.0.virtualDevice.Heizung.Wohnzimmer.valve": {
"convert": function(value) {
return value ? 0 : 1
}
},
"0_userdata.0.virtualDevice.Heizung.Badezimmer.valve": {
"convert": function(value) {
return value ? 0 : 1
}
},
"0_userdata.0.virtualDevice.Heizung.Schlafzimmer.valve": {
"convert": function(value) {
return value ? 0 : 1
}
}
},
"logic": {
"type": "number.sum"
}
},
"minTemp": {
"common": {
"type": "number",
"unit": "°C",
"def": 0,
"min": -10,
"max": 50,
"read": true,
"write": false,
"role": "value.temperature"
},
"read": {
"0_userdata.0.virtualDevice.Heizung.Wohnzimmer.temperature": {},
"0_userdata.0.virtualDevice.Heizung.Badezimmer.temperature": {},
"0_userdata.0.virtualDevice.Heizung.Schlafzimmer.temperature": {}
},
"logic": {
"type": "number.min"
}
},
"maxTemp": {
"common": {
"type": "number",
"unit": "°C",
"def": 0,
"min": -10,
"max": 50,
"read": true,
"write": false,
"role": "value.temperature"
},
"read": {
"0_userdata.0.virtualDevice.Heizung.Wohnzimmer.temperature": {},
"0_userdata.0.virtualDevice.Heizung.Badezimmer.temperature": {},
"0_userdata.0.virtualDevice.Heizung.Schlafzimmer.temperature": {}
},
"logic": {
"type": "number.max"
}
},
"avgTemp": {
"common": {
"type": "number",
"unit": "°C",
"def": 0,
"min": -10,
"max": 50,
"read": true,
"write": false,
"role": "value.temperature"
},
"read": {
"0_userdata.0.virtualDevice.Heizung.Wohnzimmer.temperature": {},
"0_userdata.0.virtualDevice.Heizung.Badezimmer.temperature": {},
"0_userdata.0.virtualDevice.Heizung.Schlafzimmer.temperature": {}
},
"logic": {
"type": "number.average",
"parameters": {
"decimals": 1
}
}
},
"test": {
"common": {
"type": "string",
"read": true,
"write": false,
"role": "value"
},
"read": {
"0_userdata.0.virtualDevice.Heizung.Wohnzimmer.temperature": {
"convert": function(value) {
return value + '°C'
}
},
"0_userdata.0.virtualDevice.Heizung.Badezimmer.temperature": {
"convert": function(value) {
return value + '°C'
}
},
"0_userdata.0.virtualDevice.Heizung.Schlafzimmer.temperature": {
"convert": function(value) {
return value + '°C'
}
}
},
"logic": {
"type": "string.concat",
"parameters": {
"separator": " | "
}
}
}
}
});