[ { "type": "tab", "id": "e3ca4149.1c35c", "label": "PI43" }, { "type": "tab", "id": "5751578d.a8aea8", "label": "Domoticz" }, { "id": "7b9bfb4b.846404", "type": "mqtt-broker", "z": "", "broker": "mqtt.aiko.sh", "port": "1883", "clientid": "pi2.aiko.sh", "usetls": false, "verifyservercert": true, "compatmode": true, "keepalive": "15", "cleansession": true, "willTopic": "", "willQos": "0", "willRetain": "false", "willPayload": "", "birthTopic": "", "birthQos": "0", "birthRetain": "false", "birthPayload": "" }, { "id": "34f9355a.cb06ca", "type": "exec", "z": "e3ca4149.1c35c", "command": "cat", "addpay": true, "append": "/sys/class/thermal/thermal_zone0/temp", "useSpawn": "", "name": "temp", "x": 327, "y": 119, "wires": [ [ "6a09262a.95f6d8" ], [], [] ] }, { "id": "5e02bbe3.a1fd44", "type": "inject", "z": "e3ca4149.1c35c", "name": "", "topic": "", "payload": "", "payloadType": "none", "repeat": "60", "crontab": "", "once": false, "x": 146, "y": 111, "wires": [ [ "34f9355a.cb06ca", "64325516.9bcdac", "596d73cf.a6928c", "13f94a14.ec06b6", "bd13d8fb.42ec28" ] ] }, { "id": "428ea0cf.bd716", "type": "debug", "z": "e3ca4149.1c35c", "name": "", "active": false, "console": "false", "complete": "payload", "x": 848, "y": 57, "wires": [] }, { "id": "6a09262a.95f6d8", "type": "function", "z": "e3ca4149.1c35c", "name": "convert to C", "func": "var cpuTemp0=msg.payload.trim();\nvar cpuTemp1=~~(cpuTemp0/1000);\nvar cpuTemp2=~~(cpuTemp0/100);\nvar cpuTempM=cpuTemp2 % cpuTemp1;\n\nmsg.payload={ \n timestamp: Date.now(),\n degrees: cpuTemp1,\n raw: cpuTemp0\n};\n\nreturn msg;", "outputs": 1, "noerr": 0, "x": 550, "y": 140, "wires": [ [ "1a28d1d1.e5d72e" ] ] }, { "id": "596d73cf.a6928c", "type": "exec", "z": "e3ca4149.1c35c", "command": "pm2", "addpay": true, "append": "jlist", "useSpawn": "", "name": "", "x": 321, "y": 238, "wires": [ [ "7fd702.ff8029" ], [], [] ] }, { "id": "7fd702.ff8029", "type": "json", "z": "e3ca4149.1c35c", "name": "", "x": 535, "y": 238, "wires": [ [ "f47e2c77.0b81d" ] ] }, { "id": "64325516.9bcdac", "type": "exec", "z": "e3ca4149.1c35c", "command": "cat", "addpay": true, "append": "/proc/uptime", "useSpawn": "", "name": "uptime", "x": 320, "y": 357, "wires": [ [ "46790ddf.b986f4" ], [], [] ] }, { "id": "46790ddf.b986f4", "type": "function", "z": "e3ca4149.1c35c", "name": "convert uptime to json", "func": "var values=msg.payload.trim().split(' ');\nvar uptime=parseFloat(values[0]);\nvar idletime=parseFloat(values[1]);\n\nmsg.payload={\n timestamp: Date.now(),\n raw: msg.payload.trim(),\n seconds: uptime,\n idle_total: idletime,\n idle_per_core: idletime / 4\n};\nreturn msg;", "outputs": 1, "x": 552, "y": 352, "wires": [ [ "45bafe05.ba45" ] ] }, { "id": "45bafe05.ba45", "type": "mqtt out", "z": "e3ca4149.1c35c", "name": "", "topic": "pi2/status/uptime", "qos": "", "retain": "true", "broker": "7b9bfb4b.846404", "x": 889, "y": 353, "wires": [] }, { "id": "f47e2c77.0b81d", "type": "mqtt out", "z": "e3ca4149.1c35c", "name": "", "topic": "pi2/status/pm2", "qos": "", "retain": "true", "broker": "7b9bfb4b.846404", "x": 890, "y": 234, "wires": [] }, { "id": "1a28d1d1.e5d72e", "type": "mqtt out", "z": "e3ca4149.1c35c", "name": "", "topic": "pi2/sensors/temp/cpu", "qos": "", "retain": "true", "broker": "7b9bfb4b.846404", "x": 890, "y": 123, "wires": [] }, { "id": "13f94a14.ec06b6", "type": "exec", "z": "e3ca4149.1c35c", "command": "cat", "addpay": true, "append": "/proc/loadavg", "useSpawn": "", "name": "loadavg", "x": 320, "y": 460, "wires": [ [ "8a7d6fb9.75829" ], [], [] ] }, { "id": "8a7d6fb9.75829", "type": "function", "z": "e3ca4149.1c35c", "name": "convert load to json", "func": "var values=msg.payload.trim().split(' ');\nvar load_1min_part=parseFloat(values[0]);\nvar load_5min_part=parseFloat(values[1]);\nvar load_10min_part=parseFloat(values[2]);\n\nmsg.payload={\n timestamp: Date.now(),\n raw: msg.payload.trim(),\n load: {\n one: load_1min_part,\n five: load_5min_part,\n ten: load_10min_part\n }\n};\nreturn msg;", "outputs": 1, "x": 550, "y": 464, "wires": [ [ "3aa055f6.c55faa" ] ] }, { "id": "3aa055f6.c55faa", "type": "mqtt out", "z": "e3ca4149.1c35c", "name": "", "topic": "pi2/status/loadavg", "qos": "", "retain": "true", "broker": "7b9bfb4b.846404", "x": 887, "y": 462, "wires": [] }, { "id": "bd13d8fb.42ec28", "type": "exec", "z": "e3ca4149.1c35c", "command": "cat", "addpay": true, "append": "/proc/meminfo", "useSpawn": "", "name": "meminfo", "x": 314, "y": 566, "wires": [ [ "f714138c.08ebf" ], [], [] ] }, { "id": "f714138c.08ebf", "type": "function", "z": "e3ca4149.1c35c", "name": "convert meminfo to json", "func": "/*\n$ cat /proc/meminfo\nMemTotal: 947444 kB\nMemFree: 715036 kB\nMemAvailable: 806656 kB\nBuffers: 23904 kB\nCached: 74096 kB\nSwapCached: 0 kB\nActive: 171528 kB\nInactive: 31968 kB\nActive(anon): 105524 kB\nInactive(anon): 796 kB\nActive(file): 66004 kB\nInactive(file): 31172 kB\nUnevictable: 8 kB\nMlocked: 8 kB\nSwapTotal: 0 kB\nSwapFree: 0 kB\nDirty: 4 kB\nWriteback: 0 kB\nAnonPages: 105520 kB\nMapped: 29252 kB\nShmem: 828 kB\nSlab: 15904 kB\nSReclaimable: 8344 kB\nSUnreclaim: 7560 kB\nKernelStack: 1048 kB\nPageTables: 1496 kB\nNFS_Unstable: 0 kB\nBounce: 0 kB\nWritebackTmp: 0 kB\nCommitLimit: 473720 kB\nCommitted_AS: 428368 kB\nVmallocTotal: 1105920 kB\nVmallocUsed: 1156 kB\nVmallocChunk: 885752 kB\n*/\n\n\nvar lines=msg.payload.split('\\n');\nvar mem_total=parseInt(lines[0].split(':')[1].trim().split(' ')[0]);\nvar mem_free=parseInt(lines[1].split(':')[1].trim().split(' ')[0]);\nvar mem_available=parseInt(lines[2].split(':')[1].trim().split(' ')[0]);\nvar buffers=parseInt(lines[3].split(':')[1].trim().split(' ')[0]);\nvar cache=parseInt(lines[4].split(':')[1].trim().split(' ')[0]);\nvar swap_cache=parseInt(lines[5].split(':')[1].trim().split(' ')[0]);\nvar swap_total=parseInt(lines[14].split(':')[1].trim().split(' ')[0]);\nvar swap_free=parseInt(lines[15].split(':')[1].trim().split(' ')[0]);\n\nmsg.payload={\n timestamp: Date.now(),\n raw: msg.payload.trim(),\n mem: {\n total: mem_total,\n free: mem_free,\n available: mem_available,\n buffers: buffers\n },\n cache: {\n cache: cache,\n swap_cache: swap_cache\n },\n swap: {\n total: swap_total,\n free: swap_free\n }\n};\nreturn msg;", "outputs": 1, "x": 544, "y": 570, "wires": [ [ "28b96119.d7469e" ] ] }, { "id": "28b96119.d7469e", "type": "mqtt out", "z": "e3ca4149.1c35c", "name": "", "topic": "pi2/status/meminfo", "qos": "", "retain": "true", "broker": "7b9bfb4b.846404", "x": 881, "y": 568, "wires": [] }, { "id": "98897726.677688", "type": "mqtt in", "z": "5751578d.a8aea8", "name": "OwnTracks", "topic": "owntracks/user/+", "broker": "7b9bfb4b.846404", "x": 122, "y": 87, "wires": [ [ "cf3debf5.30c218" ] ] }, { "id": "50ac2f84.af53d", "type": "mqtt out", "z": "5751578d.a8aea8", "name": "-> domoticz", "topic": "domoticz/in", "qos": "", "retain": "", "broker": "7b9bfb4b.846404", "x": 722, "y": 226, "wires": [] }, { "id": "fc8b685e.037498", "type": "debug", "z": "5751578d.a8aea8", "name": "", "active": false, "console": "false", "complete": "payload", "x": 731.5, "y": 414, "wires": [] }, { "id": "b2660ad8.4d99f8", "type": "geofence", "z": "5751578d.a8aea8", "name": "Zwenkgras", "mode": "circle", "inside": "true", "rad": 19.052575362829838, "points": [], "centre": { "latitude": 52.179497356310755, "longitude": 4.50451523065567 }, "x": 139.5, "y": 170, "wires": [ [ "a3705e81.5c8fa", "bee69fe2.41196" ] ] }, { "id": "745cb8b.f8ba348", "type": "geofence", "z": "5751578d.a8aea8", "name": "Haarlemmerstraat", "mode": "circle", "inside": "true", "rad": 6.491419969978727, "points": [], "centre": { "latitude": 52.16045383788138, "longitude": 4.490457773208618 }, "x": 139, "y": 256, "wires": [ [ "31ba2f6d.ce45d", "bee69fe2.41196" ] ] }, { "id": "cf3debf5.30c218", "type": "function", "z": "5751578d.a8aea8", "name": "Convert Owntracks -> geofence", "func": "var loc = JSON.parse(msg.payload);\nif (loc.lat && loc.lon) {\n var newmsg = { lat: loc.lat, lon: loc.lon };\n msg.location = newmsg;\n}\nreturn msg;", "outputs": 1, "noerr": 0, "x": 405.5, "y": 86, "wires": [ [ "745cb8b.f8ba348", "b2660ad8.4d99f8", "f2fed1ae.0d013", "40c1d833.bf3e28" ] ] }, { "id": "bee69fe2.41196", "type": "debug", "z": "5751578d.a8aea8", "name": "", "active": false, "console": "false", "complete": "location", "x": 409, "y": 416, "wires": [] }, { "id": "31ba2f6d.ce45d", "type": "function", "z": "5751578d.a8aea8", "name": "Map to Aiko@Haarlemmerstraat", "func": "var idx = 29;\ndelete msg.topic;\nif (msg.location) {\n var isat = msg.location.isat;\n msg.payload = {\n idx: idx,\n command: \"switchlight\", \n switchcmd: (isat === undefined ? 'Off' : 'On'),\n// level: 100 \n svalue: (isat === undefined ? '0' : '1')\n };\n} else {\n msg.payload = {};\n}\nreturn msg;\n", "outputs": 1, "noerr": 0, "x": 440.5, "y": 275, "wires": [ [ "fc8b685e.037498", "50ac2f84.af53d" ] ] }, { "id": "f2fed1ae.0d013", "type": "geofence", "z": "5751578d.a8aea8", "name": "notHaarlemmerstraat", "mode": "circle", "inside": "false", "rad": 6.491419969978727, "points": [], "centre": { "latitude": 52.16045383788138, "longitude": 4.490457773208618 }, "x": 144, "y": 306, "wires": [ [ "bee69fe2.41196", "31ba2f6d.ce45d" ] ] }, { "id": "a3705e81.5c8fa", "type": "function", "z": "5751578d.a8aea8", "name": "Map to Aiko@Zwenkgras", "func": "var idx = 30;\ndelete msg.topic;\nif (msg.location) {\n var isat = msg.location.isat;\n msg.payload = {\n idx: idx,\n command: \"switchlight\", \n switchcmd: (isat === undefined ? 'Off' : 'On'),\n// level: 100 \n svalue: (isat === undefined ? '0' : '1')\n };\n} else {\n msg.payload = {};\n}\nreturn msg;\n", "outputs": 1, "noerr": 0, "x": 443, "y": 187, "wires": [ [ "fc8b685e.037498", "50ac2f84.af53d" ] ] }, { "id": "40c1d833.bf3e28", "type": "geofence", "z": "5751578d.a8aea8", "name": "notZwenkgras", "mode": "circle", "inside": "false", "rad": 19.052575362829838, "points": [], "centre": { "latitude": 52.179497356310755, "longitude": 4.50451523065567 }, "x": 139, "y": 214, "wires": [ [ "a3705e81.5c8fa" ] ] }, { "id": "182445e0.3190ea", "type": "debug", "z": "5751578d.a8aea8", "name": "", "active": false, "console": "false", "complete": "true", "x": 609.5, "y": 840, "wires": [] }, { "id": "af2676e9.4d2048", "type": "mqtt in", "z": "5751578d.a8aea8", "name": "domoticz ->", "topic": "domoticz/out", "broker": "7b9bfb4b.846404", "x": 121.5, "y": 486, "wires": [ [ "da82479.34da7b8" ] ] }, { "id": "5080268a.606bd8", "type": "function", "z": "5751578d.a8aea8", "name": "advise badkamer fan", "func": " /*\n g/m^3 =((0.000002*Temp^4)+(0.0002*Temp^3)+(0.0095*Temp^2)+( 0.337*Temp)+4.9034)*RH\n */\n function calcGM3(Temp, RH) {\n var temp2 = Temp * Temp;\n var temp3 = temp2 * Temp;\n var temp4 = temp2 * temp2;\n return Math.floor((\n (0.000002 * temp4) +\n (0.0002 * temp3) +\n (0.0095 * temp2) +\n ( 0.337 * Temp) +\n 4.9034\n ) * RH);\n\n }\n\n function switchFan(payload, inside, outside) {\n if (inside && outside) {\n if (inside > outside) {\n payload.fanOn = true;\n } else {\n payload.fanOn = false;\n }\n payload.inside = inside;\n payload.outside = outside;\n }\n return payload;\n }\n\n var payload = JSON.parse(msg.payload);\n\n if (payload.idx === 15 || payload.idx === 84) {\n var temp = parseFloat(payload.svalue1);\n var hum = parseFloat(payload.svalue2);\n var gm3 = calcGM3(temp, hum);\n if ((payload.idx === 15 ) && (gm3 !== context.outside)) {\n payload = switchFan({}, context.inside, gm3);\n payload.insideT = context.insideT;\n payload.insideRH = context.insideRH;\n payload.outsiteT = context.outsideT = temp;\n payload.outsideRH = context.outsideRH = hum;\n context.outside = gm3;\n msg.payload = payload;\n return msg;\n }\n\n if ((payload.idx === 84) && (gm3 !== context.inside)) {\n payload = switchFan({}, gm3, context.outside);\n payload.outsiteT = context.outsideT;\n payload.outsideRH = context.outsideRH;\n payload.insideT = context.insideT = temp;\n payload.insideRH = context.insideRH = hum;\n context.inside = gm3;\n msg.payload = payload;\n return msg;\n }\n }\n return null;\n", "outputs": 1, "noerr": 0, "x": 401.5, "y": 618, "wires": [ [ "182445e0.3190ea" ] ] }, { "id": "da82479.34da7b8", "type": "function", "z": "5751578d.a8aea8", "name": "filter badkamer info", "func": "var payload = JSON.parse(msg.payload);\n\nif (payload.idx === 15 || \n payload.idx === 84 ||\n payload.idx === 48\n) {\n return msg;\n}\nreturn null;", "outputs": 1, "noerr": 0, "x": 167, "y": 618, "wires": [ [ "5080268a.606bd8", "182445e0.3190ea", "7aaeeb3f.855114" ] ] }, { "id": "2cbff763.27d548", "type": "function", "z": "5751578d.a8aea8", "name": "Switch Fan", "func": "var idx = 48;\ndelete msg.topic;\nvar fanOn = msg.payload.fanOn;\nmsg.payload = {\n idx: idx,\n command: \"switchlight\", \n switchcmd: (fanOn ? 'On' : 'Off'),\n// level: 100 \n svalue: (fanOn ? '1' : '0')\n};\nreturn msg;\n", "outputs": 1, "noerr": 0, "x": 564, "y": 498, "wires": [ [ "182445e0.3190ea", "50ac2f84.af53d" ] ] }, { "id": "8509eb15.7af618", "type": "inject", "z": "5751578d.a8aea8", "name": "", "topic": "", "payload": "", "payloadType": "date", "repeat": "", "crontab": "", "once": false, "x": 99.5, "y": 843, "wires": [ [ "c914d39a.36eb3" ] ] }, { "id": "c914d39a.36eb3", "type": "function", "z": "5751578d.a8aea8", "name": "", "func": "var _ = global.get('_');\nmsg.payload = _.first(['a','b']);\nreturn msg;", "outputs": 1, "noerr": 0, "x": 311.5, "y": 838, "wires": [ [ "182445e0.3190ea" ] ] }, { "id": "7aaeeb3f.855114", "type": "function", "z": "5751578d.a8aea8", "name": "advanced advise bathroom fan ", "func": "var payload = JSON.parse(msg.payload);\npayload.log = '';\nvar _ = global.get('lodash');\n// some constants\n// % rise in humidity that will trigger the fan\nvar fanDeltaTrigger = 5;\n// maximum amount of minutes the fan should be on,\n// in case we never reach the target humidity\nvar fanMaxMinutesOn = 45;\n// ventilator turns off when humidity within 2% of target.\nvar humTargetOffset = 2;\nvar gm3TargetOffset = 200;\n// interval to determine lowest humidity over\nvar recentMinutes = 10;\n// never ventilate below the hard minimum.\nvar hardMinimumHumidity = 40;\n// keep bathroom humidity history\nvar nrHistoryToKeep = 10;\n\nvar outsideSensor = 15;\nvar insideSensor = 84;\nvar sensors = [insideSensor, outsideSensor];\nvar manualSwitch = 48;\nvar switches = [manualSwitch];\n\nvar minutes = 60 * 1000;\nvar fanMaxMinutesOnInMilliseconds = fanMaxMinutesOn * minutes;\nvar domotIdx = payload.idx;\nvar now = Date.now();\n\nfunction log(str) {\n\tpayload.log += str + '. ';\n}\n\n//noinspection JSUnresolvedVariable\n//node.warn('msg.topic === ' + msg.topic);\nif (msg.topic === 'node-red/fan') {\n\tif (!context.lastKnownFanState) {\n\t\t_.forEach(payload.context, function (value, key) {\n\t\t\tnode.warn('node-red/fan context: ' + key + ' = ' + JSON.stringify(value));\n\t\t\tcontext[key] = value;\n\t\t});\n\t\t//noinspection JSUnresolvedVariable\n\t\t// \tnode.warn('node-red/fan context reset');\n\t\t// } else {\n\t\t// \t//noinspection JSUnresolvedVariable\n\t\t// \tnode.warn('node-red/fan context skipped');\n\t}\n\t//noinspection JSAnnotator\n\treturn [null, null];\n}\n\n\nvar bathroomHistory = [];\nif (context.bathroomHistory) {\n\tbathroomHistory = context.bathroomHistory;\n} else {\n\tcontext.bathroomHistory = [];\n}\nvar bathroomHistoryLength = bathroomHistory.length;\nvar lastBathroomHistory = bathroomHistory[bathroomHistoryLength - 1] || {gm3: 10000};\n\nvar targetHumidity = hardMinimumHumidity;\nif (context.targetHumidity) {\n\ttargetHumidity = context.targetHumidity;\n} else {\n\tcontext.targetHumidity = hardMinimumHumidity;\n}\nvar lastKnownOutsideData = null;\nif (context.lastKnownOutsideData) {\n\tlastKnownOutsideData = context.lastKnownOutsideData;\n} else {\n\tcontext.lastKnownOutsideData = null;\n}\nvar lastKnownFanState = {on: false, ts: 0};\nif (context.lastKnownFanState) {\n\tlastKnownFanState = context.lastKnownFanState\n} else {\n\tcontext.lastKnownFanState = {on: false, ts: 0};\n}\nvar fanIsTryingToRestoreHumidity = false;\nif (context.fanIsTryingToRestoreHumidity) {\n\tfanIsTryingToRestoreHumidity = context.fanIsTryingToRestoreHumidity\n} else {\n\tcontext.fanIsTryingToRestoreHumidity = false;\n}\n\nvar riseInHumidity = false;\n\n/*\n g/m^3 =((0.000002*Temp^4)+(0.0002*Temp^3)+(0.0095*Temp^2)+( 0.337*Temp)+4.9034)*RH\n */\nfunction calcGM3(Temp, RH) {\n\tvar temp2 = Temp * Temp;\n\tvar temp3 = temp2 * Temp;\n\tvar temp4 = temp2 * temp2;\n\treturn Math.floor((\n\t\t\t(0.000002 * temp4) +\n\t\t\t(0.0002 * temp3) +\n\t\t\t(0.0095 * temp2) +\n\t\t\t(0.337 * Temp) +\n\t\t\t4.9034\n\t\t) * RH);\n\n}\n\nvar sensorData = {};\nif (_.indexOf(sensors, domotIdx) > -1) {\n\tsensorData.ts = now;\n\tsensorData.temp = parseFloat(payload.svalue1);\n\tsensorData.hum = parseFloat(payload.svalue2);\n\tsensorData.gm3 = calcGM3(sensorData.temp, sensorData.hum);\n}\nvar switchData = {};\nif (_.indexOf(switches, domotIdx) > -1) {\n\tswitchData.ts = now;\n\tswitchData.on = payload.nvalue == 1;\n\tlog('Switch sensor updated to:' + switchData.on);\n\tlastKnownFanState = switchData;\n\tcontext.lastKnownFanState = lastKnownFanState;\n}\n\nif (domotIdx === outsideSensor) {\n\tlog('Outside sensor updated to:' + sensorData.hum);\n\tlastKnownOutsideData = sensorData;\n\tcontext.lastKnownOutsideData = lastKnownOutsideData;\n\tif (lastKnownFanState.on === true) {\n\t\tif (fanIsTryingToRestoreHumidity === false) {\n\t\t\tfanStartedManually();\n\t\t} else {\n\t\t\tfanIsRestoringHumidity();\n\t\t}\n\t}\n}\n\nfunction fanStartedManually() {\n\t/*\n\t -- fan manually started\n\t */\n\tvar testLastHistoryGm3 = lastBathroomHistory.gm3 + gm3TargetOffset;\n\tlog('Fan was manually turned on');\n\tlog('Timeout difference:' + ((lastKnownFanState.ts + fanMaxMinutesOnInMilliseconds) - now) +\n\t\t' minutes:' + Math.floor(((lastKnownFanState.ts + fanMaxMinutesOnInMilliseconds) - now) / minutes) +\n\t\t' lastKnown in:' + lastBathroomHistory.gm3 +\n\t\t' out:' + (lastKnownOutsideData && lastKnownOutsideData.gm3));\n\tif (\n\t\t((lastKnownFanState.ts + fanMaxMinutesOnInMilliseconds) < now) ||\n\t\t(lastKnownOutsideData && (testLastHistoryGm3 <= lastKnownOutsideData.gm3))\n\t) {\n\t\t/*\n\t\t -- fan is on longer than max, turning it off\n\t\t -- absolute humidity outside is higher then inside\n\t\t */\n\t\tif (\n\t\t\t((lastKnownFanState.ts +\n\t\t\tfanMaxMinutesOnInMilliseconds) < now)\n\t\t) {\n\t\t\tlog('Timeout triggered');\n\t\t}\n\t\tif (\n\t\t\t(lastKnownOutsideData && (\n\t\t\ttestLastHistoryGm3 <= lastKnownOutsideData.gm3))\n\t\t) {\n\t\t\tlog('Outside humidity reached');\n\t\t}\n\t\tlog('Turning fan off');\n\t\tpayload.fanOn = false;\n\t}\n}\n\nfunction fanIsRestoringHumidity() {\n\tvar testLastHistoryGm3 = lastBathroomHistory.gm3 + gm3TargetOffset;\n\tif (\n\t\t((fanIsTryingToRestoreHumidity + fanMaxMinutesOnInMilliseconds) < now) ||\n\t\t(lastBathroomHistory.hum <= targetHumidity) ||\n\t\t(lastKnownOutsideData && (testLastHistoryGm3 <= lastKnownOutsideData.gm3))\n\t) {\n\t\t/*\n\t\t -- fan is on long enough\n\t\t -- target humidity has been reached\n\t\t -- absolute humidity outside is higher then inside\n\t\t */\n\t\tif (\n\t\t\t((fanIsTryingToRestoreHumidity + fanMaxMinutesOnInMilliseconds) < now)\n\t\t) {\n\t\t\tlog('Timeout triggered');\n\t\t}\n\t\tif (\n\t\t\t(\n\t\t\tlastBathroomHistory.hum <= targetHumidity)\n\t\t) {\n\t\t\tlog('Target humidity (' + targetHumidity + ') reached');\n\t\t}\n\t\tif (\n\t\t\t(lastKnownOutsideData &&\n\t\t\t(\n\t\t\ttestLastHistoryGm3 <=\n\t\t\tlastKnownOutsideData.gm3))\n\t\t) {\n\t\t\tlog('Outside humidity reached, ');\n\t\t}\n\t\tlog('Turning fan off');\n\n\t\tpayload.fanOn = false;\n\t\tfanIsTryingToRestoreHumidity = false;\n\t\tcontext.fanIsTryingToRestoreHumidity = fanIsTryingToRestoreHumidity;\n\t\ttargetHumidity = hardMinimumHumidity;\n\t\tcontext.targetHumidity = targetHumidity;\n\t}\n}\n\nif (domotIdx === insideSensor) {\n\tvar recent = sensorData.ts - recentMinutes * minutes;\n\tvar lowestRecent = _.reduce(bathroomHistory,\n\t\tfunction (min, data) {\n\t\t\tif (data.ts > recent) {\n\t\t\t\tif (data.hum < min) {\n\t\t\t\t\treturn data.hum;\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn min;\n\t\t}, sensorData.hum);\n\tvar delta = sensorData.hum - lowestRecent;\n\triseInHumidity = delta >= fanDeltaTrigger;\n\tlog('Inside sensor updated to:' + sensorData.hum + '. delta:' + delta);\n\tpayload.riseInHumidity = riseInHumidity;\n\ttargetHumidity = Math.max(lowestRecent + humTargetOffset, hardMinimumHumidity);\n\tpayload.targetHumidity = targetHumidity;\n\tif (bathroomHistoryLength > 0) {\n\t\tvar lastHistory = bathroomHistory[bathroomHistoryLength - 1];\n\t\tif (lastHistory.gm3 !== sensorData.gm3) {\n\t\t\tlog('added to history');\n\t\t\tbathroomHistory.push(sensorData);\n\t\t} else {\n\t\t\t// ignore new timestamp\n\t\t\tlog('ignore new timestamp');\n\t\t\tsensorData = lastHistory;\n\t\t}\n\t} else {\n\t\tlog('started keeping a history');\n\t\tbathroomHistory.push(sensorData);\n\t}\n\tif (bathroomHistoryLength > nrHistoryToKeep) {\n\t\tbathroomHistory.shift();\n\t}\n\tcontext.bathroomHistory = bathroomHistory;\n\tbathroomHistoryLength = bathroomHistory.length;\n\tlastBathroomHistory = bathroomHistory[bathroomHistoryLength - 1] || {gm3: 10000};\n\n\tif (\n\t\t(lastKnownFanState.on === false) ||\n\t\t(lastKnownFanState.on === true && fanIsTryingToRestoreHumidity === false)\n\t) {\n\t\t/*\n\t\t -- either the fan is off or it is on but the decrease program has not started\n\t\t -- in that latter case we start the program anyway. This could happen if someone turns on the ventilator\n\t\t -- manually because he/she is about to take a shower and doesn't like damp mirrors.\n\t\t */\n\n\t\tlog('Fan is:' + lastKnownFanState.on + ', restoring:' + fanIsTryingToRestoreHumidity);\n\n\t\tif (lastKnownFanState.on === true && fanIsTryingToRestoreHumidity === false) {\n\t\t\tfanStartedManually();\n\t\t}\n\t\t\n\t\tif (fanIsTryingToRestoreHumidity !== false && lastKnownFanState.on === false) {\n\t\t\t/*\n\t\t\t -- likely someone turned off the ventilator while the program was running\n\t\t\t */\n\t\t\tlog('Fan was turned off while trying to restore humidity');\n\t\t\tfanIsTryingToRestoreHumidity = false;\n\t\t\tcontext.fanIsTryingToRestoreHumidity = fanIsTryingToRestoreHumidity;\n\t\t}\n\t\t\n\t\tif (riseInHumidity) {\n\t\t\tlog('Rise in humidity, turning fan on');\n\n\t\t\tpayload.fanOn = true;\n\t\t\tif (fanIsTryingToRestoreHumidity === false) {\n\t\t\t\tlog('Setting target humidity: ' + targetHumidity + '.');\n\t\t\t\tcontext.targetHumidity = targetHumidity;\n\t\t\t}\n\t\t\tfanIsTryingToRestoreHumidity = now;\n\t\t\tcontext.fanIsTryingToRestoreHumidity = fanIsTryingToRestoreHumidity;\n\t\t}\n\n\t} else {\n\t\t/*\n\t\t -- fan is on and possibly trying to restore humidity\n\t\t */\n\n\n\t\tif (fanIsTryingToRestoreHumidity) {\n\t\t\tif (riseInHumidity) {\n\t\t\t\t/*\n\t\t\t\t -- ok, there is another FAN_DELTA_TRIGGER rise in humidity\n\t\t\t\t -- when this happen we reset the fanMaxTimer to a new count down\n\t\t\t\t -- because we have to ventilate a bit longer due to the extra humidity\n\t\t\t\t */\n\t\t\t\tfanIsTryingToRestoreHumidity = now;\n\t\t\t\tcontext.fanIsTryingToRestoreHumidity = fanIsTryingToRestoreHumidity;\n\t\t\t\tlog('It\\'s getting more humid, extend shutdown time');\n\t\t\t}\n\t\t\tfanIsRestoringHumidity();\n\t\t} else {\n\t\t\tfanStartedManually();\n\t\t}\n\t}\n}\n\nif (fanIsTryingToRestoreHumidity) {\n\t//noinspection JSUnresolvedVariable\n\tnode.status({\n\t\tfill: \"blue\",\n\t\tshape: \"dot\",\n\t\ttext: \"fanIsTryingToRestoreHumidity\"\n\t});\n} else {\n\tif (lastKnownFanState.on) {\n\t\t//noinspection JSUnresolvedVariable\n\t\tnode.status({\n\t\t\tfill: \"blue\",\n\t\t\tshape: \"ring\",\n\t\t\ttext: \"lastKnownFanState.on\"\n\t\t});\n\t} else {\n\t\t//noinspection JSUnresolvedVariable\n\t\tnode.status({});\n\t}\n}\n\nlastBathroomHistory = bathroomHistory[bathroomHistoryLength - 1] || {gm3: 10000};\n\nvar contextMsg = {\n\tpayload: {\n\t\tlog: payload.log,\n\t\tcontext: {\n\t\t\tbathroomHistory: context.bathroomHistory,\n\t\t\tlastKnownInsideData: lastBathroomHistory,\n\t\t\ttargetHumidity: context.targetHumidity,\n\t\t\tlastKnownOutsideData: context.lastKnownOutsideData,\n\t\t\tlastKnownFanState: context.lastKnownFanState,\n\t\t\tfanIsTryingToRestoreHumidity: context.fanIsTryingToRestoreHumidity\n\t\t}\n\t}\n};\nif (typeof(payload.fanOn) === 'boolean') {\n\t//only pass on fan state changes;\n\tpayload.context = contextMsg.payload.context;\n\tmsg.payload = payload;\n} else {\n\tmsg = null;\n}\n//noinspection JSAnnotator\nreturn [msg, contextMsg];\n", "outputs": "2", "noerr": 0, "x": 368.5, "y": 559, "wires": [ [ "2cbff763.27d548", "d028818.f2fd78" ], [ "182445e0.3190ea", "b6dda284.49226", "5caeaf10.a3515" ] ] }, { "id": "d028818.f2fd78", "type": "debug", "z": "5751578d.a8aea8", "name": "", "active": true, "console": "false", "complete": "payload", "x": 684.5, "y": 717, "wires": [] }, { "id": "b6dda284.49226", "type": "function", "z": "5751578d.a8aea8", "name": "send log message", "func": "delete msg.topic;\nvar log = msg.payload.log;\nmsg.payload = {\n subject: \"Bathroom Fan\",\n body: log,\n command: \"sendnotification\", \n};\nif(log){\n return msg;\n}\nreturn null\n", "outputs": 1, "noerr": 0, "x": 652.5, "y": 584, "wires": [ [ "50ac2f84.af53d", "d028818.f2fd78" ] ] }, { "id": "8827eee3.77d81", "type": "mqtt in", "z": "5751578d.a8aea8", "name": "", "topic": "node-red/fan", "broker": "7b9bfb4b.846404", "x": 117.5, "y": 417, "wires": [ [ "7aaeeb3f.855114" ] ] }, { "id": "5caeaf10.a3515", "type": "mqtt out", "z": "5751578d.a8aea8", "name": "", "topic": "node-red/fan", "qos": "", "retain": "true", "broker": "7b9bfb4b.846404", "x": 833.5, "y": 481, "wires": [] } ]