diff --git a/CHANGELOG.md b/CHANGELOG.md
index 613d19c..c74646c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -15,6 +15,19 @@ Install of a specific Version in Redmatic (on a Homematic):
This can be also used to go back to an older Version.
+### 2.0.7: maintenance release
+
+- blind-control + clock-time
+ - fixed blind-control example 3 and 4; clock-time example #388
+ - the function node in the example can now simulate different days for testing at the same time on different days #389
+ - renamed external given time property from `.dNow` to `.now` to maintain consistency to other nodes
+ - nodes can be completely disables and enabled by incoming message (topic must be `enableNode` and `disableNode`) #365
+
+- blind-control
+ - after expire of manual override with -1 force to send output #387
+ - add possibility to force output to first output when topic contains `forceOutput`
+ - add possibility to add offset for the open/close position of the blind in active sun control #371
+
### 2.0.6: bug fixes
- time-inject fix for next property #364
diff --git a/examples/blind-control/wiki-3 overrides and sun-position.json b/examples/blind-control/wiki-3 overrides and sun-position.json
index 7181019..ca67500 100644
--- a/examples/blind-control/wiki-3 overrides and sun-position.json
+++ b/examples/blind-control/wiki-3 overrides and sun-position.json
@@ -37,11 +37,12 @@
"z": "c224c971b366d1da",
"g": "f154222105f64635",
"name": "30min 1sec",
- "func": "\nconst minutesEachLoop = 30; // minutes to add\nconst loopCycle = 1; // seconds delay\nlet timeObj = context.get(\"timeObj\");\n\nif (timeObj && msg.topic.includes('stop')) {\n clearInterval(timeObj);\n context.set(\"timeObj\", null);\n context.set(\"orgtopic\", null);\n let d = new Date(context.get(\"date\"));\n node.log(\"STOP \" + d.toLocaleTimeString() + ' ####################################### payload='+msg.payload+' topic='+msg.topic);\n node.log('<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< ' + d.toISOString());\n node.status({fill:\"red\",shape:\"ring\",text:\"stopped - \" + d.toLocaleTimeString()});\n return null;\n} else if (!timeObj && msg.topic.includes('start')) {\n context.set(\"message\", msg);\n context.set(\"orgtopic\", msg.topic);\n let d = new Date();\n let num = Number(msg.payload);\n if (isNaN(num) && num < 24) {\n d.setHours(num);\n d.setMinutes(0);\n } else {\n let dt = new Date(msg.payload);\n if (dt instanceof Date && !isNaN(dt)) {\n d = dt;\n } else {\n d.setHours(0);\n d.setMinutes(0);\n }\n }\n context.set(\"date\", d.getTime());\n msg.tsISO = d.toISOString();\n msg.ts = d.getTime();\n msg.topic += ' ' + d.toLocaleTimeString();\n node.log('>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>');\n node.log(\"START \" + d.toLocaleTimeString() + ' ####################################### payload='+msg.payload+' topic='+msg.topic);\n node.send(msg);\n\n let timeObj = setInterval(function(){\n let msg = context.get(\"message\");\n let topic = context.get(\"orgtopic\");\n let d = new Date(context.get(\"date\"));\n //d.setHours(d.getHours()+1);\n let dt = d.getDate();\n let dm = d.getMonth();\n d.setMinutes(d.getMinutes() + minutesEachLoop)\n d.setDate(dt);\n d.getMonth(dm);\n context.set(\"date\", d.getTime());\n msg.tsISO = d.toISOString();\n msg.ts = d.getTime();\n msg.topic = topic + ' ' + d.toLocaleTimeString();\n node.status({fill:\"green\",shape:\"dot\",text:\"run - \" + d.toLocaleTimeString()});\n node.log(\"sending \" + d.toLocaleTimeString() + ' ####################################### payload='+msg.payload+' topic='+msg.topic);\n node.send(msg);\n\t}, (1000 * loopCycle));\n context.set(\"timeObj\", timeObj);\n node.status({fill:\"green\",shape:\"ring\",text:\"start - \" + d.toLocaleTimeString()});\n return null;\n}\n\nlet d = new Date(context.get(\"date\"));\nif (!(d instanceof Date) || isNaN(d)) {\n d = new Date();\n}\nd.setMinutes(d.getMinutes() + 1)\n//d.setHours(d.getHours()+1);\nmsg.tsISO = d.toISOString();\nmsg.ts = d.getTime();\nmsg.topic += ' ' + d.toLocaleTimeString();\nnode.status({fill:\"yellow\",shape:\"dot\",text:\"interposed - \" + d.toLocaleTimeString() + ' - ' + msg.payload});\nnode.log(\"sending interposed msg \" + d.toLocaleTimeString() + ' ####################################### payload='+msg.payload+' topic='+msg.topic);\nnode.send(msg);\nreturn null;\n",
+ "func": "\nconst minutesEachLoop = 20; // minutes to add\nconst loopCycle = 2; // 0.3; // seconds delay\nlet timeObj = context.get(\"timeObj\");\n\nconst days = ['sun','mon','tue','wed','thu','fri','sat'];\n\nif (timeObj && msg.topic.includes('stop')) {\n clearInterval(timeObj);\n context.set(\"timeObj\", null);\n context.set(\"orgtopic\", null);\n let d = new Date(context.get(\"date\"));\n node.log(\"STOP \" + d.toLocaleTimeString() + ' ####################################### payload='+msg.payload+' topic='+msg.topic);\n node.log('<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< ' + d.toISOString());\n node.status({fill:\"red\",shape:\"ring\",text:\"stopped - \" + d.toLocaleTimeString() + ' (' + d.getDay() + ' ' + days[d.getDay()] + ')'});\n return null;\n} else if (!timeObj && msg.topic.includes('start')) {\n context.set(\"message\", msg);\n context.set(\"orgtopic\", msg.topic);\n context.set(\"dateroll\", -1);\n let d = new Date();\n if (typeof msg.payload === 'string' && msg.payload.length > 0) {\n for (let i=0; i<7; i++) {\n\t \t if (msg.payload.includes(days[i])) {\n\t \t msg.payload = msg.payload.replace(days[i],'').trim();\n \t d.setDate(d.getDate() + (i+(7-d.getDay())) % 7);\n \t break;\n }\n }\n if (msg.payload.includes('days')) {\n d.setDate(d.getDate() + (1+(7-d.getDay())) % 7);\n msg.payload = msg.payload.replace('days','').trim();\n context.set(\"dateroll\", 1);\n }\n }\n let num = Number(msg.payload);\n if (!isNaN(num) && num < 24) {\n d.setHours(num);\n d.setMinutes(0);\n } else {\n let dt = new Date(msg.payload);\n if (dt instanceof Date && !isNaN(dt)) {\n d = dt;\n } else {\n d.setHours(0);\n d.setMinutes(0);\n }\n }\n context.set(\"date\", d.getTime());\n msg.tsISO = d.toISOString();\n msg.ts = d.getTime();\n msg.topic += ' ' + d.toLocaleTimeString();\n node.log('>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>');\n node.log(\"START \" + d.toLocaleTimeString() + ' ####################################### payload='+msg.payload+' topic='+msg.topic);\n node.send(msg);\n\n let timeObj = setInterval(function(){\n let msg = context.get(\"message\");\n let topic = context.get(\"orgtopic\");\n let dr = context.get(\"dateroll\");\n let d = new Date(context.get(\"date\"));\n let dt = d.getDate();\n let dm = d.getMonth();\n if (dr >0) {\n dr++;\n if (dr>8) { dr=1; }\n d.setDate(d.getDate() + (dr+(7-d.getDay())) % 7);\n context.set(\"dateroll\", dr);\n } else {\n //d.setHours(d.getHours()+1);\n d.setMinutes(d.getMinutes() + minutesEachLoop)\n d.setDate(dt);\n d.getMonth(dm);\n }\n context.set(\"date\", d.getTime());\n msg.tsISO = d.toISOString();\n msg.ts = d.getTime();\n msg.topic = topic + ' ' + d.toLocaleTimeString();\n node.status({fill:\"green\",shape:\"dot\",text:\"run - \" + d.toLocaleTimeString() + ' (' + d.getDay() + ' ' + days[d.getDay()] + ')'});\n node.log(\"sending \" + d.toLocaleTimeString() + ' ####################################### payload='+msg.payload+' topic='+msg.topic);\n node.send(msg);\n\t}, (1000 * loopCycle));\n context.set(\"timeObj\", timeObj);\n node.status({fill:\"green\",shape:\"ring\",text:\"start - \" + d.toLocaleTimeString() + ' (' + d.getDay() + ' ' + days[d.getDay()] + ')'});\n return null;\n}\n\nlet d = new Date(context.get(\"date\"));\nif (!(d instanceof Date) || isNaN(d)) {\n d = new Date();\n}\nd.setMinutes(d.getMinutes() + 1)\n//d.setHours(d.getHours()+1);\nmsg.tsISO = d.toISOString();\nmsg.ts = d.getTime();\nmsg.topic += ' ' + d.toLocaleTimeString();\nnode.status({fill:\"yellow\",shape:\"dot\",text:\"interposed - \" + d.toLocaleTimeString() + ' (' + d.getDay() + ' ' + days[d.getDay()] + ') - ' + msg.payload});\nnode.log(\"sending interposed msg \" + d.toLocaleTimeString() + ' ####################################### payload='+msg.payload+' topic='+msg.topic);\nnode.send(msg);\nreturn null;",
"outputs": 1,
"noerr": 0,
"initialize": "",
"finalize": "",
+ "libs": [],
"x": 530,
"y": 580,
"wires": [
diff --git a/examples/blind-control/wiki-4 usage of message properties.json b/examples/blind-control/wiki-4 usage of message properties.json
index 7db42a8..2b30909 100644
--- a/examples/blind-control/wiki-4 usage of message properties.json
+++ b/examples/blind-control/wiki-4 usage of message properties.json
@@ -45,11 +45,12 @@
"z": "c224c971b366d1da",
"g": "c76466a96ba836b1",
"name": "30min 0.5sec",
- "func": "\nconst minutesEachLoop = 30; // minutes to add\nconst loopCycle = 0.5; // seconds delay\nlet timeObj = context.get(\"timeObj\");\n\nif (timeObj && msg.topic.includes('stop')) {\n clearInterval(timeObj);\n context.set(\"timeObj\", null);\n context.set(\"orgtopic\", null);\n let d = new Date(context.get(\"date\"));\n node.log(\"STOP \" + d.toLocaleTimeString() + ' ####################################### payload='+msg.payload+' topic='+msg.topic);\n node.log('<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< ' + d.toISOString());\n node.status({fill:\"red\",shape:\"ring\",text:\"stopped - \" + d.toLocaleTimeString()});\n return null;\n} else if (!timeObj && msg.topic.includes('start')) {\n context.set(\"message\", msg);\n context.set(\"orgtopic\", msg.topic);\n let d = new Date();\n let num = Number(msg.payload);\n if (isNaN(num) && num < 24) {\n d.setHours(num);\n d.setMinutes(0);\n } else {\n let dt = new Date(msg.payload);\n if (dt instanceof Date && !isNaN(dt)) {\n d = dt;\n } else {\n d.setHours(0);\n d.setMinutes(0);\n }\n }\n context.set(\"date\", d.getTime());\n msg.tsISO = d.toISOString();\n msg.ts = d.getTime();\n msg.topic += ' ' + d.toLocaleTimeString();\n node.log('>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>');\n node.log(\"START \" + d.toLocaleTimeString() + ' ####################################### payload='+msg.payload+' topic='+msg.topic);\n node.send(msg);\n\n let timeObj = setInterval(function(){\n let msg = context.get(\"message\");\n let topic = context.get(\"orgtopic\");\n let d = new Date(context.get(\"date\"));\n //d.setHours(d.getHours()+1);\n let dt = d.getDate();\n let dm = d.getMonth();\n d.setMinutes(d.getMinutes() + minutesEachLoop)\n d.setDate(dt);\n d.getMonth(dm);\n context.set(\"date\", d.getTime());\n msg.tsISO = d.toISOString();\n msg.ts = d.getTime();\n msg.topic = topic + ' ' + d.toLocaleTimeString();\n node.status({fill:\"green\",shape:\"dot\",text:\"run - \" + d.toLocaleTimeString()});\n node.log(\"sending \" + d.toLocaleTimeString() + ' ####################################### payload='+msg.payload+' topic='+msg.topic);\n node.send(msg);\n\t}, (1000 * loopCycle));\n context.set(\"timeObj\", timeObj);\n node.status({fill:\"green\",shape:\"ring\",text:\"start - \" + d.toLocaleTimeString()});\n return null;\n}\n\nlet d = new Date(context.get(\"date\"));\nif (!(d instanceof Date) || isNaN(d)) {\n d = new Date();\n}\nd.setMinutes(d.getMinutes() + 1)\n//d.setHours(d.getHours()+1);\nmsg.tsISO = d.toISOString();\nmsg.ts = d.getTime();\nmsg.topic += ' ' + d.toLocaleTimeString();\nnode.status({fill:\"yellow\",shape:\"dot\",text:\"interposed - \" + d.toLocaleTimeString() + ' - ' + msg.payload});\nnode.log(\"sending interposed msg \" + d.toLocaleTimeString() + ' ####################################### payload='+msg.payload+' topic='+msg.topic);\nnode.send(msg);\nreturn null;\n",
+ "func": "\nconst minutesEachLoop = 20; // minutes to add\nconst loopCycle = 2; // 0.3; // seconds delay\nlet timeObj = context.get(\"timeObj\");\n\nconst days = ['sun','mon','tue','wed','thu','fri','sat'];\n\nif (timeObj && msg.topic.includes('stop')) {\n clearInterval(timeObj);\n context.set(\"timeObj\", null);\n context.set(\"orgtopic\", null);\n let d = new Date(context.get(\"date\"));\n node.log(\"STOP \" + d.toLocaleTimeString() + ' ####################################### payload='+msg.payload+' topic='+msg.topic);\n node.log('<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< ' + d.toISOString());\n node.status({fill:\"red\",shape:\"ring\",text:\"stopped - \" + d.toLocaleTimeString() + ' (' + d.getDay() + ' ' + days[d.getDay()] + ')'});\n return null;\n} else if (!timeObj && msg.topic.includes('start')) {\n context.set(\"message\", msg);\n context.set(\"orgtopic\", msg.topic);\n context.set(\"dateroll\", -1);\n let d = new Date();\n if (typeof msg.payload === 'string' && msg.payload.length > 0) {\n for (let i=0; i<7; i++) {\n\t \t if (msg.payload.includes(days[i])) {\n\t \t msg.payload = msg.payload.replace(days[i],'').trim();\n \t d.setDate(d.getDate() + (i+(7-d.getDay())) % 7);\n \t break;\n }\n }\n if (msg.payload.includes('days')) {\n d.setDate(d.getDate() + (1+(7-d.getDay())) % 7);\n msg.payload = msg.payload.replace('days','').trim();\n context.set(\"dateroll\", 1);\n }\n }\n let num = Number(msg.payload);\n if (!isNaN(num) && num < 24) {\n d.setHours(num);\n d.setMinutes(0);\n } else {\n let dt = new Date(msg.payload);\n if (dt instanceof Date && !isNaN(dt)) {\n d = dt;\n } else {\n d.setHours(0);\n d.setMinutes(0);\n }\n }\n context.set(\"date\", d.getTime());\n msg.tsISO = d.toISOString();\n msg.ts = d.getTime();\n msg.topic += ' ' + d.toLocaleTimeString();\n node.log('>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>');\n node.log(\"START \" + d.toLocaleTimeString() + ' ####################################### payload='+msg.payload+' topic='+msg.topic);\n node.send(msg);\n\n let timeObj = setInterval(function(){\n let msg = context.get(\"message\");\n let topic = context.get(\"orgtopic\");\n let dr = context.get(\"dateroll\");\n let d = new Date(context.get(\"date\"));\n let dt = d.getDate();\n let dm = d.getMonth();\n if (dr >0) {\n dr++;\n if (dr>8) { dr=1; }\n d.setDate(d.getDate() + (dr+(7-d.getDay())) % 7);\n context.set(\"dateroll\", dr);\n } else {\n //d.setHours(d.getHours()+1);\n d.setMinutes(d.getMinutes() + minutesEachLoop)\n d.setDate(dt);\n d.getMonth(dm);\n }\n context.set(\"date\", d.getTime());\n msg.tsISO = d.toISOString();\n msg.ts = d.getTime();\n msg.topic = topic + ' ' + d.toLocaleTimeString();\n node.status({fill:\"green\",shape:\"dot\",text:\"run - \" + d.toLocaleTimeString() + ' (' + d.getDay() + ' ' + days[d.getDay()] + ')'});\n node.log(\"sending \" + d.toLocaleTimeString() + ' ####################################### payload='+msg.payload+' topic='+msg.topic);\n node.send(msg);\n\t}, (1000 * loopCycle));\n context.set(\"timeObj\", timeObj);\n node.status({fill:\"green\",shape:\"ring\",text:\"start - \" + d.toLocaleTimeString() + ' (' + d.getDay() + ' ' + days[d.getDay()] + ')'});\n return null;\n}\n\nlet d = new Date(context.get(\"date\"));\nif (!(d instanceof Date) || isNaN(d)) {\n d = new Date();\n}\nd.setMinutes(d.getMinutes() + 1)\n//d.setHours(d.getHours()+1);\nmsg.tsISO = d.toISOString();\nmsg.ts = d.getTime();\nmsg.topic += ' ' + d.toLocaleTimeString();\nnode.status({fill:\"yellow\",shape:\"dot\",text:\"interposed - \" + d.toLocaleTimeString() + ' (' + d.getDay() + ' ' + days[d.getDay()] + ') - ' + msg.payload});\nnode.log(\"sending interposed msg \" + d.toLocaleTimeString() + ' ####################################### payload='+msg.payload+' topic='+msg.topic);\nnode.send(msg);\nreturn null;",
"outputs": 1,
"noerr": 0,
"initialize": "",
"finalize": "",
+ "libs": [],
"x": 530,
"y": 1060,
"wires": [
diff --git a/examples/clock-timer/general example of usage.json b/examples/clock-timer/general example of usage.json
index 9dede70..51ea23a 100644
--- a/examples/clock-timer/general example of usage.json
+++ b/examples/clock-timer/general example of usage.json
@@ -38,11 +38,12 @@
"z": "c224c971b366d1da",
"g": "f1d64eab81895d2b",
"name": "30min 0.5sec",
- "func": "\nconst minutesEachLoop = 30; // minutes to add\nconst loopCycle = 0.5; // seconds delay\nlet timeObj = context.get(\"timeObj\");\n\nif (timeObj && msg.topic.includes('stop')) {\n clearInterval(timeObj);\n context.set(\"timeObj\", null);\n context.set(\"orgtopic\", null);\n let d = new Date(context.get(\"date\"));\n node.log(\"STOP \" + d.toLocaleTimeString() + ' ####################################### payload='+msg.payload+' topic='+msg.topic);\n node.log('<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< ' + d.toISOString());\n node.status({fill:\"red\",shape:\"ring\",text:\"stopped - \" + d.toLocaleTimeString()});\n return null;\n} else if (!timeObj && msg.topic.includes('start')) {\n context.set(\"message\", msg);\n context.set(\"orgtopic\", msg.topic);\n let d = new Date();\n let num = Number(msg.payload);\n if (isNaN(num) && num < 24) {\n d.setHours(num);\n d.setMinutes(0);\n } else {\n let dt = new Date(msg.payload);\n if (dt instanceof Date && !isNaN(dt)) {\n d = dt;\n } else {\n d.setHours(0);\n d.setMinutes(0);\n }\n }\n context.set(\"date\", d.getTime());\n msg.tsISO = d.toISOString();\n msg.ts = d.getTime();\n msg.topic += ' ' + d.toLocaleTimeString();\n node.log('>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>');\n node.log(\"START \" + d.toLocaleTimeString() + ' ####################################### payload='+msg.payload+' topic='+msg.topic);\n node.send(msg);\n\n let timeObj = setInterval(function(){\n let msg = context.get(\"message\");\n let topic = context.get(\"orgtopic\");\n let d = new Date(context.get(\"date\"));\n //d.setHours(d.getHours()+1);\n let dt = d.getDate();\n let dm = d.getMonth();\n d.setMinutes(d.getMinutes() + minutesEachLoop)\n d.setDate(dt);\n d.getMonth(dm);\n context.set(\"date\", d.getTime());\n msg.tsISO = d.toISOString();\n msg.ts = d.getTime();\n msg.topic = topic + ' ' + d.toLocaleTimeString();\n node.status({fill:\"green\",shape:\"dot\",text:\"run - \" + d.toLocaleTimeString()});\n node.log(\"sending \" + d.toLocaleTimeString() + ' ####################################### payload='+msg.payload+' topic='+msg.topic);\n node.send(msg);\n\t}, (1000 * loopCycle));\n context.set(\"timeObj\", timeObj);\n node.status({fill:\"green\",shape:\"ring\",text:\"start - \" + d.toLocaleTimeString()});\n return null;\n}\n\nlet d = new Date(context.get(\"date\"));\nif (!(d instanceof Date) || isNaN(d)) {\n d = new Date();\n}\nd.setMinutes(d.getMinutes() + 1)\n//d.setHours(d.getHours()+1);\nmsg.tsISO = d.toISOString();\nmsg.ts = d.getTime();\nmsg.topic += ' ' + d.toLocaleTimeString();\nnode.status({fill:\"yellow\",shape:\"dot\",text:\"interposed - \" + d.toLocaleTimeString() + ' - ' + msg.payload});\nnode.log(\"sending interposed msg \" + d.toLocaleTimeString() + ' ####################################### payload='+msg.payload+' topic='+msg.topic);\nnode.send(msg);\nreturn null;\n",
+ "func": "\nconst minutesEachLoop = 20; // minutes to add\nconst loopCycle = 2; // 0.3; // seconds delay\nlet timeObj = context.get(\"timeObj\");\n\nconst days = ['sun','mon','tue','wed','thu','fri','sat'];\n\nif (timeObj && msg.topic.includes('stop')) {\n clearInterval(timeObj);\n context.set(\"timeObj\", null);\n context.set(\"orgtopic\", null);\n let d = new Date(context.get(\"date\"));\n node.log(\"STOP \" + d.toLocaleTimeString() + ' ####################################### payload='+msg.payload+' topic='+msg.topic);\n node.log('<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< ' + d.toISOString());\n node.status({fill:\"red\",shape:\"ring\",text:\"stopped - \" + d.toLocaleTimeString() + ' (' + d.getDay() + ' ' + days[d.getDay()] + ')'});\n return null;\n} else if (!timeObj && msg.topic.includes('start')) {\n context.set(\"message\", msg);\n context.set(\"orgtopic\", msg.topic);\n context.set(\"dateroll\", -1);\n let d = new Date();\n if (typeof msg.payload === 'string' && msg.payload.length > 0) {\n for (let i=0; i<7; i++) {\n\t \t if (msg.payload.includes(days[i])) {\n\t \t msg.payload = msg.payload.replace(days[i],'').trim();\n \t d.setDate(d.getDate() + (i+(7-d.getDay())) % 7);\n \t break;\n }\n }\n if (msg.payload.includes('days')) {\n d.setDate(d.getDate() + (1+(7-d.getDay())) % 7);\n msg.payload = msg.payload.replace('days','').trim();\n context.set(\"dateroll\", 1);\n }\n }\n let num = Number(msg.payload);\n if (!isNaN(num) && num < 24) {\n d.setHours(num);\n d.setMinutes(0);\n } else {\n let dt = new Date(msg.payload);\n if (dt instanceof Date && !isNaN(dt)) {\n d = dt;\n } else {\n d.setHours(0);\n d.setMinutes(0);\n }\n }\n context.set(\"date\", d.getTime());\n msg.tsISO = d.toISOString();\n msg.ts = d.getTime();\n msg.topic += ' ' + d.toLocaleTimeString();\n node.log('>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>');\n node.log(\"START \" + d.toLocaleTimeString() + ' ####################################### payload='+msg.payload+' topic='+msg.topic);\n node.send(msg);\n\n let timeObj = setInterval(function(){\n let msg = context.get(\"message\");\n let topic = context.get(\"orgtopic\");\n let dr = context.get(\"dateroll\");\n let d = new Date(context.get(\"date\"));\n let dt = d.getDate();\n let dm = d.getMonth();\n if (dr >0) {\n dr++;\n if (dr>8) { dr=1; }\n d.setDate(d.getDate() + (dr+(7-d.getDay())) % 7);\n context.set(\"dateroll\", dr);\n } else {\n //d.setHours(d.getHours()+1);\n d.setMinutes(d.getMinutes() + minutesEachLoop)\n d.setDate(dt);\n d.getMonth(dm);\n }\n context.set(\"date\", d.getTime());\n msg.tsISO = d.toISOString();\n msg.ts = d.getTime();\n msg.topic = topic + ' ' + d.toLocaleTimeString();\n node.status({fill:\"green\",shape:\"dot\",text:\"run - \" + d.toLocaleTimeString() + ' (' + d.getDay() + ' ' + days[d.getDay()] + ')'});\n node.log(\"sending \" + d.toLocaleTimeString() + ' ####################################### payload='+msg.payload+' topic='+msg.topic);\n node.send(msg);\n\t}, (1000 * loopCycle));\n context.set(\"timeObj\", timeObj);\n node.status({fill:\"green\",shape:\"ring\",text:\"start - \" + d.toLocaleTimeString() + ' (' + d.getDay() + ' ' + days[d.getDay()] + ')'});\n return null;\n}\n\nlet d = new Date(context.get(\"date\"));\nif (!(d instanceof Date) || isNaN(d)) {\n d = new Date();\n}\nd.setMinutes(d.getMinutes() + 1)\n//d.setHours(d.getHours()+1);\nmsg.tsISO = d.toISOString();\nmsg.ts = d.getTime();\nmsg.topic += ' ' + d.toLocaleTimeString();\nnode.status({fill:\"yellow\",shape:\"dot\",text:\"interposed - \" + d.toLocaleTimeString() + ' (' + d.getDay() + ' ' + days[d.getDay()] + ') - ' + msg.payload});\nnode.log(\"sending interposed msg \" + d.toLocaleTimeString() + ' ####################################### payload='+msg.payload+' topic='+msg.topic);\nnode.send(msg);\nreturn null;",
"outputs": 1,
"noerr": 0,
"initialize": "",
"finalize": "",
+ "libs": [],
"x": 550,
"y": 2520,
"wires": [
@@ -394,7 +395,6 @@
"autoTrigger": true,
"autoTriggerTime": 1200000,
"startDelayTime": 10000,
- "storeName": "",
"results": [
{
"p": "",
diff --git a/nodes/blind-control.html b/nodes/blind-control.html
index 33c5418..0d8b698 100644
--- a/nodes/blind-control.html
+++ b/nodes/blind-control.html
@@ -244,6 +244,34 @@
blindPosMaxType: {
value: 'levelFixed'
},
+ blindOpenPosOffset: {
+ value: 0.00,
+ required: false,
+ validate(v) {
+ if (typeof v === 'undefined' || v === '') return true;
+ const n = parseFloat(v);
+ const mm = getMinMaxBlindPos(this);
+ return n === 0.00 ||
+ (RED.validators.number()(v) &&
+ (Number.isInteger(n / parseFloat(getVal(this, 'blindIncrement')))) &&
+ (n < mm.max) &&
+ (n > mm.min));
+ }
+ },
+ blindClosedPosOffset: {
+ value: 0.00,
+ required: false,
+ validate(v) {
+ if (typeof v === 'undefined' || v === '') return true;
+ const n = parseFloat(v);
+ const mm = getMinMaxBlindPos(this);
+ return n === 0.00 ||
+ (RED.validators.number()(v) &&
+ (Number.isInteger(n / parseFloat(getVal(this, 'blindIncrement')))) &&
+ (n < mm.max) &&
+ (n > mm.min));
+ }
+ },
sunSlat: {
value: '',
validate: RED.validators.typedInput('sunSlatType')
@@ -3777,16 +3805,6 @@
-
-
-
-
-
-
@@ -3813,6 +3831,32 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/nodes/blind-control.js b/nodes/blind-control.js
index 0418af9..10938f0 100644
--- a/nodes/blind-control.js
+++ b/nodes/blind-control.js
@@ -126,7 +126,7 @@ module.exports = function (RED) {
callback: (result, _obj) => {
return ctrlLib.evalTempData(node, _obj.type, _obj.value, result, tempData);
}
- }, false, oNow.dNow)));
+ }, false, oNow.now)));
} catch (err) {
node.error(RED._('blind-control.errors.getOversteerData', err));
node.log(util.inspect(err, Object.getOwnPropertyNames(err)));
@@ -218,7 +218,7 @@ module.exports = function (RED) {
}
let newPos = hlp.getMsgNumberValue(msg, ['blindPosition', 'position', 'level', 'blindLevel'], ['manual', 'levelOverwrite']);
let nExpire = hlp.getMsgNumberValue(msg, 'expire', 'expire');
- if (msg.topic && String(msg.topic).includes('noExpir')) {
+ if (String(msg.topic).includes('noExpir')) { // hlp.getMsgTopicContains(msg, 'noExpir')) {
nExpire = -1;
}
if (!isNaN(newPos)) {
@@ -254,7 +254,7 @@ module.exports = function (RED) {
} else if (typeof msg.topic === 'string' && msg.topic.includes('slatOverwrite')) {
node.level.slat = msg.payload;
} else {
- node.level.slat = node.positionConfig.getPropValue(node, msg, node.nodeData.slat, false, oNow.dNow);
+ node.level.slat = node.positionConfig.getPropValue(node, msg, node.nodeData.slat, false, oNow.now);
}
}
@@ -302,19 +302,19 @@ module.exports = function (RED) {
function calcBlindSunPosition(node, msg, oNow, tempData) {
// node.debug('calcBlindSunPosition: calculate blind position by sun');
// sun control is active
- const sunPosition = node.positionConfig.getSunCalc(oNow.dNow, false, false);
+ const sunPosition = node.positionConfig.getSunCalc(oNow.now, false, false);
// node.debug('sunPosition: ' + util.inspect(sunPosition, { colors: true, compact: 10, breakLength: Infinity }));
const azimuthStart = node.positionConfig.getFloatProp(node, msg, node.windowSettings.azimuthStartType, node.windowSettings.azimuthStart, NaN, (result, _obj) => {
if (result !== null && typeof result !== 'undefined') {
tempData[_obj.type + '.' + _obj.value] = result;
}
- }, true, oNow.dNow);
+ }, true, oNow.now);
const azimuthEnd = node.positionConfig.getFloatProp(node, msg, node.windowSettings.azimuthEndType, node.windowSettings.azimuthEnd, NaN, (result, _obj) => {
if (result !== null && typeof result !== 'undefined') {
tempData[_obj.type + '.' + _obj.value] = result;
}
- }, true, oNow.dNow);
+ }, true, oNow.now);
sunPosition.InWindow = angleBetween_(sunPosition.azimuthDegrees, azimuthStart, azimuthEnd);
// node.debug(`sunPosition: InWindow=${sunPosition.InWindow} azimuthDegrees=${sunPosition.azimuthDegrees} AzimuthStart=${azimuthStart} AzimuthEnd=${azimuthEnd}`);
@@ -342,7 +342,7 @@ module.exports = function (RED) {
if (res) {
node.level.current = getBlindPosFromTI(node, undefined, res.blindPos.type, res.blindPos.value, node.nodeData.levelTop);
node.level.currentInverse = getInversePos_(node, node.level.current);
- node.level.slat = node.positionConfig.getPropValue(node, msg, res.slatPos, false, oNow.dNow);
+ node.level.slat = node.positionConfig.getPropValue(node, msg, res.slatPos, false, oNow.now);
node.level.topic = node.oversteer.topic;
node.previousData.last.sunLevel = node.level.current;
node.reason.code = 10;
@@ -361,7 +361,7 @@ module.exports = function (RED) {
node.level.current = node.nodeData.levelMin;
node.level.currentInverse = getInversePos_(node, node.level.current);
node.level.topic = node.sunData.topic;
- node.level.slat = node.positionConfig.getPropValue(node, msg, node.sunData.slat, false, oNow.dNow);
+ node.level.slat = node.positionConfig.getPropValue(node, msg, node.sunData.slat, false, oNow.now);
node.previousData.last.sunLevel = node.level.current;
node.reason.code = 13;
node.reason.state = RED._('node-red-contrib-sun-position/position-config:ruleCtrl.states.sunNotInWinMin');
@@ -370,7 +370,7 @@ module.exports = function (RED) {
node.level.current = node.nodeData.levelMax;
node.level.currentInverse = getInversePos_(node, node.level.current);
node.level.topic = node.sunData.topic;
- node.level.slat = node.positionConfig.getPropValue(node, msg, node.sunData.slat, false, oNow.dNow);
+ node.level.slat = node.positionConfig.getPropValue(node, msg, node.sunData.slat, false, oNow.now);
node.previousData.last.sunLevel = node.level.current;
node.reason.code = 13;
node.reason.state = RED._('node-red-contrib-sun-position/position-config:ruleCtrl.states.sunNotInWinMax');
@@ -386,7 +386,7 @@ module.exports = function (RED) {
if (node.sunData.mode === cWinterMode) {
node.level.current = node.nodeData.levelMax;
node.level.currentInverse = getInversePos_(node, node.level.current);
- node.level.slat = node.positionConfig.getPropValue(node, msg, node.sunData.slat, false, oNow.dNow);
+ node.level.slat = node.positionConfig.getPropValue(node, msg, node.sunData.slat, false, oNow.now);
node.level.topic = node.sunData.topic;
node.previousData.last.sunLevel = node.level.current;
node.reason.code = 12;
@@ -396,7 +396,7 @@ module.exports = function (RED) {
} else if (node.sunData.mode === cMinimizeMode) {
node.level.current = node.nodeData.levelMin;
node.level.currentInverse = getInversePos_(node, node.level.current);
- node.level.slat = node.positionConfig.getPropValue(node, msg, node.sunData.slat, false, oNow.dNow);
+ node.level.slat = node.positionConfig.getPropValue(node, msg, node.sunData.slat, false, oNow.now);
node.level.topic = node.sunData.topic;
node.previousData.last.sunLevel = node.level.current;
node.reason.code = 12;
@@ -409,17 +409,17 @@ module.exports = function (RED) {
if (result !== null && typeof result !== 'undefined') {
tempData[_obj.type + '.' + _obj.value] = result;
}
- }, true, oNow.dNow);
+ }, true, oNow.now);
const wTop = node.positionConfig.getFloatProp(node, msg, node.windowSettings.topType, node.windowSettings.top, NaN, (result, _obj) => {
if (result !== null && typeof result !== 'undefined') {
tempData[_obj.type + '.' + _obj.value] = result;
}
- }, true, oNow.dNow);
+ }, true, oNow.now);
const wBottom = node.positionConfig.getFloatProp(node, msg, node.windowSettings.bottomType, node.windowSettings.bottom, NaN, (result, _obj) => {
if (result !== null && typeof result !== 'undefined') {
tempData[_obj.type + '.' + _obj.value] = result;
}
- }, true, oNow.dNow);
+ }, true, oNow.now);
const height = Math.tan(sunPosition.altitudeRadians) * floorLength;
// node.debug(`height=${height} - altitude=${sunPosition.altitudeRadians} - floorLength=${floorLength}`);
@@ -430,10 +430,11 @@ module.exports = function (RED) {
node.level.current = node.nodeData.levelTop;
node.level.currentInverse = node.nodeData.levelBottom;
} else {
- node.level.current = posPrcToAbs_(node, (height - wBottom) / (wTop - wBottom));
+ const levelPercent = (height - wBottom) / (wTop - wBottom);
+ node.level.current = posRound_(node, ((node.nodeData.levelTopSun - node.nodeData.levelBottomSun) * levelPercent) + node.nodeData.levelBottomSun);
node.level.currentInverse = getInversePos_(node, node.level.current);
}
- node.level.slat = node.positionConfig.getPropValue(node, msg, node.sunData.slat, false, oNow.dNow);
+ node.level.slat = node.positionConfig.getPropValue(node, msg, node.sunData.slat, false, oNow.now);
node.level.topic = node.sunData.topic;
const delta = Math.abs(node.previousData.level - node.level.current);
@@ -460,7 +461,7 @@ module.exports = function (RED) {
node.reason.state = RED._('node-red-contrib-sun-position/position-config:ruleCtrl.states.sunCtrl');
node.reason.description = RED._('node-red-contrib-sun-position/position-config:ruleCtrl.reasons.sunCtrl');
node.sunData.changeAgain = oNow.nowNr + node.smoothTime;
- // node.debug(`set next time - smoothTime= ${node.smoothTime} changeAgain= ${node.sunData.changeAgain} dNow=` + oNow.nowNr);
+ // node.debug(`set next time - smoothTime= ${node.smoothTime} changeAgain= ${node.sunData.changeAgain} nowNr=` + oNow.nowNr);
}
if (node.level.current < node.nodeData.levelMin) {
// min
@@ -513,7 +514,7 @@ module.exports = function (RED) {
function checkRules(node, msg, oNow, tempData) {
// node.debug('checkRules --------------------');
const livingRuleData = {};
- ctrlLib.prepareRules(node, msg, tempData, oNow.dNow);
+ ctrlLib.prepareRules(node, msg, tempData, oNow.now);
// node.debug(`checkRules rules.count=${node.rules.count}, rules.lastUntil=${node.rules.lastUntil}, oNow=${util.inspect(oNow, {colors:true, compact:10})}`);
let ruleSel = null;
@@ -522,21 +523,21 @@ module.exports = function (RED) {
let ruleSelMin = null;
let ruleSelMax = null;
let ruleindex = -1;
- // node.debug('first loop count:' + node.rules.count + ' lastuntil:' + node.rules.lastUntil);
+ // node.debug(`first loop count:${ node.rules.count } lastuntil:${ node.rules.lastUntil}`);
for (let i = 0; i <= node.rules.lastUntil; ++i) {
const rule = node.rules.data[i];
- // node.debug('rule ' + rule.time.operator + ' - ' + (rule.time.operator !== cRuleFrom) + ' - ' + util.inspect(rule, {colors:true, compact:10, breakLength: Infinity }));
+ // node.debug(`rule ${rule.name} (${rule.pos}) enabled=${rule.enabled} operator=${rule.time.operator} noFrom=${rule.time.operator !== cRuleFrom} data=${util.inspect(rule, {colors:true, compact:10, breakLength: Infinity })}`);
if (!rule.enabled) { continue; }
if (rule.time && rule.time.operator === cRuleFrom) { continue; }
- // const res = fktCheck(rule, r => (r >= nowNr));
- let res = null;
+ const res = ctrlLib.compareRules(node, msg, rule, r => (r >= oNow.nowNr), oNow);
+ /*
if (!rule.time || rule.time.operator === cRuleFrom) {
res = ctrlLib.compareRules(node, msg, rule, r => (r <= oNow.nowNr), oNow);
} else {
res = ctrlLib.compareRules(node, msg, rule, r => (r >= oNow.nowNr), oNow);
- }
+ } */
if (res) {
- // node.debug('1. ruleSel ' + util.inspect(res, { colors: true, compact: 10, breakLength: Infinity }));
+ // node.debug(`1. ruleSel ${rule.name} (${rule.pos}) data=${ util.inspect(res, { colors: true, compact: 10, breakLength: Infinity }) }`);
if (res.level.operator === cRule.slatOversteer) {
ruleSlatOvs = res;
} else if (res.level.operator === cRule.topicOversteer) {
@@ -559,12 +560,12 @@ module.exports = function (RED) {
// node.debug('--------- starting second loop ' + node.rules.count);
for (let i = (node.rules.count - 1); i >= 0; --i) {
const rule = node.rules.data[i];
- // node.debug('rule ' + rule.time.operator + ' - ' + (rule.time.operator !== cRuleUntil) + ' - ' + util.inspect(rule, {colors:true, compact:10, breakLength: Infinity }));
+ // node.debug(`rule ${rule.name} (${rule.pos}) enabled=${rule.enabled} operator=${rule.time.operator} noUntil=${rule.time.operator !== cRuleUntil} data=${util.inspect(rule, {colors:true, compact:10, breakLength: Infinity })}`);
if (!rule.enabled) { continue; }
if (rule.time && rule.time.operator === cRuleUntil) { continue; } // - From: timeOp === cRuleFrom
const res = ctrlLib.compareRules(node, msg, rule, r => (r <= oNow.nowNr), oNow);
if (res) {
- // node.debug('2. ruleSel ' + util.inspect(res, { colors: true, compact: 10, breakLength: Infinity }));
+ // node.debug(`2. ruleSel ${rule.name} (${rule.pos}) data=${ util.inspect(res, { colors: true, compact: 10, breakLength: Infinity }) }`);
if (res.level.operator === cRule.slatOversteer) {
ruleSlatOvs = res;
} else if (res.level.operator === cRule.topicOversteer) {
@@ -603,7 +604,7 @@ module.exports = function (RED) {
timeLimited: (!!ruleSlatOvs.time),
conditon: ruleSlatOvs.conditon,
time: ruleSlatOvs.timeData,
- slat: node.positionConfig.getPropValue(node, msg, ruleSlatOvs.slat, false, oNow.dNow),
+ slat: node.positionConfig.getPropValue(node, msg, ruleSlatOvs.slat, false, oNow.now),
importance: ruleSlatOvs.importance
};
}
@@ -621,7 +622,7 @@ module.exports = function (RED) {
timeLimited: (!!ruleSelMin.time),
conditon: ruleSelMin.conditon,
time: ruleSelMin.timeData
- // slat: node.positionConfig.getPropValue(node, msg, ruleSelMin.slat, false, oNow.dNow)
+ // slat: node.positionConfig.getPropValue(node, msg, ruleSelMin.slat, false, oNow.now)
// importance: ruleSelMin.importance,
// resetOverwrite: ruleSelMin.resetOverwrite,
// topic : ruleSelMin.topic
@@ -642,7 +643,7 @@ module.exports = function (RED) {
timeLimited: (!!ruleSelMax.time),
conditon: ruleSelMax.conditon,
time: ruleSelMax.timeData
- // slat: node.positionConfig.getPropValue(node, msg, ruleSelMax.slat, false, oNow.dNow)
+ // slat: node.positionConfig.getPropValue(node, msg, ruleSelMax.slat, false, oNow.now)
// importance: ruleSelMax.importance,
// resetOverwrite: ruleSelMax.resetOverwrite,
// topic : ruleSelMax.topic
@@ -722,12 +723,12 @@ module.exports = function (RED) {
livingRuleData.isOff = true;
} else if (ruleSel.level.operator === cRule.levelAbsolute) { // absolute rule
livingRuleData.level = getBlindPosFromTI(node, msg, ruleSel.level.type, ruleSel.level.value, -1);
- livingRuleData.slat = node.positionConfig.getPropValue(node, msg, ruleSel.slat, false, oNow.dNow);
+ livingRuleData.slat = node.positionConfig.getPropValue(node, msg, ruleSel.slat, false, oNow.now);
livingRuleData.active = (livingRuleData.level > -1);
} else {
livingRuleData.active = false;
livingRuleData.level = node.nodeData.levelDefault;
- livingRuleData.slat = node.positionConfig.getPropValue(node, msg, node.nodeData.slat, false, oNow.dNow);
+ livingRuleData.slat = node.positionConfig.getPropValue(node, msg, node.nodeData.slat, false, oNow.now);
}
return livingRuleData;
}
@@ -736,7 +737,7 @@ module.exports = function (RED) {
livingRuleData.importance = 0;
livingRuleData.resetOverwrite = false;
livingRuleData.level = node.nodeData.levelDefault;
- livingRuleData.slat = node.positionConfig.getPropValue(node, msg, node.nodeData.slat, false, oNow.dNow);
+ livingRuleData.slat = node.positionConfig.getPropValue(node, msg, node.nodeData.slat, false, oNow.now);
livingRuleData.topic = node.nodeData.topic;
livingRuleData.code = 1;
livingRuleData.state = RED._('node-red-contrib-sun-position/position-config:ruleCtrl.states.default');
@@ -846,9 +847,12 @@ module.exports = function (RED) {
azimuthEndType: config.windowAzimuthEndType || 'num'
};
node.nodeData = {
+ isDisabled: false,
/** The Level of the window */
levelTop: Number(hlp.chkValueFilled(config.blindOpenPos, 100)),
levelBottom: Number(hlp.chkValueFilled(config.blindClosedPos, 0)),
+ levelTopOffset: Number(hlp.chkValueFilled(config.blindOpenPosOffset, 0)),
+ levelBottomOffset: Number(hlp.chkValueFilled(config.blindClosedPosOffset, 0)),
increment: Number(hlp.chkValueFilled(config.blindIncrement, 1)),
levelDefault: NaN,
levelMin: NaN,
@@ -869,11 +873,28 @@ module.exports = function (RED) {
node.nodeData.overwrite.expireDuration = parseFloat(hlp.chkValueFilled(config.overwriteExpire, NaN));
if (node.nodeData.levelTop < node.nodeData.levelBottom) {
- const tmp = node.nodeData.levelBottom;
+ let tmp = node.nodeData.levelBottom;
node.nodeData.levelBottom = node.nodeData.levelTop;
node.nodeData.levelTop = tmp;
+
+ tmp = node.nodeData.levelBottomOffset;
+ node.nodeData.levelBottomOffset = node.nodeData.levelTopOffset;
+ node.nodeData.levelTopOffset = tmp;
node.levelReverse = true;
}
+ node.nodeData.levelBottomSun = node.nodeData.levelBottom + node.nodeData.levelBottomOffset;
+ if (node.nodeData.levelBottomSun < node.nodeData.levelBottom || node.nodeData.levelBottomSun > node.nodeData.levelTop) {
+ node.nodeData.levelBottomSun = node.nodeData.levelBottom;
+ node.error(RED._('blind-control.errors.invalidOpenPosOffset'));
+ }
+
+ node.nodeData.levelTopSun = node.nodeData.levelTop - node.nodeData.levelTopOffset;
+ if (node.nodeData.levelTopSun < node.nodeData.levelBottom || node.nodeData.levelTopSun > node.nodeData.levelTop) {
+ node.nodeData.levelTopSun = node.nodeData.levelTop;
+ node.error(RED._('blind-control.errors.invalidClosedPosOffset'));
+ }
+ delete node.nodeData.levelBottomOffset;
+ delete node.nodeData.levelTopOffset;
node.nodeData.levelDefault = getBlindPosFromTI(node, undefined, config.blindPosDefaultType, config.blindPosDefault, node.nodeData.levelTop);
node.nodeData.levelMin = getBlindPosFromTI(node, undefined, config.blindPosMinType, config.blindPosMin, node.nodeData.levelBottom);
@@ -1086,7 +1107,10 @@ module.exports = function (RED) {
node.context().set('mode', newMode, node.contextStore);
}
- if (msg.topic && (typeof msg.topic === 'string') && msg.topic.startsWith('set')) {
+ if (msg.topic && (typeof msg.topic === 'string') &&
+ (msg.topic.startsWith('set') ||
+ msg.topic.startsWith('disable') ||
+ msg.topic.startsWith('enable'))) {
const getFloatValue = def => {
const val = parseFloat(msg.payload);
if (isNaN(val)) {
@@ -1150,6 +1174,12 @@ module.exports = function (RED) {
case 'enableRuleByPos':
changeRules(node, parseInt(msg.payload), undefined, { enabled: true });
break;
+ case 'enableNode':
+ node.nodeData.isDisabled = false;
+ break;
+ case 'disableNode':
+ node.nodeData.isDisabled = true;
+ break;
default:
break;
}
@@ -1160,6 +1190,10 @@ module.exports = function (RED) {
node.levelReverse = true;
}
}
+ if (node.nodeData.isDisabled) {
+ done();
+ return null;
+ }
// initialize
node.nowarn = {};
@@ -1172,7 +1206,11 @@ module.exports = function (RED) {
node.previousData.reasonCode = node.reason.code;
node.previousData.reasonState = node.reason.state;
node.previousData.reasonDescription = node.reason.description;
- node.context().set('previous', node.previousData, node.contextStore);
+ if (String(msg.topic).includes('forceOutput')) { // hlp.getMsgTopicContains(msg, 'forceOutput')) {
+ node.previousData.forceNext = true;
+ }
+ } else {
+ node.previousData.forceNext = true;
}
node.oversteer.isChecked = false;
node.reason.code = NaN;
@@ -1192,7 +1230,7 @@ module.exports = function (RED) {
tempData[_obj.type + '.' + _obj.value] = result;
}
}
- }, true, oNow.dNow);
+ }, true, oNow.now);
}
const blindCtrl = {
reason : node.reason,
@@ -1212,8 +1250,10 @@ module.exports = function (RED) {
node.previousData.last.ruleId = blindCtrl.rule.id;
node.previousData.last.ruleLevel = blindCtrl.rule.level;
node.previousData.last.ruleTopic = blindCtrl.rule.topic;
- node.context().set('previous', node.previousData, node.contextStore);
if (blindCtrl.rule.isOff === true) {
+ node.previousData.forceNext = true;
+ node.context().set('previous', node.previousData, node.contextStore);
+ node.context().set('cacheData', tempData, node.contextStore);
// rule set the controller off
done();
return null;
@@ -1288,7 +1328,7 @@ module.exports = function (RED) {
}
},
operator: el.operator
- }, false, oNow.dNow);
+ }, false, oNow.now);
});
}
@@ -1328,7 +1368,8 @@ module.exports = function (RED) {
}
if ((!node.startDelayTimeOut) &&
(!isNaN(node.level.current)) &&
- ((node.level.current !== node.previousData.level) ||
+ (node.previousData.forceNext === true ||
+ (node.level.current !== node.previousData.level) ||
(!isEqual(node.level.slat, node.previousData.slat)) ||
(node.level.topic !== node.previousData.topic))) {
const msgOut = {};
@@ -1348,7 +1389,7 @@ module.exports = function (RED) {
} else if (prop.type === 'strPlaceholder') {
resultObj = hlp.topicReplace(''+prop.value, replaceAttrs);
} else {
- resultObj = node.positionConfig.getPropValue(this, msg, prop, false, oNow.dNow);
+ resultObj = node.positionConfig.getPropValue(this, msg, prop, false, oNow.now);
}
if (typeof resultObj !== 'undefined') {
if (resultObj.error) {
@@ -1359,12 +1400,14 @@ module.exports = function (RED) {
}
}
send([msgOut, { topic, payload: blindCtrl, reason: node.reason, mode: node.sunData.mode }]);
+ delete node.previousData.forceNext;
} else {
send([null, { topic, payload: blindCtrl, reason: node.reason, mode: node.sunData.mode }]);
}
if (isNaN(ruleId)) {
node.previousData.usedRule = ruleId;
}
+ node.context().set('previous', node.previousData, node.contextStore);
node.context().set('cacheData', tempData, node.contextStore);
if (node.autoTrigger) {
node.debug('next autoTrigger will set to ' + node.autoTrigger.time + ' - ' + node.autoTrigger.type);
diff --git a/nodes/clock-timer.js b/nodes/clock-timer.js
index c07f039..bc71cf8 100644
--- a/nodes/clock-timer.js
+++ b/nodes/clock-timer.js
@@ -154,7 +154,7 @@ module.exports = function (RED) {
function checkRules(node, msg, oNow, tempData) {
// node.debug('checkRules --------------------');
const livingRuleData = {};
- ctrlLib.prepareRules(node, msg, tempData, oNow.dNow);
+ ctrlLib.prepareRules(node, msg, tempData, oNow.now);
// node.debug(`checkRules rules.count=${node.rules.count}, rules.lastUntil=${node.rules.lastUntil}, oNow=${util.inspect(oNow, {colors:true, compact:10})}`);
let ruleSel = null;
@@ -349,6 +349,7 @@ module.exports = function (RED) {
// temporary node Data
node.contextStore = config.contextStore || this.positionConfig.contextStore;
node.nodeData = {
+ isDisabled: false,
/** The Level of the window */
payloadDefault: config.payloadDefault,
payloadDefaultType: config.payloadDefaultType,
@@ -428,7 +429,10 @@ module.exports = function (RED) {
}
// allow to overwrite settings by incomming message
- if (msg.topic && (typeof msg.topic === 'string') && msg.topic.startsWith('set')) {
+ if (msg.topic && (typeof msg.topic === 'string') &&
+ (msg.topic.startsWith('set') ||
+ msg.topic.startsWith('disable') ||
+ msg.topic.startsWith('enable'))) {
switch (msg.topic) {
/* Default Settings */
case 'setSettingsTopic':
@@ -438,7 +442,7 @@ module.exports = function (RED) {
case 'setAutoTriggerTime':
node.autoTrigger.defaultTime = parseInt(msg.payload) || node.autoTrigger.defaultTime; // payload of 0 makes no sense, use then default
break;
- case 'setCntextStore':
+ case 'setContextStore':
node.contextStore = msg.payload || node.contextStore;
break;
case 'disableRule':
@@ -453,10 +457,20 @@ module.exports = function (RED) {
case 'enableRuleByPos':
changeRules(node, parseInt(msg.payload), undefined, { enabled: true });
break;
+ case 'enableNode':
+ node.nodeData.isDisabled = false;
+ break;
+ case 'disableNode':
+ node.nodeData.isDisabled = true;
+ break;
default:
break;
}
}
+ if (node.nodeData.isDisabled) {
+ done();
+ return null;
+ }
// initialize
node.nowarn = {};
@@ -483,7 +497,7 @@ module.exports = function (RED) {
tempData[_obj.type + '.' + _obj.value] = result;
}
}
- }, true, oNow.dNow);
+ }, true, oNow.now);
}
const timeCtrl = {
reason : node.reason,
@@ -513,7 +527,7 @@ module.exports = function (RED) {
if (!overwrite || timeCtrl.rule.importance > node.nodeData.overwrite.importance) {
ruleId = timeCtrl.rule.id;
if (timeCtrl.rule.payloadData) {
- node.payload.current = node.positionConfig.getOutDataProp(node, msg, timeCtrl.rule.payloadData, oNow.dNow);
+ node.payload.current = node.positionConfig.getOutDataProp(node, msg, timeCtrl.rule.payloadData, oNow.now);
}
node.payload.topic = timeCtrl.rule.topic;
node.reason.code = timeCtrl.rule.code;
@@ -564,7 +578,7 @@ module.exports = function (RED) {
} else if (prop.type === 'strPlaceholder') {
resultObj = hlp.topicReplace(''+prop.value, replaceAttrs);
} else {
- resultObj = node.positionConfig.getPropValue(this, msg, prop, false, oNow.dNow);
+ resultObj = node.positionConfig.getPropValue(this, msg, prop, false, oNow.now);
}
if (typeof resultObj !== 'undefined') {
if (resultObj.error) {
diff --git a/nodes/lib/dateTimeHelper.js b/nodes/lib/dateTimeHelper.js
index c408f6c..109083d 100644
--- a/nodes/lib/dateTimeHelper.js
+++ b/nodes/lib/dateTimeHelper.js
@@ -21,6 +21,7 @@ module.exports = {
checkLimits,
getMsgBoolValue,
getMsgBoolValue2,
+ getMsgTopicContains,
getMsgNumberValue,
getMsgNumberValue2,
getSpecialDayOfMonth,
@@ -455,7 +456,7 @@ function getNowTimeStamp(node, msg) {
/**
* support timeData
* @name ITimeObject Data
-* @param {Date} dNow
+* @param {Date} now
* @param {number} nowNr
* @param {number} dayNr
* @param {number} monthNr
@@ -473,7 +474,7 @@ function getNowTimeStamp(node, msg) {
function getNowObject(node, msg) {
const dNow = getNowTimeStamp(node, msg);
return {
- dNow,
+ now : dNow,
nowNr : dNow.getTime(),
dayNr : dNow.getDay(),
dateNr : dNow.getDate(),
@@ -607,7 +608,7 @@ function getMsgNumberValue2(msg, ids, isFound, notFound) {
return notFound;
}
/**
- * check the type of the message
+ * check if the msg or msg.property contains a property with value true or the topic contains the given name
* @param {*} msg message
* @param {*} name property name
*/
@@ -632,16 +633,26 @@ function getMsgBoolValue(msg, ids, names, isFound, notFound) {
}
}
}
- if (names && msg && msg.topic) {
- if (!Array.isArray(names)) {
- names = [names];
+ return getMsgTopicContains(msg, names, isFound, notFound);
+}
+
+/**
+ * check if the msg contains a property with value true (no payload check)
+ * @param {*} msg message
+ * @param {*} name property name
+ */
+function getMsgBoolValue2(msg, ids, isFound, notFound) {
+ if (ids && msg) {
+ if (!Array.isArray(ids)) {
+ ids = [ids];
}
- for (let i = 0; i < names.length; i++) {
- if (String(msg.topic).includes(String(names[i]))) {
+ for (let i = 0; i < ids.length; i++) {
+ const id = ids[i];
+ if ((typeof msg[id] !== 'undefined') && (msg[id] !== null) && (msg[id] !== '')) {
if (typeof isFound === 'function') {
- return isFound(true, msg.payload, msg.topic);
+ return isFound(isTrue(msg[id]), msg[id], msg.topic);
}
- return true;
+ return isTrue(msg[id]);
}
}
}
@@ -654,22 +665,22 @@ function getMsgBoolValue(msg, ids, names, isFound, notFound) {
}
/**
- * check the type of the message
+ * check if thetopic contains one of the given names
* @param {*} msg message
* @param {*} name property name
*/
-function getMsgBoolValue2(msg, ids, isFound, notFound) {
- if (ids && msg) {
- if (!Array.isArray(ids)) {
- ids = [ids];
+function getMsgTopicContains(msg, names, isFound, notFound) {
+ if (msg) { // && msg.topic
+ if (!Array.isArray(names)) {
+ names = [names];
}
- for (let i = 0; i < ids.length; i++) {
- const id = ids[i];
- if ((typeof msg[id] !== 'undefined') && (msg[id] !== null) && (msg[id] !== '')) {
+ const topic = String(msg.topic);
+ for (let i = 0; i < names.length; i++) {
+ if (topic.includes(String(names[i]))) {
if (typeof isFound === 'function') {
- return isFound(isTrue(msg[id]), msg[id], msg.topic);
+ return isFound(true, msg.payload, topic);
}
- return isTrue(msg[id]);
+ return true;
}
}
}
diff --git a/nodes/lib/timeControlHelper.js b/nodes/lib/timeControlHelper.js
index 4692d6d..f749105 100644
--- a/nodes/lib/timeControlHelper.js
+++ b/nodes/lib/timeControlHelper.js
@@ -257,7 +257,7 @@ function prepareRules(node, msg, tempData, dNow) {
* @return {number} timestamp of the rule
*/
function getRuleTimeData(node, msg, rule, dNow) {
- rule.time.dNow = dNow;
+ rule.time.now = dNow;
rule.timeData = node.positionConfig.getTimeProp(node, msg, rule.time);
if (rule.timeData.error) {
hlp.handleError(node, RED._('node-red-contrib-sun-position/position-config:errors.error-time', { message: rule.timeData.error }), undefined, rule.timeData.error);
@@ -267,9 +267,10 @@ function getRuleTimeData(node, msg, rule, dNow) {
}
rule.timeData.source = 'Default';
rule.timeData.ts = rule.timeData.value.getTime();
+ // node.debug(`time=${rule.timeData.value} -> ${new Date(rule.timeData.value)}`);
rule.timeData.dayId = hlp.getDayId(rule.timeData.value);
if (rule.timeMin) {
- rule.timeMin.dNow = dNow;
+ rule.timeMin.now = dNow;
rule.timeDataMin = node.positionConfig.getTimeProp(node, msg, rule.timeMin);
const numMin = rule.timeDataMin.value.getTime();
rule.timeDataMin.source = 'Min';
@@ -288,7 +289,7 @@ function getRuleTimeData(node, msg, rule, dNow) {
}
}
if (rule.timeMax) {
- rule.timeMax.dNow = dNow;
+ rule.timeMax.now = dNow;
rule.timeDataMax = node.positionConfig.getTimeProp(node, msg, rule.timeMax);
const numMax = rule.timeDataMax.value.getTime();
rule.timeDataMax.source = 'Max';
@@ -321,7 +322,7 @@ function getRuleTimeData(node, msg, rule, dNow) {
/**
* support timeData
* @name ITimeObject Data
-* @param {Date} dNow
+* @param {Date} now
* @param {number} nowNr
* @param {number} dayNr
* @param {number} monthNr
@@ -340,10 +341,11 @@ function getRuleTimeData(node, msg, rule, dNow) {
* @returns {Object|null} returns the rule if rule is valid, otherwhise null
*/
function compareRules(node, msg, rule, cmp, data) {
- // node.debug('fktCheck rule ' + util.inspect(rule, {colors:true, compact:10}));
+ // node.debug(`compareRules rule ${rule.name} (${rule.pos}) data=${util.inspect(rule, {colors:true, compact:10})}`);
if (rule.conditional) {
try {
if (!rule.conditon.result) {
+ node.debug(`compareRules rule ${rule.name} (${rule.pos}) conditon does not match`);
return null;
}
} catch (err) {
@@ -356,21 +358,27 @@ function compareRules(node, msg, rule, cmp, data) {
return rule;
}
if (rule.time.days && !rule.time.days.includes(data.dayNr)) {
+ node.debug(`compareRules rule ${rule.name} (${rule.pos}) invalid days`);
return null;
}
if (rule.time.months && !rule.time.months.includes(data.monthNr)) {
+ node.debug(`compareRules rule ${rule.name} (${rule.pos}) invalid month`);
return null;
}
if (rule.time.onlyOddDays && (data.dateNr % 2 === 0)) { // even
+ node.debug(`compareRules rule ${rule.name} (${rule.pos}) invalid even days`);
return null;
}
if (rule.time.onlyEvenDays && (data.dateNr % 2 !== 0)) { // odd
+ node.debug(`compareRules rule ${rule.name} (${rule.pos}) invalid odd days`);
return null;
}
if (rule.time.onlyOddWeeks && (data.weekNr % 2 === 0)) { // even
+ node.debug(`compareRules rule ${rule.name} (${rule.pos}) invalid even week`);
return null;
}
if (rule.time.onlyEvenWeeks && (data.weekNr % 2 !== 0)) { // odd
+ node.debug(`compareRules rule ${rule.name} (${rule.pos}) invalid odd week`);
return null;
}
if (rule.time.dateStart || rule.time.dateEnd) {
@@ -378,21 +386,24 @@ function compareRules(node, msg, rule, cmp, data) {
rule.time.dateEnd.setFullYear(data.yearNr);
if (rule.time.dateEnd > rule.time.dateStart) {
// in the current year
- if (data.dNow < rule.time.dateStart || data.dNow > rule.time.dateEnd) {
+ if (data.now < rule.time.dateStart || data.now > rule.time.dateEnd) {
+ node.debug(`compareRules rule ${rule.name} (${rule.pos}) invalid date range within year`);
return null;
}
} else {
// switch between year from end to start
- if (data.dNow < rule.time.dateStart && data.dNow > rule.time.dateEnd) {
+ if (data.now < rule.time.dateStart && data.now > rule.time.dateEnd) {
+ node.debug(`compareRules rule ${rule.name} (${rule.pos}) invalid date range over year`);
return null;
}
}
}
- const num = getRuleTimeData(node, msg, rule, data.dNow);
- // node.debug(`pos=${rule.pos} type=${rule.time.operatorText} - ${rule.time.value} - num=${num} - rule.timeData = ${ util.inspect(rule.timeData, { colors: true, compact: 40, breakLength: Infinity }) }`);
+ const num = getRuleTimeData(node, msg, rule, data.now);
+ // node.debug(`compareRules ${rule.name} (${rule.pos}) type=${rule.time.operatorText} - ${rule.time.value} - num=${num} - rule.timeData = ${ util.inspect(rule.timeData, { colors: true, compact: 40, breakLength: Infinity }) }`);
if (data.dayId === rule.timeData.dayId && num >=0 && (cmp(num) === true)) {
return rule;
}
+ // node.debug(`compareRules rule ${rule.name} (${rule.pos}) dayId=${data.dayId} rule-DayID=${rule.timeData.dayId} num=${num} cmp=${cmp(num)} invalid time`);
return null;
}
/*************************************************************************************************************************/
diff --git a/nodes/locales/de/blind-control.json b/nodes/locales/de/blind-control.json
index da75379..1fa02bc 100644
--- a/nodes/locales/de/blind-control.json
+++ b/nodes/locales/de/blind-control.json
@@ -11,6 +11,8 @@
"blindIncrement": "Schritt",
"blindOpenPos": "Rollladen offen (max)",
"blindClosedPos": "Rollladen geschlossen (min)",
+ "blindOpenPosOffset": "Rollladen offen offset",
+ "blindClosedPosOffset": "Rollladen geschlossen offset",
"blindLevelDefault": "Standard Rollladenposition",
"slatPosDefault": "Standard Lamellenposition",
"blindLevelMin": "minimale Rollladenposition",
@@ -58,6 +60,8 @@
"blindIncrement": "0.01 oder 1",
"blindOpenPos": "1 oder 100",
"blindClosedPos": "0",
+ "blindOpenPosOffset": "offset für die Position geöffnet",
+ "blindClosedPosOffset": "offset für die Position geschlossen",
"blindLevelDefault": "Rollladenposition, wenn keine andere zutrifft",
"slatPosDefault": "Lamellenposition, wenn keine andere zutrifft",
"blindLevelMin": "maximale Rollladenposition Sonnenstands abhängig",
diff --git a/nodes/locales/de/position-config.json b/nodes/locales/de/position-config.json
index 644d1ec..03b4e7d 100644
--- a/nodes/locales/de/position-config.json
+++ b/nodes/locales/de/position-config.json
@@ -439,6 +439,8 @@
"windowAzimuth": "Darstellung der Ausrichtung des Fensters zum geografischen Norden (in Grad), wenn die Sonne in das Fenster fällt (Sonnenrichtung/Azimuth).",
"windowPos": "Fenster Eigenschaften, Messung vom Boden bis zum unter und Oberseite des Fensters",
"oversteer": "erlaubt das Übersteuern der Position in Abhängigkeit von der Sonne unter definierten Bedingungen wie Sonnenwinkel oder Wetter (z. B. Wolkenhimmel, Beleuchtung, etc.). Hiermit kann auch eine minimale oder maximaler Höhenwinkel definiert werden, wenn Berge/Gebäude, etc das Fenster verdecken.",
+ "sunAdvanced": "Erweiterte Einstellungen für die Sonnensteuerung",
+ "blindPosOffset": "Wenn der Fensterbereich kleiner als der Rollladen öffnen/schließenBereichs entspricht, weil ein breiter Fensterrahmen vorhanden ist oder verzögerte öffnen/schließen vorliegt (durch beispielsweise Schlitze welche sich erst Öffnen/schließen müssen), kann hier ein Offset dafür eingestellt werden.",
"sunControlMaximization": "
Wenn keine Regel oder Überschreiben (Override) zutrifft,
Wenn die Sonne nicht em> in das Fenster scheint, wird die Rollladenhöhe die definierte Minimalposition strong> gesetzt. (Übersteuern wird ignoriert) li>
Wenn sich die Sonne im Fenster befindet li>
Wenn eine Übersteueruzng aufgesetzt ist und die bedingung zutrifft, wird der Behang auf die Höhe des Übersteuerns gesetzt.< / li>
Andernfalls wird die Behanghöhe auf die definierte Maximalposition strong> gesetzt. li> ul> li> ul>",
"sunControlMinimization": "
Wenn keine Regel oder Überschreiben (Override) zutrifft,
Wenn die Sonne nicht em> in das Fenster scheint, wird die Rollladenhöhe die definierte Maximalposition strong> gesetzt. (Übersteuern wird ignoriert) li>
Wenn sich die Sonne im Fenster befindet li>
Wenn eine Übersteueruzng aufgesetzt ist und die bedingung zutrifft, wird der Behang auf die Höhe des Übersteuerns gesetzt.< / li>
Andernfalls wird die Behanghöhe auf die definierte Minimalposition strong> gesetzt. li> ul> li> ul>",
"sunControlRestriction": "Wenn keine Regel oder Überschreiben (Override) zutrifft, berechnet der Knoten die entsprechende Blindposition, um die Menge des direkten Sonnenlichts, das in den Raum fällt, zu begrenzen.",
diff --git a/nodes/locales/en-US/blind-control.json b/nodes/locales/en-US/blind-control.json
index 1bd9e5c..e8c9983 100644
--- a/nodes/locales/en-US/blind-control.json
+++ b/nodes/locales/en-US/blind-control.json
@@ -11,6 +11,8 @@
"blindIncrement": "Increment",
"blindOpenPos": "open position (max)",
"blindClosedPos": "closed position (min)",
+ "blindOpenPosOffset": "open position offset",
+ "blindClosedPosOffset": "closed position offset",
"blindLevelDefault": "default position",
"slatPosDefault": "default slat",
"blindLevelMin": "min position",
@@ -58,6 +60,8 @@
"blindIncrement": "0.01 or 1",
"blindOpenPos": "100",
"blindClosedPos": "0",
+ "blindOpenPosOffset": "offset for the open position",
+ "blindClosedPosOffset": "offset for the closed position",
"blindLevelDefault": "blind position if no other used",
"slatPosDefault": "slat position if no other used",
"blindLevelMin": "minimum position if sun calculated position",
@@ -125,7 +129,9 @@
"invalid-blind-level": "Given Blind-Position __pos__ is not a valid Position!",
"getOversteerData": "error getting oversteer data: __message__",
"getBlindPosData": "error getting blind level: __message__",
- "smoothTimeToolong": "The selected smooth is too long!!"
+ "smoothTimeToolong": "The selected smooth is too long!!",
+ "invalidOpenPosOffset":"Invalid value for blind open position offset for the sun control!",
+ "invalidClosedPosOffset":"Invalid value for blind closed position offset for the sun control!"
}
}
}
diff --git a/nodes/locales/en-US/position-config.json b/nodes/locales/en-US/position-config.json
index 6ab96f7..4f2d447 100644
--- a/nodes/locales/en-US/position-config.json
+++ b/nodes/locales/en-US/position-config.json
@@ -454,6 +454,8 @@
"windowAzimuth": "representing the orientation of the window to geographical north (in degrees) when the sun falls into the window (sun azimuth).",
"windowPos": "window settings, measurement from the floor to bottom and top of the window covered by the blind",
"oversteer": "allows to oversteer the position depending on the sun under defined condition such as elevation, weather (for example sky covered by clouds, lighting, ...) This can also be used to define a minimum or maximum elevation angle if mountains / buildings, etc. cover the window.",
+ "sunAdvanced": "advanced settigns for sun control",
+ "blindPosOffset": "If the window free space does not match the blind open and closed position, here can be defined an offset to this. Example for this is a wide window frame or a delay in opening the blind caused by open/close the slits between the slats.",
"sunControlMaximization": "
If no rule or override matches
If the sun is not in the window the blind will set to defined minimum position. (oversteer will be ignored)
If the sun is in the window
If any oversteer data are setup and oversteer conditions are fulfilled the blind will set to the defined oversteer blind position.
otherwise the blind level is set to defined maximum position.
",
"sunControlMinimization": "
If no rule or override matches
If the sun is not in the window the blind will set to defined maximum position. (oversteer will be ignored)
If the sun is in the window
If any oversteer data are setup and oversteer conditions are fulfilled the blind will set to the defined oversteer blind position.
otherwise the blind level is set to defined minimum position.