Controlling Indoor Air Quality and Ventilation using ZiggBee Devices
Covid and climate change have raised concerns about indoor air quality and raised my interest in what I could do to improve the situation. We have an existing HRV (Heat Recovery Ventilator) which has worked well, but didn't seem to be keeping up with the task since I retired and we started taking in B&B guests to help with the limited housing supply and supplement our income.
The functionality is as follows:
Air Quality montitoring ( temperature, humidity, co2, voc, formaldehyde) with a TuYa "smart air box". This device works reliably but the data is noisy and the firmware has no restraints on the data flow posting message packets several times per second. Consequently the data must be filtered to be useful. A statistics node returns median values over a dataset and these are written to flow context. The flow context data is then injected on 1 second intervals and the data is further processed through a "smooth" node (rolling average filter) and then saved to global context. Air quality data is also written to an Allen Bradley PLC controlling the HVAC system.
Gas Stove "Rangehood" (Exhaust Fan) control. An Aqara temperature/humidity/pressure sensor is located under the hood. The control is based on the difference in temperature and absolute humidity between the rangehood sensor and reference temperature (from an existing analog temp sensor) and absolute humidity (from the TuYa) values in the kitchen. Note that the relative humidity (rh) values from the sensor will not work in this situation.
Bathroom Vents (hrv system). Aqara t/h/p sensors report the relative humidity, a statistics node provides a minimum (floor) humdity value. The vent is opened and the hrv fan is set to high speed when a difference between room rh and rh (floor) exceeds a threshold.
Back Door Entry Light. A Sonoff switch module provides a means to automatically turn off the light after a set delay.
Garage Door state monitoring. Magnetically trigged (hall effect) switches monitor the "closed' state of the garage doors. I could not find a zigbee module that would report the state of a low voltage digital input so I made do with "switch" modules, which required the addition of triac optoisolators (mounted on a small perf board) to interface to their mains level inputs. If either garage door is remains open past a set time an eWeLink switch module will ring the basement doorbell on a set interval to alert us of the situation.
Notes The zigbee2MQTT base topic has been changed from "zigbee2MQTT" to "house" I made extensive use of the excellent MQTT-Explorer app for testing and debugging.
[{"id":"24d3c36e.663b3c","type":"tab","label":"ZigBee","disabled":false,"info":""},{"id":"47bb0880d2415381","type":"mqtt in","z":"24d3c36e.663b3c","name":"","topic":"house/main/hall/sensor/airquality","qos":"0","datatype":"json","broker":"48982d6c.76a7a4","nl":false,"rap":true,"rh":0,"inputs":0,"x":150,"y":200,"wires":[["316acecc91c65a29"]]},{"id":"2e998984421c95ba","type":"inject","z":"24d3c36e.663b3c","name":"filtered data","props":[{"p":"payload"}],"repeat":"10","crontab":"","once":true,"onceDelay":"0.55","topic":"","payload":"airquality_filtered","payloadType":"global","x":110,"y":560,"wires":[["a638464da087723c","f539559b73a4a4bf"]]},{"id":"47df41eceb9e72c8","type":"comment","z":"24d3c36e.663b3c","name":"TuYa smart air box","info":"The TuYa smart airbox is functional but problematic. It floods the network with data, updating each topic at least once per second and the data is noisy and needs considerable filtering. \n\nIt requires a stable, low impedance power supply for best performance. You will need sometyhing better than a generic 5v USB phone charger. I ended up soldering a 1000uf 6.3v eletrolytic capacitor across the 5v supply cable and found the data signicantly more stable as a result. ","x":110,"y":160,"wires":[]},{"id":"7db2d59d2a32cd98","type":"mqtt out","z":"24d3c36e.663b3c","name":"house/main/kitchen/rangehood/fan","topic":"","qos":"2","retain":"","respTopic":"","contentType":"","userProps":"","correl":"","expiry":"","broker":"48982d6c.76a7a4","x":1000,"y":960,"wires":[]},{"id":"7377c6b4f051be87","type":"inject","z":"24d3c36e.663b3c","name":"Off","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"house/main/kitchen/rangehood/fan/set","payload":"{\"state_l1\":\"OFF\",\"state_l2\":\"OFF\"}","payloadType":"json","x":710,"y":1040,"wires":[["7db2d59d2a32cd98"]]},{"id":"bd238859e25ff8a6","type":"inject","z":"24d3c36e.663b3c","name":"LowSpeed","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"house/main/kitchen/rangehood/fan/set","payload":"{\"state_l1\":\"ON\",\"state_l2\":\"OFF\"}","payloadType":"json","x":720,"y":1000,"wires":[["7db2d59d2a32cd98"]]},{"id":"cafa126ab02a4c07","type":"mqtt in","z":"24d3c36e.663b3c","name":"","topic":"house/main/kitchen/rangehood/fan","qos":"0","datatype":"json","broker":"48982d6c.76a7a4","nl":false,"rap":true,"rh":0,"inputs":0,"x":160,"y":1200,"wires":[["d1095e459bf1800f"]]},{"id":"d1095e459bf1800f","type":"debug","z":"24d3c36e.663b3c","name":"","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":670,"y":1200,"wires":[]},{"id":"e4374dcfd871eaa9","type":"function","z":"24d3c36e.663b3c","name":"rangehood fan","func":"//this function is part of the control scheme for the kitchen\n//rangehood fan. The fan control has two states: \"off\"(0), \"run\"(1),\n//These states are messaged to a zigbee relay module\n//which controls the fan.\n//Messages are sent on a change of statel\nvar msg1 = {};\nvar msg2 = {};\n\nvar rh_fan_state = 0;\nvar state_changed = false;\nvar active = false;\n\n// Setpoints reference the difference in absolute humidity \n// and temperature between a sensor under the rangehood and a\n// reference sensor in the kitchen\n\nconst delta_h_on = 1.0; // absolute humidity differential\nconst delta_h_off = 2.0; //\n\nconst delta_t_on = 2.0; // temperature differential\nconst delta_t_off = 6.0; // \n\nconst cool_down_time = 300; // max secs run time after temp starts to fall\n// End of Setpoints\n\n// initialise flow context memory if it doesn't exist already\nvar startup = context.get('startup_rh')||0;\nvar timesecs = global.get('timesecs')||0; // time(s) from 20220101\nvar delta_h_mem = context.get('delta_h_mem')||0; // for info only\nvar delta_t_mem = context.get('delta_t_mem')||0; // for info only\nvar t_state_mem = context.get('t_state_mem')||0; // temp falling: -1, rising: +1\nvar rh_fan_state_mem = flow.get('rh_fan_state_mem')||0;\n//var rh_fan_state_changed_mem = flow.get('rh_fan_state_changed_mem')||0;\nvar fan_on_time = context.get('fan_on_time')|0;\nvar fan_cool_down_start = context.get('fan_cool_down_start')||0; // time when temp starts to drop\nvar fan_run_time = context.get('fan_run_time')||0; // fan run time\nvar fan_cool_down_time = context.get('fan_cool_down_time')||0; // fan run time after temp strats dropping\nvar airquality_filtered = global.get('airquality_filtered')||0;\nvar ref_humidity = airquality_filtered.humidity;\nvar co2_filtered = airquality_filtered.co2;\nvar rt = global.get('hvac_global.Room_Temp.nKitchen')||0; // from LM35 temp sensor in kitchen via PLC\nvar ref_temp = Math.round(rt/5)/20; //rt/100; \ncontext.set('ref_temp', ref_temp);\nvar abshumid = context.get('abshumid')||0; // for info only\n\n// at range hood - calculate absolute humidity\nvar temperature = msg.payload.temperature;\nvar t_state = msg.payload.t_state; // 1 = temp rising, -1 = falling\nvar humidity = msg.payload.humidity; //Relative Humidity (RH) from Xiaomi aqara sensor under rangehood\n// RH must be converted to AH\n// Absolute Humidity (AH) formula : https://github.com/atmos-python/atmos/issues/3 mcgibbon\nlet p = msg.payload.pressure*100\nlet RH = humidity;\nlet T = temperature + 273.15;\nlet es = 611.2* Math.exp(17.67*(T-273.15)/(T-29.65))\nlet rvs = 0.622*es/(p - es)\nlet rv = RH/100.0 * rvs\nlet qv = rv/(1 + rv)\n//AH = qv * 1000.0\nlet AH = Math.round(qv*100000) / 100;\ncontext.set('abshumid', AH);\n\n// absolute humidity reference, from sensor in hall next to kitchen \nRH = Math.round(ref_humidity/0.5)*0.5; // round to nearest 0.5\nT = ref_temp + 273.15;\nes = 611.2* Math.exp(17.67*(T-273.15)/(T-29.65))\nrvs = 0.622*es/(p - es)\nrv = RH/100.0 * rvs\nqv = rv/(1 + rv)\n//AH = qv * 1000.0\nlet ref_AH = Math.round(qv*100000) / 100;\ncontext.set('absrefhumid', ref_AH);\n\n// difference between absolute humidity levels\nvar delta_h = Math.round((AH - ref_AH)*100)/100;\ncontext.set('delta_h_mem', delta_h);\n\nvar delta_t = Math.round((temperature - ref_temp)*100)/100;\ncontext.set('delta_t_mem', delta_t);\n\n // on conditions: fan off and temp rising\nif (rh_fan_state_mem == 0 && t_state == 1){\n if (delta_h > delta_h_on || delta_t > delta_t_on){\n rh_fan_state= 1;\n active = true;\n context.set('fan_on_time', timesecs);\n }\n}\n// off conditions: fan on and temp is falling \nif (rh_fan_state_mem == 1 && t_state_mem == -1 ){\n if((delta_h<=delta_h_off&&delta_t<=delta_t_off)||(fan_cool_down_time>=cool_down_time)){\n rh_fan_state = 0;\n active = true;\n }\n} \n// off conditions: fan on and temp and humidity are below \"on\" thresholds\nif (rh_fan_state_mem == 1){\n if(delta_h<delta_h_on&&delta_t<delta_t_on){\n rh_fan_state = 0;\n active = true;\n }\n} \n\n// check for change in fan state\nif (startup || (active && (rh_fan_state != rh_fan_state_mem ))){\n state_changed = true;\n } \n\n// fan on run time and cool down time after temp starts falling\nif (rh_fan_state_mem == 1){\n fan_run_time = (timesecs - fan_on_time);\n if (t_state == 1){ //temp is rising\n fan_cool_down_time = 0; \n }else if (t_state == -1){ //temperature is falling\n if (t_state != t_state_mem){ // and temp has just started to fall\n fan_cool_down_start = timesecs;\n fan_cool_down_time = 0;\n }else{ \n fan_cool_down_time = timesecs - fan_cool_down_start;\n }\n }\n}\n\ncontext.set('t_state_mem', t_state);\ncontext.set('fan_run_time', fan_run_time);\ncontext.set('fan_cool_down_time', fan_cool_down_time);\ncontext.set('fan_cool_down_start', fan_cool_down_start);\ncontext.set('startup_rh', false);\n\n//for msg2\nvar mypayload = msg.payload;\n// parse the abs humidity into the payload\n// new key, value pair for time and date\nvar abs_humidity = {\"abs_humidity\": AH};\nvar my_delta_h = {\"delta_h\": delta_h};\nvar my_delta_t = {\"delta_t\": delta_t};\n\n// merge oblects\nmsg2.payload = {\n ...mypayload,\n ...abs_humidity,\n ...my_delta_h,\n ...my_delta_t,\n}\nmsg1.topic = \"house/main/kitchen/rangehood/fan/set\"\nmsg2.topic = \"house/main/kitchen/rangehood/thp_abs\"\n\nif (state_changed){\n // store the state value in flow retentive memory\n flow.set('rh_fan_state_mem', rh_fan_state);\n// flow.set('rh_fan_state_changed_mem', true );\n if (rh_fan_state==1){\n msg1.payload = {\"state_l1\":\"ON\",\"state_l2\":\"OFF\"}; \n } else{\n msg1.payload = {\"state_l1\":\"OFF\",\"state_l2\":\"OFF\"}; \n }\n} else{\n msg1.payload = '';\n}\nreturn [msg1, msg2];\n","outputs":2,"noerr":0,"initialize":"// Code added here will be run once\n// whenever the node is started.\n\n//declare variables\nvar startup = context.get('startup_rh')||0;\ncontext.set('startup_rh', true)\n//var rh_fan_state_mem = flow.get('rh_fan_state_mem')||0;\n//var rh_fan_state_changed_mem = flow.get('rh_fan_state_changed_mem')||0;\n\n\n//flow.set('rh_fan_state_mem', -1);\n//flow.set('rh_fan_state_changed_mem', false);\n","finalize":"","libs":[],"x":480,"y":960,"wires":[["11610aa841a73b10"],["32245b9561be06ee"]]},{"id":"7199b3475d8eee3d","type":"comment","z":"24d3c36e.663b3c","name":"air quality data to plc and hrv control","info":"","x":180,"y":520,"wires":[]},{"id":"44522daa8e19e351","type":"comment","z":"24d3c36e.663b3c","name":"about the control scheme","info":"Scheme for kitchen rangehood fan:\nControl is based on the temperature differential and the absolute humidity differential between a sensor located under the rangehood (Xiaomi Aqara WSDCGQ11LM) and a reference temperature and humidity sensors located in the kitchen.\nA zigbee TuYa TS0002 2 gang switch provides the motor control, only 1 gang is used.\nMessages are sent on a change of state and refereshed on a timed interval\n\n\n","x":450,"y":920,"wires":[]},{"id":"1a8768b0f4fa5628","type":"comment","z":"24d3c36e.663b3c","name":"rangehood fan state","info":"","x":110,"y":1160,"wires":[]},{"id":"e37002dfadc2546c","type":"comment","z":"24d3c36e.663b3c","name":"read me","info":"override off is delayed by the 'cool_down_time' setting in the rangehood fan node ","x":240,"y":1100,"wires":[]},{"id":"71c672037992e22e","type":"mqtt in","z":"24d3c36e.663b3c","name":"","topic":"house/upstairs/guestbath/sensor/thp","qos":"2","datatype":"json","broker":"48982d6c.76a7a4","nl":false,"rap":true,"rh":0,"inputs":0,"x":160,"y":1380,"wires":[["be90630e9d59d0fb"]]},{"id":"5be38a1514d28990","type":"comment","z":"24d3c36e.663b3c","name":"Xiaomi Aqara temp/humidity sensor","info":"","x":160,"y":1340,"wires":[]},{"id":"b3bce4f74314e92f","type":"mqtt out","z":"24d3c36e.663b3c","name":"house/upstairs/guestbath/ventfan","topic":"","qos":"2","retain":"","respTopic":"","contentType":"","userProps":"","correl":"","expiry":"","broker":"48982d6c.76a7a4","x":1080,"y":1500,"wires":[]},{"id":"28f649fbfb7ff934","type":"mqtt in","z":"24d3c36e.663b3c","name":"","topic":"house/upstairs/guestbath/ventfan","qos":"0","datatype":"json","broker":"48982d6c.76a7a4","nl":false,"rap":true,"rh":0,"inputs":0,"x":150,"y":1660,"wires":[["d86b03f860f180f7"]]},{"id":"d86b03f860f180f7","type":"debug","z":"24d3c36e.663b3c","name":"","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":470,"y":1660,"wires":[]},{"id":"f4cbc9bc68433336","type":"inject","z":"24d3c36e.663b3c","name":"get state","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":true,"onceDelay":0.1,"topic":"house/upstairs/guestbath/ventfan/get","payload":"{\"state\":\"\"}","payloadType":"json","x":740,"y":1620,"wires":[["b3bce4f74314e92f"]]},{"id":"f539559b73a4a4bf","type":"function","z":"24d3c36e.663b3c","name":"hrv speedup ","func":"// process request for hrv fan speedup\nvar bitmask = 1;\n\nvar rh_fan_state = flow.get('rh_fan_state_mem')||0;\nvar gb_fan_state = flow.get('gb_fan_state_mem')||0;\nvar es_fan_state = flow.get('es_fan_state_mem')||0;\n\nfan_state = rh_fan_state + gb_fan_state + es_fan_state;\nif (fan_state > 1){\n fan_state = 1;\n} \n\nmsg.payload = fan_state & bitmask; // bit 0 controls HRV fan\nreturn msg;\n","outputs":1,"noerr":0,"initialize":"// Code added here will be run once\n// whenever the node is started.\n//var startup = context.get('startup_hrv')||0;\n//context.set('startup_hrv', true)","finalize":"","libs":[],"x":290,"y":600,"wires":[["6dc9e3ddf72d4845"]]},{"id":"c5a658b93e4cc94b","type":"eth-ip out","z":"24d3c36e.663b3c","endpoint":"315be0a9031b15eb","variable":"nhouse_airquality_co2","program":"","name":"co2 to PLC","x":730,"y":520,"wires":[]},{"id":"e81762237fa80595","type":"eth-ip out","z":"24d3c36e.663b3c","endpoint":"315be0a9031b15eb","variable":"nFromNodeRed","program":"","name":"hrv speedup to PLC","x":580,"y":720,"wires":[]},{"id":"148cd070faf1bb85","type":"debug","z":"24d3c36e.663b3c","name":"","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":1010,"y":1460,"wires":[]},{"id":"fb9648c13bc55d8d","type":"inject","z":"24d3c36e.663b3c","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"{\"humidity\":65}","payloadType":"json","x":280,"y":1520,"wires":[["70090fecfd422414"]]},{"id":"252c45b56ff83e70","type":"inject","z":"24d3c36e.663b3c","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"{\"humidity\":50}","payloadType":"json","x":280,"y":1600,"wires":[["70090fecfd422414"]]},{"id":"87a6fee6d4c511fe","type":"inject","z":"24d3c36e.663b3c","name":"turn on","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"26","payloadType":"num","x":90,"y":1060,"wires":[["1b91da19b080a9d9"]]},{"id":"19caecd7ee5b6674","type":"inject","z":"24d3c36e.663b3c","name":"turn off","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"20","payloadType":"json","x":90,"y":1100,"wires":[["426d4cc261506446"]]},{"id":"c17e720562c374a3","type":"inject","z":"24d3c36e.663b3c","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":true,"onceDelay":0.1,"topic":"house/upstairs/guestbath/ventfan/set","payload":"{\"state\": \"ON\"}","payloadType":"json","x":760,"y":1540,"wires":[["b3bce4f74314e92f"]]},{"id":"1d05dc150279c8d5","type":"inject","z":"24d3c36e.663b3c","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"{\"humidity\":63}","payloadType":"json","x":280,"y":1560,"wires":[["70090fecfd422414"]]},{"id":"587f1d282f982ac8","type":"inject","z":"24d3c36e.663b3c","name":"guest bath humidity","props":[{"p":"payload"}],"repeat":"10","crontab":"","once":true,"onceDelay":".75","topic":"","payload":"gb_temp_humid","payloadType":"flow","x":140,"y":1480,"wires":[["70090fecfd422414"]]},{"id":"08dbf167eee4946e","type":"comment","z":"24d3c36e.663b3c","name":"inject the sensor data ","info":"","x":120,"y":1440,"wires":[]},{"id":"6844258890224f46","type":"mqtt out","z":"24d3c36e.663b3c","name":"house/basement/hall/doorchime","topic":"","qos":"0","retain":"","respTopic":"","contentType":"","userProps":"","correl":"","expiry":"","broker":"48982d6c.76a7a4","x":970,"y":2460,"wires":[]},{"id":"33c773b3d43382e0","type":"comment","z":"24d3c36e.663b3c","name":"guest bath hrv vent control","info":" - Xiaomi Aqara Temp/Humid Sensor - WSDCGQ11LM\n - eWeLink - ZB-SW02\n Controlling Eneready Heat Rocovery Ventilator -HRV","x":130,"y":1300,"wires":[]},{"id":"a64fd2c7260a051f","type":"comment","z":"24d3c36e.663b3c","name":"garage door monitoring ","info":"Hall effect sensors monitor the garage door open/closed state. The sensors are on a low voltage DC circuit and interface to TuYa zigbee TS0001 switch modules through triac optoisolators.\nThis is not the ideal way to monitor a binary state, however I was not able to find a more suitable zigbee device to monitor an on/off state\n","x":340,"y":2200,"wires":[]},{"id":"2cd4a0cf3a9f1a52","type":"mqtt in","z":"24d3c36e.663b3c","name":"","topic":"house/basement/garagedoor/sensor/south","qos":"2","datatype":"json","broker":"48982d6c.76a7a4","nl":false,"rap":true,"rh":0,"inputs":0,"x":180,"y":2280,"wires":[["e822d1089732c291","cc36e4520ad60742","999aab2ed64311cf"]]},{"id":"791444575ccaae55","type":"mqtt in","z":"24d3c36e.663b3c","name":"","topic":"house/basement/garagedoor/sensor/north","qos":"2","datatype":"json","broker":"48982d6c.76a7a4","nl":false,"rap":true,"rh":0,"inputs":0,"x":180,"y":2340,"wires":[["e822d1089732c291","86d548c41d3f6cd0","999aab2ed64311cf"]]},{"id":"0b496845fd46c0b5","type":"comment","z":"24d3c36e.663b3c","name":"back door light (automatic off delay)","info":"Limit the maximum time that the backdoor light will stay on.","x":340,"y":2580,"wires":[]},{"id":"1867bc7bc6b7bb65","type":"mqtt in","z":"24d3c36e.663b3c","name":"","topic":"house/main/northentry/light/control","qos":"2","datatype":"json","broker":"48982d6c.76a7a4","nl":false,"rap":true,"rh":0,"inputs":0,"x":160,"y":2660,"wires":[["e6f4f72d1ac65a10","38335fc27b0f32bc"]]},{"id":"9bc581d9f6bd3649","type":"inject","z":"24d3c36e.663b3c","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"house/main/northentry/light/control/get","payload":"{\"state\":\"\"}","payloadType":"json","x":320,"y":2860,"wires":[["503b124abd885552"]]},{"id":"503b124abd885552","type":"mqtt out","z":"24d3c36e.663b3c","name":"house/main/northentry/light/control","topic":"","qos":"2","retain":"","respTopic":"","contentType":"","userProps":"","correl":"","expiry":"","broker":"48982d6c.76a7a4","x":660,"y":2820,"wires":[]},{"id":"60d5c9d05fba61d0","type":"inject","z":"24d3c36e.663b3c","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"house/main/northentry/light/control/set","payload":"{\"state\":\"ON\"}","payloadType":"json","x":310,"y":2820,"wires":[["503b124abd885552"]]},{"id":"b487bfabdcaceb0e","type":"inject","z":"24d3c36e.663b3c","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"house/main/northentry/light/control/set","payload":"{\"state\":\"OFF\"}","payloadType":"json","x":300,"y":2780,"wires":[["503b124abd885552"]]},{"id":"bcd793881e1b126d","type":"function","z":"24d3c36e.663b3c","name":"off delay timer","func":"\n// this function provides an off delay for an entry door light\n// the switching device is a TuYa TS0001. \n// the original toggle switch is wired to the TS0001 switch inputs\n\n// incoming msg.payload is from the global timesecs value (seconds since 20220101)\n// which is posted in the \"Utilities\" flow\n\n// get flow data\nvar n_entry_light_state = flow.get('n_entry_light_state')||\"\";\nvar light_state_mem = context.get('light_state_mem')||\"\";\nvar turn_on_time = context.get('turn_on_time')||0;\nvar changed = false;\nvar acc_time = 0; //accumulated time\n\nvar timesecs = msg.payload;\n\n// setpoint, delay time before turning off the light\nconst time_sp = 2700; //45 minutes\n\nif(n_entry_light_state != light_state_mem){\n changed = true;\n}\n// store the lamp state in context memory\ncontext.set('light_state_mem', n_entry_light_state);\n\nif (n_entry_light_state==\"ON\"){\n if (changed){\n context.set('turn_on_time', timesecs);\n }else{\n acc_time = timesecs - turn_on_time ;\n if (acc_time >= time_sp){\n msg.payload = {\"state\":\"OFF\"}\n msg.topic = \"house/main/northentry/light/control/set\";\n return msg;\n } \n } \n} \n \n\n","outputs":1,"noerr":0,"initialize":"// Code added here will be run once\n// whenever the node is started.\n//var north_entry_light_on = context.get('north_entry_light_on')||0;","finalize":"","libs":[],"x":380,"y":2720,"wires":[["503b124abd885552","f956db68be6f5f1e"]]},{"id":"e6f4f72d1ac65a10","type":"change","z":"24d3c36e.663b3c","name":"","rules":[{"t":"set","p":"n_entry_light_state","pt":"flow","to":"payload.state","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":520,"y":2660,"wires":[[]]},{"id":"e14fe53ef91c5108","type":"inject","z":"24d3c36e.663b3c","name":"","props":[{"p":"payload"}],"repeat":"7","crontab":"","once":true,"onceDelay":"0. 85","topic":"","payload":"timesecs","payloadType":"global","x":130,"y":2720,"wires":[["bcd793881e1b126d"]]},{"id":"f956db68be6f5f1e","type":"debug","z":"24d3c36e.663b3c","name":"","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":710,"y":2720,"wires":[]},{"id":"69090e79af6a61e0","type":"comment","z":"24d3c36e.663b3c","name":"for testing","info":"","x":80,"y":1020,"wires":[]},{"id":"1c945c64a4a6840e","type":"comment","z":"24d3c36e.663b3c","name":"for testing","info":"","x":100,"y":1560,"wires":[]},{"id":"b3a8744ea8db0c01","type":"comment","z":"24d3c36e.663b3c","name":"for testing","info":"","x":120,"y":2820,"wires":[]},{"id":"5d400fe752f550db","type":"mqtt in","z":"24d3c36e.663b3c","name":"","topic":"house/main/kitchen/rangehood/thp","qos":"2","datatype":"json","broker":"48982d6c.76a7a4","nl":false,"rap":true,"rh":0,"inputs":0,"x":160,"y":860,"wires":[["8aac2df9124477dd"]]},{"id":"60d73361d0be22cf","type":"change","z":"24d3c36e.663b3c","name":"temp and humidity","rules":[{"t":"move","p":"payload","pt":"msg","to":"rh_temp_humid","tot":"flow"}],"action":"","property":"","from":"","to":"","reg":false,"x":730,"y":860,"wires":[[]]},{"id":"8aac2df9124477dd","type":"function","z":"24d3c36e.663b3c","name":"process rh sensor data","func":"// context memory to determine if temperature is rising or falling \n\nvar t_state = flow.get('rh_t_state')||0; //temp rising or falling\nvar t_last = context.get('t_last')||0; //last temp reading\n\n// is temperature rising or falling?\nlet t = msg.payload.temperature;\nif (t > (t_last+0.25)){ // rising\n t_state = 1;\n}else if (t < (t_last-0.25)){\n t_state = -1;\n}\nflow.set('rh_t_state', t_state );\ncontext.set('t_last', t);\n\n// function to insert a key value pair into an existing message payload\n// get global date string\nvar date = global.get('date_string')||' ';\n\n// new key, value pair for time and date\nvar mytime = {\"time\": date}\nvar myt_state = {\"t_state\": t_state}\n\nvar mypayload = msg.payload;\n\n// merge oblects\nmsg.payload = {\n ...mytime,\n ...myt_state,\n ...mypayload\n};\n\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":480,"y":860,"wires":[["60d73361d0be22cf"]]},{"id":"9f62030a8d9cfccc","type":"inject","z":"24d3c36e.663b3c","name":"range hood temp & humidity","props":[{"p":"payload"}],"repeat":"5","crontab":"","once":true,"onceDelay":".75","topic":"","payload":"rh_temp_humid","payloadType":"flow","x":160,"y":960,"wires":[["e4374dcfd871eaa9"]]},{"id":"1b91da19b080a9d9","type":"function","z":"24d3c36e.663b3c","name":"override on","func":"// function to insert a key value pair into an existing message payload\n\n// test override\nvar override = msg.payload;\n\nvar mypayload = flow.get('rh_temp_humid')||0;\nmsg.payload = mypayload;\n\n//fake temperature rising\nflow.set('rh_t_state', 1);\n//apply override\nmsg.payload.temperature = override;\nmsg.payload.t_state = 1;\n\n\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":250,"y":1020,"wires":[["e4374dcfd871eaa9","5aabcecfb725207b"]]},{"id":"5aabcecfb725207b","type":"debug","z":"24d3c36e.663b3c","name":"","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":470,"y":1020,"wires":[]},{"id":"396c524bd114e237","type":"debug","z":"24d3c36e.663b3c","name":"","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":910,"y":1140,"wires":[]},{"id":"babcb7c4b78791a5","type":"mqtt out","z":"24d3c36e.663b3c","name":"house/main/kitchen/rangehood/thp_abs","topic":"","qos":"2","retain":"","respTopic":"","contentType":"","userProps":"","correl":"","expiry":"","broker":"48982d6c.76a7a4","x":1000,"y":1080,"wires":[]},{"id":"dada95978506ceda","type":"mqtt in","z":"24d3c36e.663b3c","name":"","topic":"house/upstairs/ensuite/ventfan","qos":"0","datatype":"json","broker":"48982d6c.76a7a4","nl":false,"rap":true,"rh":0,"inputs":0,"x":140,"y":2120,"wires":[["01c68beea271de47"]]},{"id":"01c68beea271de47","type":"debug","z":"24d3c36e.663b3c","name":"","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":450,"y":2120,"wires":[]},{"id":"7c98a54f95639404","type":"inject","z":"24d3c36e.663b3c","name":"get state","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":true,"onceDelay":0.1,"topic":"house/upstairs/ensuite/ventfan/get","payload":"{\"state\":\"\"}","payloadType":"json","x":740,"y":2060,"wires":[["30ad1eb2bf5c0eaf"]]},{"id":"30ad1eb2bf5c0eaf","type":"mqtt out","z":"24d3c36e.663b3c","name":"house/upstairs/ensuite/ventfan","topic":"","qos":"2","retain":"","respTopic":"","contentType":"","userProps":"","correl":"","expiry":"","broker":"48982d6c.76a7a4","x":1030,"y":2000,"wires":[]},{"id":"85f7300f582be358","type":"inject","z":"24d3c36e.663b3c","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":true,"onceDelay":0.1,"topic":"house/upstairs/ensuite/ventfan/set","payload":"{\"state\": \"OFF\"}","payloadType":"json","x":720,"y":2020,"wires":[["30ad1eb2bf5c0eaf"]]},{"id":"e4f0aa97f0e9bbcd","type":"inject","z":"24d3c36e.663b3c","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":true,"onceDelay":0.1,"topic":"house/upstairs/ensuite/ventfan/set","payload":"{\"state\": \"ON\"}","payloadType":"json","x":720,"y":1980,"wires":[["30ad1eb2bf5c0eaf"]]},{"id":"426d4cc261506446","type":"function","z":"24d3c36e.663b3c","name":"override off","func":"// function to insert a key value pair into an existing message payload\n\n// test override\nvar override = msg.payload;\n\nvar mypayload = flow.get('rh_temp_humid')||0;\nmsg.payload = mypayload;\n\n//fake temperature falling\nflow.set('rh_t_state', -1);\n//apply override\nmsg.payload.temperature = override;\nmsg.payload.t_state = -1;\n\n\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":250,"y":1060,"wires":[["e4374dcfd871eaa9","5aabcecfb725207b"]]},{"id":"4ff04c55a03dfc0d","type":"function","z":"24d3c36e.663b3c","name":"ensuite bath hrv vent 'es'","func":"// this function monitors bathroom humdity and actuates the HRV\n//ventilator when the humidity exceeds a specified value\n\n// initialise flow context memory if it doesn't exist already\nvar es_fan_state = 0;\nvar es_fan_state_mem = flow.get('es_fan_state_mem')||0;\nvar active = false;\nvar humidity = msg.payload.humidity;\nvar delta_h = msg.payload.humidity - msg.payload.humidity_floor;\n\n// Setpoints reference delta humidity above floor (min) value\nconst sp_on_delta_h = 6;\nconst sp_off_delta_h = 5;\n\nif (delta_h >= sp_on_delta_h){\n es_fan_state= 1;\n active = true;\n}else if (delta_h < sp_off_delta_h) {\n es_fan_state = 0;\n active = true;\n} \nif (active){\n flow.set('es_fan_state_mem', es_fan_state);\n es_fan_state_mem = flow.get('es_fan_state_mem');\n}\n\nmsg.topic = \"house/upstairs/ensuite/ventfan/set\"\n//controlling a ziesee 3 mode switch (default mode)\n//ZB-SW01\nif (es_fan_state_mem==1){\n msg.payload = {\"state\": \"ON\"};\n} else{\n msg.payload = {\"state\":\"OFF\"};\n}\nreturn msg;\n\n","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":530,"y":1940,"wires":[["73d5c3aa12109cac"]]},{"id":"64f517e65684ff0e","type":"debug","z":"24d3c36e.663b3c","name":"","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":970,"y":1940,"wires":[]},{"id":"2906413b25a60cb7","type":"inject","z":"24d3c36e.663b3c","name":"ensuite bath humidity","props":[{"p":"payload"}],"repeat":"8","crontab":"","once":true,"onceDelay":".75","topic":"","payload":"es_temp_humid","payloadType":"flow","x":140,"y":1940,"wires":[["4ff04c55a03dfc0d"]]},{"id":"3b426a1b9afebdcc","type":"comment","z":"24d3c36e.663b3c","name":"inject ensuite bath sensor data on 10s intervals","info":"","x":200,"y":1900,"wires":[]},{"id":"7768e49ec66b1d99","type":"comment","z":"24d3c36e.663b3c","name":"ensuite bath hrv vent control","info":" - Xiaomi Aqara Temp/Humid Sensor - WSDCGQ11LM\n - eWeLink - ZB-SW01\n Controlling Eneready Heat Rocovery Ventilator -HRV","x":380,"y":1740,"wires":[]},{"id":"f6ef9ba4a01a5216","type":"inject","z":"24d3c36e.663b3c","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"{\"humidity\":56}","payloadType":"json","x":260,"y":1980,"wires":[["4ff04c55a03dfc0d"]]},{"id":"a000e5833949cc8c","type":"inject","z":"24d3c36e.663b3c","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"{\"humidity\":30}","payloadType":"json","x":260,"y":2060,"wires":[["4ff04c55a03dfc0d"]]},{"id":"2cbf6630b201ccde","type":"inject","z":"24d3c36e.663b3c","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"{\"humidity\":52}","payloadType":"json","x":260,"y":2020,"wires":[["4ff04c55a03dfc0d"]]},{"id":"15d10e9b222ab3c6","type":"comment","z":"24d3c36e.663b3c","name":"for testing","info":"","x":80,"y":2020,"wires":[]},{"id":"23ef2b723e51e05b","type":"comment","z":"24d3c36e.663b3c","name":"ensuite vent control","info":"","x":530,"y":1900,"wires":[]},{"id":"a2df0d4ccd20df45","type":"catch","z":"24d3c36e.663b3c","name":"","scope":null,"uncaught":false,"x":100,"y":2980,"wires":[["80bde2a4e8fb0d0e"]]},{"id":"80bde2a4e8fb0d0e","type":"debug","z":"24d3c36e.663b3c","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"error.message","targetType":"msg","statusVal":"","statusType":"auto","x":370,"y":2980,"wires":[]},{"id":"1af3baadaa36588d","type":"eth-ip out","z":"24d3c36e.663b3c","endpoint":"315be0a9031b15eb","variable":"nhouse_airquality_voc","program":"","name":"voc*10 to PLC","x":820,"y":560,"wires":[]},{"id":"43db61d7cbf5e60c","type":"eth-ip out","z":"24d3c36e.663b3c","endpoint":"315be0a9031b15eb","variable":"nhouse_airquality_formaldehyde","program":"","name":"formaldehyde*10 to PLC","x":910,"y":600,"wires":[]},{"id":"874821badd532a2d","type":"comment","z":"24d3c36e.663b3c","name":"inject air quality data on 1s intervals, filter and save to global context","info":"","x":260,"y":280,"wires":[]},{"id":"8a9c30b1ee3a1e71","type":"change","z":"24d3c36e.663b3c","name":"filtered co2 to global","rules":[{"t":"move","p":"payload","pt":"msg","to":"airquality_filtered.co2","tot":"global"}],"action":"","property":"","from":"","to":"","reg":false,"x":980,"y":260,"wires":[[]]},{"id":"5f941a395b5176e0","type":"change","z":"24d3c36e.663b3c","name":"filtered formaldehyde*10 to global","rules":[{"t":"move","p":"payload","pt":"msg","to":"airquality_filtered.formaldehyd","tot":"global"}],"action":"","property":"","from":"","to":"","reg":false,"x":1020,"y":300,"wires":[[]]},{"id":"181c37bab884a703","type":"change","z":"24d3c36e.663b3c","name":"filtered humidity to global","rules":[{"t":"move","p":"payload","pt":"msg","to":"airquality_filtered.humidity","tot":"global"}],"action":"","property":"","from":"","to":"","reg":false,"x":990,"y":340,"wires":[[]]},{"id":"1c10a6569a1c4b0a","type":"change","z":"24d3c36e.663b3c","name":"filtered voc*10 to global","rules":[{"t":"move","p":"payload","pt":"msg","to":"airquality_filtered.voc","tot":"global"}],"action":"","property":"","from":"","to":"","reg":false,"x":990,"y":380,"wires":[[]]},{"id":"23aab75751e3ac3f","type":"smooth","z":"24d3c36e.663b3c","name":"","property":"payload","action":"mean","count":"60","round":"0","mult":"single","reduce":false,"x":660,"y":300,"wires":[["8d5e705282d17e73"]]},{"id":"9085609bc59598c5","type":"smooth","z":"24d3c36e.663b3c","name":"","property":"payload","action":"mean","count":"60","round":"0","mult":"multi","reduce":false,"x":660,"y":380,"wires":[["a5fbbe7aa00e8e1c"]]},{"id":"51792229736fcbf8","type":"function","z":"24d3c36e.663b3c","name":"parse data","func":"//create object if undefined\nvar airquality_filtered = global.get('airquality_filtered')||{};\n \nvar msg1 = {};\nvar msg2 = {};\nvar msg3 = {};\nvar msg4 = {};\n\n\nmsg1.payload = msg.payload.co2;\nmsg2.payload = msg.payload.formaldehyd * 10;\nmsg3.payload = msg.payload.humidity;\nmsg4.payload = msg.payload.voc * 10;\n\nreturn [msg1, msg2, msg3, msg4];\n\n","outputs":4,"noerr":0,"initialize":"","finalize":"","libs":[],"x":390,"y":340,"wires":[["04915e48c998a89d"],["23aab75751e3ac3f"],["9cebf317d560a3d8"],["9085609bc59598c5"]]},{"id":"b525b31642e777c4","type":"inject","z":"24d3c36e.663b3c","name":"flow context air quality data","props":[{"p":"payload"}],"repeat":"1","crontab":"","once":true,"onceDelay":"0.25","topic":"","payload":"airquality","payloadType":"flow","x":160,"y":340,"wires":[["51792229736fcbf8"]]},{"id":"c7b974c5df93fcd4","type":"comment","z":"24d3c36e.663b3c","name":"for testing","info":"","x":540,"y":2020,"wires":[]},{"id":"e822d1089732c291","type":"debug","z":"24d3c36e.663b3c","name":"","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":510,"y":2380,"wires":[]},{"id":"86d548c41d3f6cd0","type":"change","z":"24d3c36e.663b3c","name":"","rules":[{"t":"move","p":"payload.state","pt":"msg","to":"garage_door_north","tot":"flow"}],"action":"","property":"","from":"","to":"","reg":false,"x":690,"y":2320,"wires":[[]]},{"id":"cc36e4520ad60742","type":"change","z":"24d3c36e.663b3c","name":"","rules":[{"t":"move","p":"payload.state","pt":"msg","to":"garage_door_south","tot":"flow"}],"action":"","property":"","from":"","to":"","reg":false,"x":690,"y":2260,"wires":[[]]},{"id":"999aab2ed64311cf","type":"function","z":"24d3c36e.663b3c","name":"","func":"\n // respond to payload from north and south garage door sensors\n // output \n var garage_door_state = flow.get('garage_door_state')||{};\n \n if (msg.topic == \"house/basement/garagedoor/sensor/south\") {\n garage_door_state.south = msg.payload \n }else if (msg.topic==\"house/basement/garagedoor/sensor/north\") {\n garage_door_state.north = msg.payload \n }\n \n flow.set('garage_door_state', garage_door_state);\n \n \n \n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n\n\n\n\n\nreturn msg;\n\n\n","outputs":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":480,"y":2300,"wires":[]},{"id":"8928c7b5c05cd521","type":"comment","z":"24d3c36e.663b3c","name":"Xiaomi Aqara Temperature, Humidity, & Pressure ","info":"","x":200,"y":820,"wires":[]},{"id":"accf1196bf134497","type":"comment","z":"24d3c36e.663b3c","name":"save to flow context","info":"","x":730,"y":820,"wires":[]},{"id":"597d5b772f346fb8","type":"comment","z":"24d3c36e.663b3c","name":"exaust hood over gas stove in kitchen ","info":"","x":330,"y":780,"wires":[]},{"id":"93348fec657b79a0","type":"comment","z":"24d3c36e.663b3c","name":"Inject the sensor data","info":"","x":120,"y":920,"wires":[]},{"id":"617d0301439bd62d","type":"comment","z":"24d3c36e.663b3c","name":"for graphs","info":"","x":1060,"y":1040,"wires":[]},{"id":"1fa608320a8f4501","type":"comment","z":"24d3c36e.663b3c","name":"TuYa TS0002 zigbee switch","info":"","x":980,"y":920,"wires":[]},{"id":"8f2271291d7e5f83","type":"comment","z":"24d3c36e.663b3c","name":"for information","info":"","x":850,"y":1200,"wires":[]},{"id":"a49f812bbb506f82","type":"comment","z":"24d3c36e.663b3c","name":"control guest bath vent & fan","info":"","x":560,"y":1440,"wires":[]},{"id":"6e74afca4a878ef2","type":"comment","z":"24d3c36e.663b3c","name":"for testing","info":"","x":580,"y":1560,"wires":[]},{"id":"d4f6d44a752a9b40","type":"comment","z":"24d3c36e.663b3c","name":"eWeLink ZB-SW01","info":"","x":1030,"y":1560,"wires":[]},{"id":"4a53c8b98c8c222f","type":"comment","z":"24d3c36e.663b3c","name":"eWeLink ZB-SW01","info":"","x":1070,"y":2040,"wires":[]},{"id":"b0c0b4abc832c285","type":"comment","z":"24d3c36e.663b3c","name":"TuYa TS0001","info":"","x":730,"y":2780,"wires":[]},{"id":"c6084548f1aef24e","type":"comment","z":"24d3c36e.663b3c","name":"PLC controlling heating and hrv ventilation","info":"","x":980,"y":520,"wires":[]},{"id":"4fee20eb07b45bcb","type":"eth-ip out","z":"24d3c36e.663b3c","endpoint":"315be0a9031b15eb","variable":"nhouse_airquality_humidity","program":"","name":"humidity*10 to PLC","x":950,"y":640,"wires":[]},{"id":"24133608c1bb8781","type":"function","z":"24d3c36e.663b3c","name":"filtered data to mqtt","func":"\n\n// restore formaldehyde and voc readings to original scaling\nmsg.payload.formaldehyd = msg.payload.formaldehyd / 10;\nmsg.payload.voc = msg.payload.voc / 10;\nmsg.topic =\"house/main/hall/airquality_filtered\"\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":570,"y":680,"wires":[["a21a8d0a720b0882"]]},{"id":"a21a8d0a720b0882","type":"mqtt out","z":"24d3c36e.663b3c","name":"house/main/hall/airquality_filtered","topic":"","qos":"2","retain":"","respTopic":"","contentType":"","userProps":"","correl":"","expiry":"","broker":"48982d6c.76a7a4","x":1120,"y":720,"wires":[]},{"id":"a638464da087723c","type":"rbe","z":"24d3c36e.663b3c","name":"on change","func":"rbe","gap":"","start":"","inout":"out","septopics":false,"property":"payload","topi":"topic","x":290,"y":560,"wires":[["24133608c1bb8781","bc9847a6df8d6899"]]},{"id":"32245b9561be06ee","type":"rbe","z":"24d3c36e.663b3c","name":"on change","func":"rbe","gap":"","start":"","inout":"out","septopics":true,"property":"payload","topi":"topic","x":710,"y":1100,"wires":[["babcb7c4b78791a5","396c524bd114e237"]]},{"id":"8fdf7a4aa5ece0e6","type":"debug","z":"24d3c36e.663b3c","name":"","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":970,"y":880,"wires":[]},{"id":"e53f047717d4abeb","type":"comment","z":"24d3c36e.663b3c","name":"post to mqtt for graphing","info":"","x":1170,"y":760,"wires":[]},{"id":"a49f8b1803da980b","type":"function","z":"24d3c36e.663b3c","name":"round","func":"// round payload to +/-10 resolution\nvar x = msg.payload;\nmsg.payload = Math.round(x/10)*10;\n\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":790,"y":260,"wires":[["8a9c30b1ee3a1e71"]]},{"id":"16d26788879b8262","type":"function","z":"24d3c36e.663b3c","name":"round","func":"// round payload to +/- 0.1 resolution\nvar x = msg.payload;\n\nmsg.payload = Math.round(x*10)/10;\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":790,"y":340,"wires":[["181c37bab884a703"]]},{"id":"9c14a89b77f52c44","type":"comment","z":"24d3c36e.663b3c","name":"save to flow context","info":"","x":970,"y":60,"wires":[]},{"id":"6dc9e3ddf72d4845","type":"rbe","z":"24d3c36e.663b3c","name":"on change","func":"rbe","gap":"","start":"","inout":"out","septopics":false,"property":"payload","topi":"topic","x":290,"y":660,"wires":[["e81762237fa80595"]]},{"id":"0dc5037bb48fd792","type":"rbe","z":"24d3c36e.663b3c","name":"on change","func":"rbe","gap":"","start":"","inout":"out","septopics":true,"property":"payload","topi":"topic","x":790,"y":1480,"wires":[["b3bce4f74314e92f","148cd070faf1bb85"]]},{"id":"73d5c3aa12109cac","type":"rbe","z":"24d3c36e.663b3c","name":"on change","func":"rbe","gap":"","start":"","inout":"out","septopics":true,"property":"payload","topi":"topic","x":750,"y":1940,"wires":[["30ad1eb2bf5c0eaf","64f517e65684ff0e"]]},{"id":"11610aa841a73b10","type":"rbe","z":"24d3c36e.663b3c","name":"on change","func":"rbe","gap":"","start":"","inout":"out","septopics":true,"property":"payload","topi":"topic","x":710,"y":940,"wires":[["7db2d59d2a32cd98","8fdf7a4aa5ece0e6"]]},{"id":"38335fc27b0f32bc","type":"debug","z":"24d3c36e.663b3c","name":"","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":570,"y":2620,"wires":[]},{"id":"54b437904429f270","type":"comment","z":"24d3c36e.663b3c","name":"TuYa TS0001 switches","info":"","x":120,"y":2240,"wires":[]},{"id":"aaf70ea75f9dc727","type":"comment","z":"24d3c36e.663b3c","name":"TuYa TS0001 switch","info":"","x":110,"y":2620,"wires":[]},{"id":"56477d2504b05231","type":"comment","z":"24d3c36e.663b3c","name":"for testing","info":"","x":700,"y":2540,"wires":[]},{"id":"afef2d2eb40270ac","type":"inject","z":"24d3c36e.663b3c","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":true,"onceDelay":0.1,"topic":"house/upstairs/guestbath/ventfan/set","payload":"{\"state\": \"OFF\"}","payloadType":"json","x":760,"y":1580,"wires":[["b3bce4f74314e92f"]]},{"id":"316acecc91c65a29","type":"function","z":"24d3c36e.663b3c","name":"parse data","func":"// split the incoming payload for processing \n\nvar msg1 = {};\nvar msg2 = {};\nvar msg3 = {};\nvar msg4 = {};\n\nmsg1.topic = \"data/median\"; // set statistics function\nmsg1.payload = msg.payload.co2;\n\n//msg2.topic = \"data/median\"; // set statistics function\n//msg2.payload = msg.payload.temperature;\n\nmsg2.topic = \"data/median\"; // set statistics function\nmsg2.payload = msg.payload.humidity;\n\nmsg3.topic = \"data/median\"; // set statistics function\nmsg3.payload = msg.payload.voc;\n\nmsg4.topic = \"data/median\"; // set statistics function\nmsg4.payload = msg.payload.formaldehyd;\n\nreturn [msg1, msg2, msg3, msg4];","outputs":4,"noerr":0,"initialize":"// Code added here will be run once\n// whenever the node is started.\nvar airquality_flow = flow.get('airquality')||0;\nvar airquality = global.get('airquality')||0;\nairqualty = {'co2':0,'formaldehyd':0,'humidity':0,'humidity_floor':0,'temperature':0,'voc':0};\nairqualty_flow = {'co2':0,'formaldehyd':0,'humidity':0,'temperature':0,'voc':0};\nflow.set('airquality', airquality_flow);\nglobal.set('airquality', airquality);","finalize":"","libs":[],"x":390,"y":200,"wires":[["e26fcd1f2bdeb04e"],["cb1ba9dd506e8250"],["50a8d64a6f3b361c"],["1ce33e226ed9583d"]]},{"id":"e26fcd1f2bdeb04e","type":"statistics","z":"24d3c36e.663b3c","name":"median","dataSetSize":"200","inputField":"payload","inputFieldType":"msg","resultField":"payload","resultFieldType":"msg","parameterField":"","parameterFieldType":"payload","stripFunction":false,"resultOnly":true,"x":740,"y":100,"wires":[["ed436afb6e9fd223"]]},{"id":"ed436afb6e9fd223","type":"change","z":"24d3c36e.663b3c","name":"co2 to flow context","rules":[{"t":"move","p":"payload","pt":"msg","to":"airquality.co2","tot":"flow"}],"action":"","property":"","from":"","to":"","reg":false,"x":970,"y":100,"wires":[[]]},{"id":"cb1ba9dd506e8250","type":"statistics","z":"24d3c36e.663b3c","name":"median","dataSetSize":"200","inputField":"payload","inputFieldType":"msg","resultField":"payload","resultFieldType":"msg","parameterField":"","parameterFieldType":"payload","stripFunction":false,"resultOnly":true,"x":740,"y":140,"wires":[["1f426b713a6005ec"]]},{"id":"1f426b713a6005ec","type":"change","z":"24d3c36e.663b3c","name":"humid to flow context","rules":[{"t":"move","p":"payload","pt":"msg","to":"airquality.humidity","tot":"flow"}],"action":"","property":"","from":"","to":"","reg":false,"x":980,"y":140,"wires":[[]]},{"id":"50a8d64a6f3b361c","type":"change","z":"24d3c36e.663b3c","name":"voc to flow context","rules":[{"t":"move","p":"payload","pt":"msg","to":"airquality.voc","tot":"flow"}],"action":"","property":"","from":"","to":"","reg":false,"x":970,"y":180,"wires":[[]]},{"id":"1ce33e226ed9583d","type":"change","z":"24d3c36e.663b3c","name":"form to flow context","rules":[{"t":"move","p":"payload","pt":"msg","to":"airquality.formaldehyd","tot":"flow"}],"action":"","property":"","from":"","to":"","reg":false,"x":970,"y":220,"wires":[[]]},{"id":"be90630e9d59d0fb","type":"function","z":"24d3c36e.663b3c","name":"parse data ","func":"var gb_humidity_floor = flow.get('gb_humidity_floor')||0;\n\nvar msg1 = {};\nvar msg2 = {};\n\n\nmsg1.topic = \"data/min\"; // set statistics function\nmsg1.payload = msg.payload.humidity;\n\nmsg2.payload = msg.payload;\n\nreturn [msg1, msg2]","outputs":2,"noerr":0,"initialize":"","finalize":"","libs":[],"x":490,"y":1380,"wires":[["946730713ef455f8"],["3204f162bb817764"]]},{"id":"946730713ef455f8","type":"statistics","z":"24d3c36e.663b3c","name":"min","dataSetSize":"60","inputField":"payload","inputFieldType":"msg","resultField":"payload","resultFieldType":"msg","parameterField":"","parameterFieldType":"payload","stripFunction":false,"resultOnly":true,"x":730,"y":1360,"wires":[["df6c4acc6184130f"]]},{"id":"df6c4acc6184130f","type":"change","z":"24d3c36e.663b3c","name":"ref humidity","rules":[{"t":"move","p":"payload","pt":"msg","to":"gb_humidity_floor","tot":"flow"}],"action":"","property":"","from":"","to":"","reg":false,"x":930,"y":1360,"wires":[[]]},{"id":"3204f162bb817764","type":"delay","z":"24d3c36e.663b3c","name":"","pauseType":"delay","timeout":"1","timeoutUnits":"seconds","rate":"1","nbRateUnits":"1","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":false,"allowrate":false,"outputs":1,"x":740,"y":1400,"wires":[["c72bbd4a9b603c94"]]},{"id":"c72bbd4a9b603c94","type":"function","z":"24d3c36e.663b3c","name":"parse time into payload","func":"// function to insert a key value pair into an existing message payload\n\nvar humidity_floor = flow.get('gb_humidity_floor')||0;\n\n// get global date string\nvar date = global.get('date_string')||' ';\n\n// new key, value pair for time and date\nvar mytime = {\"time\": date};\n\nvar mypayload = msg.payload;\n\nvar myhumidity_floor = {\"humidity_floor\": humidity_floor};\n\n// merge oblects\nmsg.payload = {\n ...mytime,\n ...mypayload,\n ...myhumidity_floor\n};\n\nflow.set('gb_temp_humid', msg.payload);\n\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":970,"y":1400,"wires":[["58adebf32c9a545b"]]},{"id":"58adebf32c9a545b","type":"debug","z":"24d3c36e.663b3c","name":"","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":1190,"y":1400,"wires":[]},{"id":"70090fecfd422414","type":"function","z":"24d3c36e.663b3c","name":"guest bath hrv vent 'gb'","func":"// this function monitors bathroom humdity and actuates the HRV\n//ventilator when the humidity exceeds a specified value\n\n// initialise flow context memory if it doesn't exist already\nvar gb_fan_state = 0;\nvar gb_fan_state_mem = flow.get('gb_fan_state_mem')||0;\nvar active = false;\nvar humidity = msg.payload.humidity;\nvar delta_h = msg.payload.humidity - msg.payload.humidity_floor;\n\n// Setpoints reference delta humidity above floor (min) value\nconst sp_on_delta_h = 6;\nconst sp_off_delta_h = 5;\n\nif (delta_h >= sp_on_delta_h){\n gb_fan_state= 1;\n active = true;\n}else if (delta_h < sp_off_delta_h) {\n gb_fan_state = 0;\n active = true;\n} \nif (active){\n flow.set('gb_fan_state_mem', gb_fan_state);\n gb_fan_state_mem = flow.get('gb_fan_state_mem');\n}\n\nmsg.topic = \"house/upstairs/guestbath/ventfan/set\"\n//controlling a zigbee 3 mode switch (default mode)\n//ZB-SW01\nif (gb_fan_state_mem==1){\n msg.payload = {\"state\": \"ON\"};\n} else{\n msg.payload = {\"state\":\"OFF\"};\n}\nreturn msg;\n","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":550,"y":1480,"wires":[["0dc5037bb48fd792"]]},{"id":"3f96d521eb953ece","type":"comment","z":"24d3c36e.663b3c","name":"humidity floor","info":"","x":730,"y":1320,"wires":[]},{"id":"8811f41279d36ad8","type":"inject","z":"24d3c36e.663b3c","name":"","props":[{"p":"payload"}],"repeat":"9","crontab":"","once":true,"onceDelay":"0. 85","topic":"","payload":"timesecs","payloadType":"global","x":130,"y":2460,"wires":[["3485cc76e40bca21","45887bc42956a2d9"]]},{"id":"45887bc42956a2d9","type":"function","z":"24d3c36e.663b3c","name":"annunciate door open","func":"\n // respond to payload from north and south garage door sensors\nvar garage_door_state = flow.get('garage_door_state')||{\"north\": \"unknown\", \"south\": \"unknown\" };\nvar wall_clock = global.get('wall_clock')||{\"hour\": 0, \"minute\": 0, \"second\": 0};\n\n// daytime setpoints\nconst daytime_start = 9;\nconst daytime_end = 19;\n// setpoint for delay before anunciation\nconst time_delay= 1800; // 30 minutes\n\n\nvar day_time = wall_clock.hour >= daytime_start && wall_clock.hour < daytime_end;\n\nvar door_open_mem = context.get('door_open_mem')||0;\nvar door_open_time = context.get('door_open_time')||0;\nvar acc_time = 0; //accumulated time door open\nvar timesecs = msg.payload;\nvar changed = false;\nvar annunciate = false;\nvar door_open = garage_door_state.north.state==\"ON\"||garage_door_state.south.state==\"ON\"\n\n// check for change in state\nif(door_open != door_open_mem){\n changed = true;\n}\ncontext.set('door_open_mem', door_open);\n//controlling a ziesee 3 mode switch (\"inching' mode)\n//ZB-SW02\nif (door_open){\n if(changed){\n door_open_time = msg.payload;\n context.set('door_open_time', door_open_time);\n }else{\n acc_time = timesecs - door_open_time;\n annunciate = acc_time >= time_delay;\n }\n}\n\nmsg.topic = \"house/basement/hall/doorchime/set\";\nif (annunciate){\n msg.payload = {\"state_left\":'OFF',\"state_right\":\"ON\"}\n return msg; \n}\nif (changed && !door_open){\n msg.payload = {\"state_left\":'OFF',\"state_right\":\"OFF\"}\n return msg; \n}\n","outputs":1,"noerr":0,"initialize":"// Code added here will be run once\n// whenever the node is started","finalize":"","libs":[],"x":440,"y":2460,"wires":[["03617c79973a5b75","6844258890224f46"]]},{"id":"981eab3e1f1f31c2","type":"inject","z":"24d3c36e.663b3c","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"house/basement/hall/doorchime/set","payload":"{\"state_left\":\"OFF\",\"state_right\":\"ON\"}","payloadType":"json","x":710,"y":2500,"wires":[["6844258890224f46"]]},{"id":"3485cc76e40bca21","type":"debug","z":"24d3c36e.663b3c","name":"","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":390,"y":2520,"wires":[]},{"id":"03617c79973a5b75","type":"debug","z":"24d3c36e.663b3c","name":"","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":970,"y":2400,"wires":[]},{"id":"a34cc92c7a157c4c","type":"comment","z":"24d3c36e.663b3c","name":"garage door open annunciator","info":"","x":980,"y":2340,"wires":[]},{"id":"04915e48c998a89d","type":"smooth","z":"24d3c36e.663b3c","name":"","property":"payload","action":"mean","count":"60","round":"0","mult":"single","reduce":false,"x":660,"y":260,"wires":[["a49f8b1803da980b"]]},{"id":"9cebf317d560a3d8","type":"smooth","z":"24d3c36e.663b3c","name":"","property":"payload","action":"mean","count":"60","round":"1","mult":"single","reduce":false,"x":660,"y":340,"wires":[["16d26788879b8262"]]},{"id":"3ad168fe9392060e","type":"comment","z":"24d3c36e.663b3c","name":"statistics function","info":"note: the statistical operation is defined by the message topic from the previous function block","x":760,"y":60,"wires":[]},{"id":"8d5e705282d17e73","type":"function","z":"24d3c36e.663b3c","name":"round","func":"// round payload to +/- 5 resolution\nvar x = msg.payload;\nmsg.payload = Math.round(x/5)*5\n\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":790,"y":300,"wires":[["5f941a395b5176e0"]]},{"id":"a5fbbe7aa00e8e1c","type":"function","z":"24d3c36e.663b3c","name":"round","func":"// round payload to +/- 5 resolution\nvar x = msg.payload;\nmsg.payload = Math.round(x/5)*5;\n\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":790,"y":380,"wires":[["1c10a6569a1c4b0a"]]},{"id":"979f312946cd2bb7","type":"mqtt in","z":"24d3c36e.663b3c","name":"","topic":"house/upstairs/ensuite/sensor/thp","qos":"2","datatype":"json","broker":"48982d6c.76a7a4","nl":false,"rap":true,"rh":0,"inputs":0,"x":150,"y":1840,"wires":[["ae7cfaa75c15818c"]]},{"id":"dfdd5a45e0543a23","type":"comment","z":"24d3c36e.663b3c","name":"Xiaomi Aqara temp/humidity sensor","info":"","x":140,"y":1800,"wires":[]},{"id":"ae7cfaa75c15818c","type":"function","z":"24d3c36e.663b3c","name":"parse data ","func":"var es_humidity_floor = flow.get('es_humidity_floor')||0;\n\nvar msg1 = {};\nvar msg2 = {};\n\n\nmsg1.topic = \"data/min\"; // set statistics function\nmsg1.payload = msg.payload.humidity;\n\nmsg2.payload = msg.payload;\n\nreturn [msg1, msg2]","outputs":2,"noerr":0,"initialize":"","finalize":"","libs":[],"x":470,"y":1840,"wires":[["336bff0bb408150e"],["1636a604006a5c7e"]]},{"id":"336bff0bb408150e","type":"statistics","z":"24d3c36e.663b3c","name":"min","dataSetSize":"60","inputField":"payload","inputFieldType":"msg","resultField":"payload","resultFieldType":"msg","parameterField":"","parameterFieldType":"payload","stripFunction":false,"resultOnly":true,"x":710,"y":1800,"wires":[["dd720d07165c6c7e"]]},{"id":"dd720d07165c6c7e","type":"change","z":"24d3c36e.663b3c","name":"ref humidity","rules":[{"t":"move","p":"payload","pt":"msg","to":"es_humidity_floor","tot":"flow"}],"action":"","property":"","from":"","to":"","reg":false,"x":910,"y":1800,"wires":[[]]},{"id":"1636a604006a5c7e","type":"delay","z":"24d3c36e.663b3c","name":"","pauseType":"delay","timeout":"1","timeoutUnits":"seconds","rate":"1","nbRateUnits":"1","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":false,"allowrate":false,"outputs":1,"x":720,"y":1840,"wires":[["a3cf1cdca7a89d76"]]},{"id":"a3cf1cdca7a89d76","type":"function","z":"24d3c36e.663b3c","name":"parse time into payload","func":"// function to insert a key value pair into an existing message payload\n\nvar humidity_floor = flow.get('es_humidity_floor')||0;\n\n// get global date string\nvar date = global.get('date_string')||' ';\n\n// new key, value pair for time and date\nvar mytime = {\"time\": date};\n\nvar mypayload = msg.payload;\n\nvar myhumidity_floor = {\"humidity_floor\": humidity_floor};\n\n// merge oblects\nmsg.payload = {\n ...mytime,\n ...mypayload,\n ...myhumidity_floor\n};\n\nflow.set('es_temp_humid', msg.payload);\n\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":950,"y":1840,"wires":[["ce9bc087d6f90f86"]]},{"id":"ce9bc087d6f90f86","type":"debug","z":"24d3c36e.663b3c","name":"","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":1170,"y":1840,"wires":[]},{"id":"e06c55010982725b","type":"comment","z":"24d3c36e.663b3c","name":"humidity floor","info":"","x":710,"y":1760,"wires":[]},{"id":"ae81d96a92c7462e","type":"mqtt in","z":"24d3c36e.663b3c","name":"","topic":"house/main/hall/airquality_filtered","qos":"2","datatype":"json","broker":"48982d6c.76a7a4","nl":false,"rap":true,"rh":0,"inputs":0,"x":170,"y":2920,"wires":[["2d7904b1d5e0527a"]]},{"id":"2d7904b1d5e0527a","type":"debug","z":"24d3c36e.663b3c","name":"","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":430,"y":2920,"wires":[]},{"id":"5788c2d13762ae98","type":"comment","z":"24d3c36e.663b3c","name":"eWeLink ZB-SW02","info":"This two channel device was chosen because it has an enclosure and can be wall mounted, only one channel is required.","x":970,"y":2520,"wires":[]},{"id":"1391b9fc560ee37f","type":"eth-ip out","z":"24d3c36e.663b3c","endpoint":"315be0a9031b15eb","variable":"nhouse_airquality_humidity_floor","program":"","name":"humidity*10 floor to PLC","x":1030,"y":680,"wires":[]},{"id":"bc9847a6df8d6899","type":"function","z":"24d3c36e.663b3c","name":"parse data to PLC","func":"\nvar msg1 = {};\nvar msg2 = {};\nvar msg3 = {};\nvar msg4 = {};\nvar msg5 = {};\n\n\nmsg1.payload = msg.payload.co2;\nmsg2.payload = msg.payload.voc;\nmsg3.payload = msg.payload.formaldehyd;\nvar x = msg.payload.humidity * 10;\nmsg4.payload = Math.round(x);\nx = msg.payload.humidity_floor * 10;\nmsg5.payload = Math.round(x);\nreturn [msg1, msg2, msg3, msg4, msg5];","outputs":5,"noerr":0,"initialize":"","finalize":"","libs":[],"x":510,"y":560,"wires":[["c5a658b93e4cc94b"],["1af3baadaa36588d"],["43db61d7cbf5e60c"],["4fee20eb07b45bcb"],["1391b9fc560ee37f"]]},{"id":"fe17843988f2b40f","type":"inject","z":"24d3c36e.663b3c","name":"global context air quality data","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"60","crontab":"","once":true,"onceDelay":"0.25","topic":"data/min","payload":"airquality_filtered.humidity","payloadType":"global","x":170,"y":420,"wires":[["448e4ae8d211dc28"]]},{"id":"448e4ae8d211dc28","type":"statistics","z":"24d3c36e.663b3c","name":"min","dataSetSize":"180","inputField":"payload","inputFieldType":"msg","resultField":"payload","resultFieldType":"msg","parameterField":"","parameterFieldType":"payload","stripFunction":false,"resultOnly":true,"x":390,"y":420,"wires":[["8d83ed40861a35d4"]]},{"id":"8d83ed40861a35d4","type":"function","z":"24d3c36e.663b3c","name":"round","func":"// round payload to +/- 0.1 resolution\nvar x = msg.payload;\n\nmsg.payload = Math.round(x*10)/10;\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":530,"y":420,"wires":[["608ce9fa7aedc517"]]},{"id":"608ce9fa7aedc517","type":"change","z":"24d3c36e.663b3c","name":"filtered humidity floor to global","rules":[{"t":"move","p":"payload","pt":"msg","to":"airquality_filtered.humidity_floor","tot":"global"}],"action":"","property":"","from":"","to":"","reg":false,"x":1010,"y":420,"wires":[[]]},{"id":"48982d6c.76a7a4","type":"mqtt-broker","name":"HVAC-rpi","broker":"192.168.11.117","port":"1883","clientid":"","autoConnect":true,"usetls":false,"compatmode":false,"protocolVersion":"4","keepalive":"60","cleansession":true,"birthTopic":"","birthQos":"0","birthPayload":"","birthMsg":{},"closeTopic":"","closeQos":"0","closePayload":"","closeMsg":{},"willTopic":"","willQos":"0","willPayload":"","willMsg":{},"sessionExpiry":""},{"id":"315be0a9031b15eb","type":"eth-ip endpoint","address":"192.168.11.191","slot":"0","cycletime":"500","name":"Logix","vartable":{"":{"nFromNodeRed":{"type":"INT"},"nhouse_airquality_co2":{"type":"INT"},"arrtest":{"type":"INT"},"nhouse_airquality_voc":{"type":"INT"},"nhouse_airquality_formaldehyde":{"type":"INT"},"nhouse_airquality_humidity":{"type":"INT"},"nhouse_airquality_humidity_floor":{"type":"INT"}}}}]