Aquarium Controller - Raspberry PI 4B Node-RED, Atlas-Scientific i2c Devices, NCD i2c Relay Board

Aquarium Controller, Pardon the Topic Title, but I wanted this Project to be found.
This project has had many false starts as a baremetal project on ArduinoDue and later a UWP on RPI3B w/ Win10IoT.

I was particularly impressed by the Atlas IoT Monitoring Software from Atlas-Scientific that runs on a Raspberry PI 3B and Windows10IoT for connecting to their i2c enabled sensors, devices and circuits. Atlas-Scientific Atlas IoT Monitoring Software Link.

This next effort was to create an alternative to the Atlas IoT Software that would satisfy 4 principal objects:

1 Use the Raspberry PI 4B w/ it's extra memory and i2c ports. (throw hardware at the problem)
2 Adopt a well supported development environment under Raspbian (Win10IoT has been abandoned)
3 Include control functionality (Atlas IoT was monitoring only, which is appropriate for their products)
4 Local and Remote display/monitoring.

Node-RED on Raspbian Buster on RaspberryPI4 so far has fit the bill.
My project is in it's infancy, but I'll share what is working now:

DashboardUI

flows.json (14.6 KB)

[{"id":"59165d4d.066034","type":"tab","label":"Sensors","disabled":false,"info":""},{"id":"e00b8f43.9ccf9","type":"i2c in","z":"59165d4d.066034","name":"RTDData","address":"102","command":"","count":"8","x":120,"y":180,"wires":[["6588eab1.c5bdb4"]]},{"id":"791936ff.768018","type":"i2c out","z":"59165d4d.066034","name":"RTDCmnd","address":"102","command":"82","payload":"payload","payloadType":"msg","count":"1","x":110,"y":100,"wires":[["33df9f56.5deff"]]},{"id":"418f9978.531ca8","type":"debug","z":"59165d4d.066034","name":"RawBuff","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","x":520,"y":740,"wires":[]},{"id":"a9306c4d.c7cb9","type":"inject","z":"59165d4d.066034","name":"push1","topic":"","payload":"","payloadType":"num","repeat":"10","crontab":"","once":true,"onceDelay":0.1,"x":120,"y":20,"wires":[["791936ff.768018","863f23ed.6ce2b","4c96ce0f.154ec","9214c7e3.12f458"]]},{"id":"33df9f56.5deff","type":"delay","z":"59165d4d.066034","name":"delay1","pauseType":"delay","timeout":"1","timeoutUnits":"seconds","rate":"1","nbRateUnits":"1","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":false,"x":110,"y":140,"wires":[["e00b8f43.9ccf9"]]},{"id":"54aae87c.830b48","type":"i2c scan","z":"59165d4d.066034","name":"","x":320,"y":780,"wires":[["7dd33aaa.12b2a4"],[]]},{"id":"42f6e013.13b69","type":"inject","z":"59165d4d.066034","name":"push2","topic":"","payload":"Clear","payloadType":"str","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":110,"y":740,"wires":[["b1978dba.68466"]]},{"id":"7dd33aaa.12b2a4","type":"debug","z":"59165d4d.066034","name":"i2c channel devices","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","x":550,"y":780,"wires":[]},{"id":"6588eab1.c5bdb4","type":"function","z":"59165d4d.066034","name":"BuffToStr1","func":"msg.payload = (String.fromCharCode(msg.payload[1])) + (String.fromCharCode(msg.payload[2]))\n + (String.fromCharCode(msg.payload[3])) + (String.fromCharCode(msg.payload[4]))\n  + (String.fromCharCode(msg.payload[5])) + (String.fromCharCode(msg.payload[6]));\nreturn msg;","outputs":1,"noerr":0,"x":330,"y":100,"wires":[["323c7e18.bdd8e2"]]},{"id":"323c7e18.bdd8e2","type":"change","z":"59165d4d.066034","name":"StrToNum1","rules":[{"t":"set","p":"payload.value","pt":"msg","to":"","tot":"num"}],"action":"","property":"","from":"","to":"","reg":false,"x":330,"y":160,"wires":[["5b912629.f72478","1ab56f48.ad8f91"]]},{"id":"5b912629.f72478","type":"ui_gauge","z":"59165d4d.066034","name":"TankTemp","group":"ffc1aa9d.ac4028","order":1,"width":5,"height":4,"gtype":"donut","title":"Tank Temp","label":"degC","format":"{{value}}","min":0,"max":"40","colors":["#ff8040","#ff8000","#ff0000"],"seg1":"10","seg2":"30","x":650,"y":120,"wires":[]},{"id":"863f23ed.6ce2b","type":"i2c out","z":"59165d4d.066034","name":"pHCmnd","address":"100","command":"082","payload":"payload","payloadType":"msg","count":"1","x":120,"y":240,"wires":[["d76c2c51.1df71"]]},{"id":"d76c2c51.1df71","type":"delay","z":"59165d4d.066034","name":"delay2","pauseType":"delay","timeout":"1","timeoutUnits":"seconds","rate":"1","nbRateUnits":"1","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":false,"x":110,"y":280,"wires":[["4e84ff27.a688"]]},{"id":"4e84ff27.a688","type":"i2c in","z":"59165d4d.066034","name":"pHData","address":"100","command":"","count":"8","x":120,"y":320,"wires":[["5e0af3d3.7bd6bc"]]},{"id":"5e0af3d3.7bd6bc","type":"function","z":"59165d4d.066034","name":"BuffToStr2","func":"msg.payload = (String.fromCharCode(msg.payload[1])) + (String.fromCharCode(msg.payload[2]))\n + (String.fromCharCode(msg.payload[3])) + (String.fromCharCode(msg.payload[4]))\n  + (String.fromCharCode(msg.payload[5])) + (String.fromCharCode(msg.payload[6]));\nreturn msg;","outputs":1,"noerr":0,"x":330,"y":240,"wires":[["ac1f6728.22f578"]]},{"id":"ac1f6728.22f578","type":"change","z":"59165d4d.066034","name":"StrToNum2","rules":[{"t":"set","p":"payload.Value","pt":"msg","to":"","tot":"num"}],"action":"","property":"","from":"","to":"","reg":false,"x":330,"y":300,"wires":[["6436f959.83a148","adbe2f0c.98d94"]]},{"id":"6436f959.83a148","type":"ui_level","z":"59165d4d.066034","group":"ad7aabd5.7bf318","order":1,"width":5,"height":4,"name":"TankpH","label":"pH","colorHi":"#8000ff","colorWarn":"#00ff00","colorNormal":"#00ff00","colorOff":"#595959","min":"4.0","max":"10.0","segWarn":"6.0","segHigh":"8.0","unit":"","layout":"sv","channelA":"","channelB":"","decimals":"2","animations":"soft","shape":"3","colorschema":"rainbow","textoptions":"custom","colorText":"#eeeeee","fontLabel":"1.5","fontValue":"1.5","fontSmall":".75","colorFromTheme":true,"textAnimations":true,"hideValue":false,"tickmode":"segments","peakmode":false,"peaktime":3000,"x":640,"y":260,"wires":[]},{"id":"4c96ce0f.154ec","type":"i2c out","z":"59165d4d.066034","d":true,"name":"FlowCmnd","address":"104","command":"82","payload":"payload","payloadType":"msg","count":"1","x":110,"y":520,"wires":[["fb7d7fe3.15b03"]]},{"id":"fb7d7fe3.15b03","type":"delay","z":"59165d4d.066034","name":"delay4","pauseType":"delay","timeout":"1","timeoutUnits":"seconds","rate":"1","nbRateUnits":"1","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":false,"x":110,"y":560,"wires":[["89af4958.9414b8"]]},{"id":"89af4958.9414b8","type":"i2c in","z":"59165d4d.066034","d":true,"name":"FlowData","address":"104","command":"","count":"8","x":120,"y":620,"wires":[["5e8a1754.f437d8","a4f6724b.00d57"]]},{"id":"5e8a1754.f437d8","type":"function","z":"59165d4d.066034","name":"BuffToStr4a","func":"msg.payload = (String.fromCharCode(msg.payload[1])) + (String.fromCharCode(msg.payload[2]))\n + (String.fromCharCode(msg.payload[3])) + (String.fromCharCode(msg.payload[4]))\n  + (String.fromCharCode(msg.payload[5])) + (String.fromCharCode(msg.payload[6]));\nreturn msg;","outputs":1,"noerr":0,"x":330,"y":520,"wires":[["6e7c1c2d.edae24"]]},{"id":"6e7c1c2d.edae24","type":"change","z":"59165d4d.066034","name":"StrToNum4a","rules":[{"t":"set","p":"payload.Value","pt":"msg","to":"","tot":"num"}],"action":"","property":"","from":"","to":"","reg":false,"x":330,"y":580,"wires":[["117ae2b.0951e1d","8b9f9a88.59a058"]]},{"id":"117ae2b.0951e1d","type":"ui_text","z":"59165d4d.066034","group":"b03ec9c2.970168","order":3,"width":0,"height":0,"name":"l/min","label":"l/min","format":"{{msg.payload}}","layout":"row-spread","x":630,"y":580,"wires":[]},{"id":"b1978dba.68466","type":"i2c out","z":"59165d4d.066034","name":"TotClear","address":"104","command":"","payload":"payload","payloadType":"bin","count":"8","x":320,"y":740,"wires":[["418f9978.531ca8"]]},{"id":"33f61e3b.8cb402","type":"inject","z":"59165d4d.066034","name":"push3","topic":"","payload":"","payloadType":"num","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":110,"y":780,"wires":[["54aae87c.830b48"]]},{"id":"8b9f9a88.59a058","type":"ui_gauge","z":"59165d4d.066034","name":"PumpFlow","group":"b03ec9c2.970168","order":1,"width":5,"height":4,"gtype":"wave","title":"Pump Flow","label":"l/min","format":"{{value}}","min":0,"max":"30","colors":["#d7ffff","#80ffff","#00ffff"],"seg1":"10","seg2":"20","x":650,"y":520,"wires":[]},{"id":"fcfd20fc.fe16a","type":"i2c in","z":"59165d4d.066034","name":"ORPData","address":"98","command":"","count":"8","x":120,"y":460,"wires":[["8fbadc49.54a87"]]},{"id":"9214c7e3.12f458","type":"i2c out","z":"59165d4d.066034","name":"ORPCmnd","address":"98","command":"82","payload":"payload","payloadType":"msg","count":"1","x":110,"y":380,"wires":[["49ba564f.5d51a8"]]},{"id":"49ba564f.5d51a8","type":"delay","z":"59165d4d.066034","name":"delay3","pauseType":"delay","timeout":"1","timeoutUnits":"seconds","rate":"1","nbRateUnits":"1","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":false,"x":110,"y":420,"wires":[["fcfd20fc.fe16a"]]},{"id":"e272ee73.d5b14","type":"ui_text","z":"59165d4d.066034","group":"b2db3abe.0a63d8","order":3,"width":0,"height":0,"name":"mV","label":"mV","format":"{{msg.payload}}","layout":"row-spread","x":630,"y":440,"wires":[]},{"id":"8fbadc49.54a87","type":"function","z":"59165d4d.066034","name":"BuffToStr3","func":"msg.payload = (String.fromCharCode(msg.payload[1])) + (String.fromCharCode(msg.payload[2]))\n + (String.fromCharCode(msg.payload[3])) + (String.fromCharCode(msg.payload[4]))\n  + (String.fromCharCode(msg.payload[5])) + (String.fromCharCode(msg.payload[6]));\nreturn msg;","outputs":1,"noerr":0,"x":330,"y":380,"wires":[["98d203ea.9b3c2"]]},{"id":"98d203ea.9b3c2","type":"change","z":"59165d4d.066034","name":"StrToNum3","rules":[{"t":"set","p":"payload.value","pt":"msg","to":"","tot":"num"}],"action":"","property":"","from":"","to":"","reg":false,"x":330,"y":440,"wires":[["e272ee73.d5b14","7f98aed2.615d1"]]},{"id":"7f98aed2.615d1","type":"ui_gauge","z":"59165d4d.066034","name":"TankORP","group":"b2db3abe.0a63d8","order":1,"width":5,"height":4,"gtype":"gage","title":"Tank ORP","label":"mV","format":"{{value}}","min":0,"max":"500","colors":["#00ff00","#ffff00","#ff0000"],"seg1":"275","seg2":"350","x":640,"y":380,"wires":[]},{"id":"a4f6724b.00d57","type":"function","z":"59165d4d.066034","name":"BuffToStr4b","func":"msg.payload = (String.fromCharCode(msg.payload[1])) + (String.fromCharCode(msg.payload[2]))\n + (String.fromCharCode(msg.payload[3])) + (String.fromCharCode(msg.payload[4]))\n  + (String.fromCharCode(msg.payload[5])) + (String.fromCharCode(msg.payload[6]));\nreturn msg;","outputs":1,"noerr":0,"x":330,"y":640,"wires":[["7eb33ea4.484bc"]]},{"id":"7eb33ea4.484bc","type":"change","z":"59165d4d.066034","name":"StrToNum4b","rules":[{"t":"set","p":"payload.Value","pt":"msg","to":"","tot":"num"}],"action":"","property":"","from":"","to":"","reg":false,"x":330,"y":700,"wires":[["6baa2964.726738"]]},{"id":"6baa2964.726738","type":"ui_text","z":"59165d4d.066034","group":"b03ec9c2.970168","order":4,"width":0,"height":0,"name":"litres","label":"Total litres","format":"{{msg.payload}}","layout":"row-spread","x":630,"y":700,"wires":[]},{"id":"767048b8.04b1e8","type":"ui_slider","z":"59165d4d.066034","name":"TempSetpoint","label":"sp","tooltip":"Desired Temperature","group":"ffc1aa9d.ac4028","order":2,"width":5,"height":1,"passthru":false,"outs":"end","topic":"","min":"25","max":"32","step":1,"x":660,"y":180,"wires":[[]]},{"id":"832b804.3822b8","type":"ui_slider","z":"59165d4d.066034","name":"pHSetpoint","label":"sp","tooltip":"Desired pH","group":"ad7aabd5.7bf318","order":2,"width":5,"height":1,"passthru":false,"outs":"end","topic":"","min":"5.5","max":"9.5","step":".1","x":650,"y":320,"wires":[[]]},{"id":"1ab56f48.ad8f91","type":"ramp-thermostat","z":"59165d4d.066034","name":"TempCtrl","profile":"f15ee116.77d49","hysteresisplus":".25","hysteresisminus":".25","x":500,"y":160,"wires":[["a7be1c66.ddc8f"],[],["767048b8.04b1e8"]]},{"id":"d4c352ba.e030e","type":"ui_led","z":"59165d4d.066034","group":"ffc1aa9d.ac4028","order":3,"width":5,"height":2,"label":"Heater","labelPlacement":"left","labelAlignment":"left","colorForValue":[{"color":"red","value":"0","valueType":"num"},{"color":"green","value":"1","valueType":"num"}],"allowColorForValueInMessage":false,"name":"Heater","x":1230,"y":100,"wires":[]},{"id":"adbe2f0c.98d94","type":"ramp-thermostat","z":"59165d4d.066034","name":"pHCtrl","profile":"965190ba.83e9b","hysteresisplus":".1","hysteresisminus":".1","x":490,"y":300,"wires":[["a0455e.c7afeaa"],[],["832b804.3822b8"]]},{"id":"8fd0d458.8ba688","type":"ui_led","z":"59165d4d.066034","group":"ad7aabd5.7bf318","order":3,"width":5,"height":2,"label":"CO2","labelPlacement":"left","labelAlignment":"left","colorForValue":[{"color":"red","value":"0","valueType":"num"},{"color":"green","value":"1","valueType":"num"}],"allowColorForValueInMessage":false,"name":"CO2","x":1230,"y":140,"wires":[]},{"id":"35e019aa.b80d66","type":"ncd-mcp23008","z":"59165d4d.066034","name":"NCD-GPRelayCtrlr","connection":"aa1e3ead.646b2","addr":"32","interval":"100","onchange":true,"send_init":false,"outputs":9,"output_all":true,"interrupt":0,"io_1":1,"io_2":1,"io_3":1,"io_4":1,"io_5":1,"io_6":1,"io_7":1,"io_8":1,"persist":"","startup":"","x":1050,"y":200,"wires":[["d4c352ba.e030e"],["8fd0d458.8ba688"],[],[],[],[],[],[],[]]},{"id":"a7be1c66.ddc8f","type":"change","z":"59165d4d.066034","name":"RelayCh1Set","rules":[{"t":"set","p":"topic","pt":"msg","to":"channel_1","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":830,"y":140,"wires":[["35e019aa.b80d66"]]},{"id":"cd643120.10061","type":"change","z":"59165d4d.066034","name":"RelayCh2Set","rules":[{"t":"set","p":"topic","pt":"msg","to":"channel_2","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":830,"y":280,"wires":[["35e019aa.b80d66"]]},{"id":"a0455e.c7afeaa","type":"function","z":"59165d4d.066034","name":"ChgDirectAct","func":"if (msg.payload === false)\n{msg.payload=true}\nelse \n{msg.payload=false}\nreturn msg;","outputs":1,"noerr":0,"x":830,"y":240,"wires":[["cd643120.10061"]]},{"id":"4a410a71.d5f3f4","type":"comment","z":"59165d4d.066034","name":"Flow Temporarily Disabled","info":"Flow is temporarily disabled as there are not provisions in the current test tank.\nFurthermore, the Atlas-Sci non-iso carrier board needs to be re-worked (bad solder)","x":830,"y":580,"wires":[]},{"id":"ffc1aa9d.ac4028","type":"ui_group","z":"","name":"EZO RTD over i2c","tab":"ba830589.55bb38","order":1,"disp":false,"width":"5","collapse":false},{"id":"ad7aabd5.7bf318","type":"ui_group","z":"","name":"EZO pH over I2C","tab":"ba830589.55bb38","order":2,"disp":false,"width":"5","collapse":false},{"id":"b03ec9c2.970168","type":"ui_group","z":"","name":"EZO Flo over I2C","tab":"ba830589.55bb38","order":4,"disp":false,"width":"5","collapse":false},{"id":"b2db3abe.0a63d8","type":"ui_group","z":"","name":"EZO ORP over I2C","tab":"ba830589.55bb38","order":3,"disp":false,"width":"5","collapse":false},{"id":"f15ee116.77d49","type":"profile","z":"","name":"SchedTemp","time1":"00:00","temp1":"27","time2":"23:59","temp2":"27","time3":"","temp3":"","time4":"","temp4":"","time5":"","temp5":"","time6":"","temp6":"","time7":"","temp7":"","time8":"","temp8":"","time9":"","temp9":"","time10":"","temp10":""},{"id":"965190ba.83e9b","type":"profile","z":"","name":"SchedpH","time1":"00:00","temp1":"7.0","time2":"23:59","temp2":"7.0","time3":"","temp3":"","time4":"","temp4":"","time5":"","temp5":"","time6":"","temp6":"","time7":"","temp7":"","time8":"","temp8":"","time9":"","temp9":"","time10":"","temp10":""},{"id":"aa1e3ead.646b2","type":"ncd-comm","z":"","name":"Pi-i2c-1-Bus","bus":"i2c-1","commType":"standard","addr":"0","useMux":false,"muxAddr":"112","muxPort":"0"},{"id":"ba830589.55bb38","type":"ui_tab","z":"","name":"Sensor Display","icon":"dashboard","disabled":false,"hidden":false}]

I have a long way to go:
1 My 'parsing' of the data I am receiving from the RTD, pH and ORP probes, and Flow Meter over i2c is a kludge.
2 I'm struggling with using the node-red-contrib-i2c nodes for complex commands. (necessary for devices that return multiple data... or for performing calibrations of sensors
3 My familiarity with json and java-script are minimal...only what I needed to hack on a website to make something look nice... so, I've got to bone up on those a bit to continue on with the project.
4 When CoVID-19 lock down is over, I'll get over to Xerocraft Hacker Space to take a laser cutter to the controller's enclosure (very old picture.. when it was an Arduino DUE project w/ UART connected devices):

My aquarium controller is geared towards planted aquariums. I'm using this controller on a 4 aquarium system that shares resources/equipment.

However, there is a hobbyist who is building an aquarium controller as well using Atlas-Sci i2c enabled circuits, raspberry pi and node-RED.. So we are comparing notes. I'll point him to this thread and maybe he will chime in on occasion. His stuff is impressive because he does 3d modelling of everything and 3d prints things.
MaddyP's Node-RED powered Reef Tank Controller

I hope you will contribute your experience to help with problems that will arise.

3 Likes

The 'Pump Flow' portion of the Flow and Dashboard UI are only temporarily disabled because I don't have a means to connect them up to the 'experimental tank'. So, I just turned those parts off.... They work fine, sans the issue with properly parsing the data to separate instantaneous flow (gpm) and total flow (accumulated).

There is a 2nd flow that is used just to monitor and control the raspberryPI itself...

I'm including that just so folks can see it:

[{"id":"9295cc86.8ff8e","type":"tab","label":"Rpi4 SysCtrl","disabled":false,"info":""},{"id":"e129f76e.697b98","type":"ui_gauge","z":"9295cc86.8ff8e","name":"","group":"f6803ec3.11274","order":2,"width":4,"height":3,"gtype":"gage","title":"CPU Temp","label":"degC","format":"{{value}}","min":0,"max":"100","colors":["#00b500","#e6e600","#ca3838"],"seg1":"35","seg2":"60","x":650,"y":40,"wires":[]},{"id":"6b36da73.96b884","type":"exec","z":"9295cc86.8ff8e","command":"vcgencmd measure_temp","addpay":false,"append":"","useSpawn":"","timer":"","name":"CPU Temp.","x":310,"y":40,"wires":[["d2225e56.2e36e"],[],[]]},{"id":"9214d430.856398","type":"inject","z":"9295cc86.8ff8e","name":"","topic":"","payload":"","payloadType":"date","repeat":"10","crontab":"","once":true,"onceDelay":"","x":110,"y":40,"wires":[["6b36da73.96b884","f852b950.ad2178","7fa8e7c0.6866b8"]]},{"id":"d2225e56.2e36e","type":"function","z":"9295cc86.8ff8e","name":"","func":"str = msg.payload\nmsg.payload = str.substring(5,9);\nreturn msg;","outputs":1,"noerr":0,"x":490,"y":40,"wires":[["e129f76e.697b98","580e092d.d4d528"]]},{"id":"f4a62369.2c6c2","type":"ui_button","z":"9295cc86.8ff8e","name":"","group":"939dc22f.a707","order":2,"width":4,"height":2,"passthru":false,"label":"Reboot","tooltip":"","color":"","bgcolor":"","icon":"","payload":"","payloadType":"str","topic":"","x":100,"y":320,"wires":[["f7d55fa2.aa8e6"]]},{"id":"f7d55fa2.aa8e6","type":"exec","z":"9295cc86.8ff8e","command":"sudo reboot","addpay":false,"append":"","useSpawn":"","timer":"","name":"Reboot","x":320,"y":320,"wires":[[],[],[]]},{"id":"c6963d8c.03eb4","type":"ui_button","z":"9295cc86.8ff8e","name":"","group":"939dc22f.a707","order":3,"width":4,"height":2,"passthru":false,"label":"Shutdown","tooltip":"","color":"","bgcolor":"red","icon":"","payload":"","payloadType":"str","topic":"","x":100,"y":380,"wires":[["80e2273.92a36d8"]]},{"id":"80e2273.92a36d8","type":"exec","z":"9295cc86.8ff8e","command":"sudo shutdown -h now","addpay":false,"append":"","useSpawn":"","timer":"","name":"Shutdown","x":320,"y":380,"wires":[[],[],[]]},{"id":"580e092d.d4d528","type":"ui_chart","z":"9295cc86.8ff8e","name":"","group":"f1072094.d80a5","order":5,"width":6,"height":4,"label":"CPU Temp Hx","chartType":"line","legend":"false","xformat":"HH:mm:ss","interpolate":"linear","nodata":"","dot":false,"ymin":"","ymax":"","removeOlder":"8","removeOlderPoints":"","removeOlderUnit":"3600","cutout":0,"useOneColor":false,"useUTC":false,"colors":["#1f77b4","#aec7e8","#ff7f0e","#2ca02c","#98df8a","#d62728","#ff9896","#9467bd","#c5b0d5"],"useOldStyle":true,"outputs":1,"x":660,"y":80,"wires":[[]]},{"id":"f852b950.ad2178","type":"exec","z":"9295cc86.8ff8e","command":"top -d 0.5 -b -n2 | grep \"Cpu(s)\"|tail -n 1 | awk '{print $2 + $4}'","addpay":false,"append":"","useSpawn":"","timer":"","name":"CPU Load","x":310,"y":120,"wires":[["2a62ff5c.0761a"],[],[]]},{"id":"7fa8e7c0.6866b8","type":"exec","z":"9295cc86.8ff8e","command":"free | grep Mem | awk '{print 100*($4+$6+$7)/$2}'","addpay":false,"append":"","useSpawn":"","timer":"","name":"Free Memory","x":310,"y":180,"wires":[["c958ab3.b2dbe58"],[],[]]},{"id":"2a62ff5c.0761a","type":"ui_gauge","z":"9295cc86.8ff8e","name":"","group":"f6803ec3.11274","order":1,"width":4,"height":3,"gtype":"gage","title":"CPU Load","label":"CPU","format":"{{value}}","min":0,"max":"100","colors":["#00b500","#e6e600","#ca3838"],"seg1":"","seg2":"","x":650,"y":120,"wires":[]},{"id":"c958ab3.b2dbe58","type":"ui_gauge","z":"9295cc86.8ff8e","name":"","group":"f6803ec3.11274","order":3,"width":4,"height":3,"gtype":"gage","title":"Free Mem","label":"%","format":"{{value}}","min":0,"max":"100","colors":["#ff0000","#e6e600","#00ff00"],"seg1":"","seg2":"","x":650,"y":180,"wires":[]},{"id":"91fca96e.85a348","type":"exec","z":"9295cc86.8ff8e","command":"df -h","addpay":false,"append":"","useSpawn":"","timer":"","name":"Disk Usage","x":310,"y":260,"wires":[["19442615.9771aa"],[],[]]},{"id":"82e0dc3f.9082","type":"ui_gauge","z":"9295cc86.8ff8e","name":"","group":"f6803ec3.11274","order":4,"width":4,"height":3,"gtype":"gage","title":"Disk Space","label":"%","format":"{{value}}","min":0,"max":"100","colors":["#00b500","#e6e600","#ca3838"],"seg1":"","seg2":"","x":650,"y":260,"wires":[]},{"id":"19442615.9771aa","type":"function","z":"9295cc86.8ff8e","name":"","func":"var re = /([0-9]{2})%/\nvar idx = msg.payload.search(re);\nvar str = msg.payload;\nif (idx >=0) {\n    str = msg.payload.substring(idx, idx + 2);\n}\nmsg.payload = str;\nreturn msg;","outputs":1,"noerr":0,"x":490,"y":260,"wires":[["82e0dc3f.9082"]]},{"id":"3c3f274f.fae238","type":"inject","z":"9295cc86.8ff8e","name":"","topic":"","payload":"","payloadType":"date","repeat":"60","crontab":"","once":true,"x":122.88884353637695,"y":258.2222442626953,"wires":[["91fca96e.85a348"]]},{"id":"a8eb871b.783e58","type":"ui_text","z":"9295cc86.8ff8e","group":"939dc22f.a707","order":1,"width":4,"height":2,"name":"","label":"IP Address","format":"{{value.substring(0, value.indexOf(\" \"))}}","layout":"col-center","x":650,"y":440,"wires":[]},{"id":"45291ba4.b6ba44","type":"inject","z":"9295cc86.8ff8e","name":"","topic":"","payload":"","payloadType":"date","repeat":"10","crontab":"","once":true,"onceDelay":"","x":110,"y":440,"wires":[["cdfbfe40.a42c1"]]},{"id":"cdfbfe40.a42c1","type":"exec","z":"9295cc86.8ff8e","command":"hostname","addpay":false,"append":"-I","useSpawn":"","timer":"","name":"Get Local IP","x":310,"y":440,"wires":[["5b6195a8.0b069c","a8eb871b.783e58"],[],[]]},{"id":"5b6195a8.0b069c","type":"mqtt out","z":"9295cc86.8ff8e","d":true,"name":"Send IP thru MQTT","topic":"raspberrypi4_ip_gama","qos":"0","retain":"true","broker":"e5937d40.2e4a1","x":670,"y":480,"wires":[]},{"id":"ed75527e.c2c65","type":"ui_template","z":"9295cc86.8ff8e","group":"ffc1aa9d.ac4028","name":"Virtual Keyboard","order":4,"width":1,"height":1,"format":"<script> \n    \n// the semi-colon before function invocation is a safety net against concatenated\n// scripts and/or other plugins which may not be closed properly.\n; (function ($, window, document, undefined) {\n\n    // undefined is used here as the undefined global variable in ECMAScript 3 is\n    // mutable (ie. it can be changed by someone else). undefined isn't really being\n    // passed in so we can ensure the value of it is truly undefined. In ES5, undefined\n    // can no longer be modified.\n\n    // window and document are passed through as local variable rather than global\n    // as this (slightly) quickens the resolution process and can be more efficiently\n    // minified (especially when both are regularly referenced in your plugin).\n\n    // Create the defaults once\n    var pluginName = \"jkeyboard\",\n        defaults = {\n            layout: \"english\",\n            input: $('#input'),\n            customLayouts: {\n                selectable: []\n            },\n        };\n\n\n    var function_keys = {\n        backspace: {\n            text: 'DEL',\n        },\n        return: {\n            text: 'Enter'\n        },\n        shift: {\n            text: 'Shift'\n        },\n        space: {\n            text: 'Space'\n        },\n        numeric_switch: {\n            text: '123',\n            command: function () {\n                this.createKeyboard('numeric');\n                this.events();\n            }\n        },\n        layout_switch: {\n            text: '<i class=\"fa fa-keyboard-o\" aria-hidden=\"true\"></i>',\n            command: function () {\n                var l = this.toggleLayout();\n                this.createKeyboard(l);\n                this.events();\n            }\n        },\n        character_switch: {\n            text: 'ABC',\n            command: function () {\n                this.createKeyboard(layout);\n                this.events();\n            }\n        },\n        symbol_switch: {\n            text: '#+=',\n            command: function () {\n                this.createKeyboard('symbolic');\n                this.events();\n            }\n        }\n    };\n\n\n    var layouts = {\n        selectable: ['azeri', 'english', 'russian','french', 'emoji'],\n        azeri: [\n            ['q', 'ü', 'e', 'r', 't', 'y', 'u', 'i', 'o', 'p', 'ö', 'ğ'],\n            ['a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l', 'ı', 'ə'],\n            ['shift', 'z', 'x', 'c', 'v', 'b', 'n', 'm', 'ç', 'ş', 'backspace'],\n            ['numeric_switch', 'layout_switch', 'space', 'return']\n        ],\n        english: [\n            ['q', 'w', 'e', 'r', 't', 'y', 'u', 'i', 'o', 'p',],\n            ['a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l',],\n            ['shift', 'z', 'x', 'c', 'v', 'b', 'n', 'm', 'backspace'],\n            ['numeric_switch', 'layout_switch', 'space', 'return']\n        ],\n        russian: [\n            ['й', 'ц', 'у', 'к', 'е', 'н', 'г', 'ш', 'щ', 'з', 'х'],\n            ['ф', 'ы', 'в', 'а', 'п', 'р', 'о', 'л', 'д', 'ж', 'э'],\n            ['shift', 'я', 'ч', 'с', 'м', 'и', 'т', 'ь', 'б', 'ю', 'backspace'],\n            ['numeric_switch', 'layout_switch', 'space', 'return']\n        ],\n        french: [\n            ['q', 'w', 'e', 'r', 't', 'y', 'u', 'i', 'o', 'p',],\n            ['a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l','à','ç'],\n            ['shift', 'z', 'x', 'c', 'v', 'b', 'n', 'm','é','è', 'backspace'],\n            ['numeric_switch', 'layout_switch', 'space', 'return']\n        ],\n        emoji: [\n            ['😀', '😁', '😂', '🤣', '😃', '😄', '😅', '😆', '😉', '😊',],\n            ['😋', '😎', '😍', '😘', 'g', 'h', 'j', 'k', 'l','à','ç'],\n            ['shift', 'z', 'x', 'c', 'v', 'b', 'n', 'm','é','è', 'backspace'],\n            ['numeric_switch', 'layout_switch', 'space', 'return']\n        ],            \n        numeric: [\n            ['1', '2', '3', '4', '5', '6', '7', '8', '9', '0'],\n            ['-', '/', ':', ';', '(', ')', '$', '&', '@', '\"'],\n            ['symbol_switch', '.', ',', '?', '!', \"'\", 'backspace'],\n            ['character_switch', 'layout_switch', 'space', 'return'],\n        ],\n        numbers_only: [\n            ['1', '2', '3',],\n            ['4', '5', '6',],\n            ['7', '8', '9',],\n            ['0', 'return', 'backspace'],\n        ],\n        symbolic: [\n            ['[', ']', '{', '}', '#', '%', '^', '*', '+', '='],\n            ['_', '\\\\', '|', '~', '<', '>'],\n            ['numeric_switch', '.', ',', '?', '!', \"'\", 'backspace'],\n            ['character_switch', 'layout_switch', 'space', 'return'],\n\n        ]\n    }\n\n    var shift = false, capslock = false, layout = 'english', layout_id = 0;\n\n    // The actual plugin constructor\n    function Plugin(element, options) {\n        this.element = element;\n        // jQuery has an extend method which merges the contents of two or\n        // more objects, storing the result in the first object. The first object\n        // is generally empty as we don't want to alter the default options for\n        // future instances of the plugin\n        this.settings = $.extend({}, defaults, options);\n        // Extend & Merge the cusom layouts\n        layouts = $.extend(true, {}, this.settings.customLayouts, layouts);\n        if (Array.isArray(this.settings.customLayouts.selectable)) {\n            $.merge(layouts.selectable, this.settings.customLayouts.selectable);\n        }\n        this._defaults = defaults;\n        this._name = pluginName;\n        this.init();\n    }\n\n    Plugin.prototype = {\n        init: function () {\n            layout = this.settings.layout;\n            this.createKeyboard(layout);\n            this.events();\n        },\n\n        setInput: function (newInputField) {\n            this.settings.input = newInputField;\n        },\n\n        createKeyboard: function (layout) {\n            shift = false;\n            capslock = false;\n\n            var keyboard_container = $('<ul/>').addClass('jkeyboard'),\n                me = this;\n\n            layouts[layout].forEach(function (line, index) {\n                var line_container = $('<li/>').addClass('jline');\n                line_container.append(me.createLine(line));\n                keyboard_container.append(line_container);\n            });\n\n            $(this.element).html('').append(keyboard_container);\n        },\n\n        createLine: function (line) {\n            var line_container = $('<ul/>');\n\n            line.forEach(function (key, index) {\n                var key_container = $('<li/>').addClass('jkey').data('command', key);\n\n                if (function_keys[key]) {\n                    key_container.addClass(key).html(function_keys[key].text);\n                }\n                else {\n                    key_container.addClass('letter').html(key);\n                }\n\n                line_container.append(key_container);\n            })\n\n            return line_container;\n        },\n\n        events: function () {\n            var letters = $(this.element).find('.letter'),\n                shift_key = $(this.element).find('.shift'),\n                space_key = $(this.element).find('.space'),\n                backspace_key = $(this.element).find('.backspace'),\n                return_key = $(this.element).find('.return'),\n\n                me = this,\n                fkeys = Object.keys(function_keys).map(function (k) {\n                    return '.' + k;\n                }).join(',');\n\n            letters.on('click', function () {\n                me.type((shift || capslock) ? $(this).text().toUpperCase() : $(this).text());\n            });\n\n            space_key.on('click', function () {\n                me.type(' ');\n            });\n\n            return_key.on('click', function () {\n                me.type(\"\\n\");\n                me.settings.input.parents('form').submit();\n            });\n\n            backspace_key.on('click', function () {\n                me.backspace();\n            });\n\n            shift_key.on('click', function () {\n                if (capslock) {\n                    me.toggleShiftOff();\n                    capslock = false;\n                } else {\n                    me.toggleShiftOn();\n                }\n            }).on('dblclick', function () {\n                capslock = true;\n            });\n\n\n            $(fkeys).on('click', function () {\n                var command = function_keys[$(this).data('command')].command;\n                if (!command) return;\n\n                command.call(me);\n            });\n        },\n\n        type: function (key) {\n            var input = this.settings.input,\n                val = input.val(),\n                input_node = input.get(0),\n                start = input_node.selectionStart,\n                end = input_node.selectionEnd;\n\n            var max_length = $(input).attr(\"maxlength\");\n            if (start == end && end == val.length) {\n                if (!max_length || val.length < max_length) {\n                    input.val(val + key);\n                    input.change()\n                }\n            } else {\n                //console.log(input_node.type)\n                if (input_node.type != \"number\"){\n                    var new_string = this.insertToString(start, end, val, key);\n                    input.val(new_string);\n                    start++;\n                    end = start;\n                    input_node.setSelectionRange(start, end);\n                    input.change()\n                }else{\n                    console.log(\"Not supposed to go there as number types are changed to text type and back\")\n                    input.val(key + val);\n                    input.change()\n                }\n                \n            }\n\n            input.trigger('focus');\n\n            if (shift && !capslock) {\n                this.toggleShiftOff();\n            }\n        },\n\n        backspace: function () {\n            var input = this.settings.input,\n                val = input.val();\n                input_node = input.get(0),\n                start = input_node.selectionStart,\n                end = input_node.selectionEnd;\n            \n            input.val(val.slice(0, start-1) + val.slice(start))\n            input.change()\n            input.focus()\n            input_node.setSelectionRange(start-1, start-1);\n        },\n\n        toggleShiftOn: function () {\n            var letters = $(this.element).find('.letter'),\n                shift_key = $(this.element).find('.shift');\n\n            letters.addClass('uppercase');\n            shift_key.addClass('active')\n            shift = true;\n        },\n\n        toggleShiftOff: function () {\n            var letters = $(this.element).find('.letter'),\n                shift_key = $(this.element).find('.shift');\n\n            letters.removeClass('uppercase');\n            shift_key.removeClass('active');\n            shift = false;\n        },\n\n        toggleLayout: function () {\n            layout_id = layout_id || 0;\n            var plain_layouts = layouts.selectable;\n            layout_id++;\n\n            var current_id = layout_id % plain_layouts.length;\n            var SelectedLayoutName = plain_layouts[current_id];\n            $('#vkeyname').text('V-Keyboard ' + SelectedLayoutName )\n            return plain_layouts[current_id];\n        },\n\n        insertToString: function (start, end, string, insert_string) {\n            return string.substring(0, start) + insert_string + string.substring(end, string.length);\n        }\n    };\n\n        /*\n\t\t// A really lightweight plugin wrapper around the constructor,\n\t\t// preventing against multiple instantiations\n\t\t$.fn[ pluginName ] = function ( options ) {\n\t\t\t\treturn this.each(function() {\n\t\t\t\t\t\tif ( !$.data( this, \"plugin_\" + pluginName ) ) {\n\t\t\t\t\t\t\t\t$.data( this, \"plugin_\" + pluginName, new Plugin( this, options ) );\n\t\t\t\t\t\t}\n\t\t\t\t});\n\t\t};\n        */\n        var methods = {\n            init: function(options) {\n                if (!this.data(\"plugin_\" + pluginName)) {\n                    this.data(\"plugin_\" + pluginName, new Plugin(this, options));\n                }\n            },\n\t\t\tsetInput: function(content) {\n\t\t\t\tthis.data(\"plugin_\" + pluginName).setInput($(content));\n            },\n            setLayout: function(layoutname) {\n                // change layout if it is not match current\n                object = this.data(\"plugin_\" + pluginName);\n                if (typeof(layouts[layoutname]) !== 'undefined' && object.settings.layout != layoutname) {\n                    object.settings.layout = layoutname;\n                    object.createKeyboard(layoutname);\n                    object.events();\n                };\n            },\n        };\n\n\t\t$.fn[pluginName] = function (methodOrOptions) {\n            if (methods[methodOrOptions]) {\n                return methods[methodOrOptions].apply(this.first(), Array.prototype.slice.call( arguments, 1));\n            } else if (typeof methodOrOptions === 'object' || ! methodOrOptions) {\n                // Default to \"init\"\n                return methods.init.apply(this.first(), arguments);\n            } else {\n                $.error('Method ' +  methodOrOptions + ' does not exist on jQuery.tooltip');\n            }\n        };\n\n})(jQuery, window, document);\n</script>\n<style>\n    .jkeyboard {\n  display: inline-block;\n}\n.jkeyboard, .jkeyboard .jline, .jkeyboard .jline ul {\n  display: block;\n  margin: 0;\n  padding: 0;\n}\n.jkeyboard .jline {\n  text-align: center;\n  margin-left: -14px;\n}\n.jkeyboard .jline ul li {\n  font-family: arial, sans-serif;\n  font-size: 20px;\n  display: inline-block;\n  border: 1px solid #468db3;\n  -webkit-box-shadow: 0 0 3px #468db3;\n  -webkit-box-shadow: inset 0 0 3px #468db3;\n  margin: 5px 0 1px 6px;\n  color: #000000;\n  border-radius: 5px;\n  width: 52px;\n  height: 52px;\n  box-sizing: border-box;\n  text-align: center;\n  line-height: 52px;\n  overflow: hidden;\n  cursor: pointer;\n  -webkit-touch-callout: none;\n  -webkit-user-select: none;\n  -khtml-user-select: none;\n  -moz-user-select: -moz-none;\n  -ms-user-select: none;\n  user-select: none;\n}\n.jkeyboard .jline ul li.uppercase {\n  text-transform: uppercase;\n}\n.jkeyboard .jline ul li:hover, .jkeyboard .jline ul li:active {\n  background-color: #185a82;\n}\n.jkeyboard .jline .return {\n  width: 80px;\n}\n.jkeyboard .jline .space {\n  width: 366px;\n}\n.jkeyboard .jline .numeric_switch {\n  width: 65px;\n}\n.jkeyboard .jline .layout_switch {\n}\n.jkeyboard .jline .shift {\n  width: 60px;\n}\n.jkeyboard .jline .backspace {\n  width: 69px;\n}\n</style>\n\n\n\n\n<style>\nbody {font-family: Arial, Helvetica, sans-serif;}\n\n.nr-dashboard-theme .nr-dashboard-template .md-button:not(:first-of-type) {\n    margin-top: 0px;\n}\n\n/* The Modal (background) */\n.modal {\n    display: none; /* Hidden by default */\n    position: fixed; /* Stay in place */\n    opacity:0.99;\n    z-index: 100; /* Sit on top */\n    left: 0;\n    top: 0;\n    width: 100%; /* Full width */\n    height: 100%; /* Full height */\n    overflow: auto; /* Enable scroll if needed */\n    background-color: rgb(0,0,0); /* Fallback color */\n    background-color: rgba(0,0,0,0.4); /* Black w/ opacity */\n}\n\n/* Modal Content */\n.modal-content {\n    position: fixed;\n    background-color: #fefefe;\n    margin: auto;\n    padding: 0;\n    top: 50%;\n    left: 50%;\n    transform: translate(-50%, -50%);\n    border: 1px solid #888;\n    width: 720px;\n    max-width: 100%;\n    max-height: 100%;\n    box-shadow: 0 4px 8px 0 rgba(0,0,0,0.2),0 6px 20px 0 rgba(0,0,0,0.19);\n    -webkit-animation-name: animate;\n    -webkit-animation-duration: 0.4s;\n    animation-name: animate;\n    animation-duration: 0.4s\n}\n\n/* Add Animation */\n@-webkit-keyframes animate {\n    from {top:100%; opacity:0} \n    to {top:50%; opacity:1}\n}\n\n@keyframes animate {\n    from {top:100%; opacity:0}\n    to {top:50%; opacity:1}\n}\n\n/* The Close Button */\n.close {\n    color: black;\n    float: right;\n    font-size: 28px;\n    font-weight: bold;\n}\n\n.close:hover,\n.close:focus {\n    color: #000;\n    text-decoration: none;\n    cursor: pointer;\n}\n\n.modal-header {\n    padding: 2px 16px;\n    background-color: aliceblue;\n    color: white;\n}\n\n.modal-body {padding: 2px 16px;}\n\n.modal-footer {\n    padding: 2px 16px;\n    background-color: #5cb85c;\n    color: white;\n}\n</style>\n\n<!-- The Modal -->\n<div id=\"myModal\" class=\"modal\">\n\n  <!-- Modal content -->\n  <div class=\"modal-content\">\n      <div class=\"modal-header\">\n      <span class=\"close\" onclick=\"closeModal()\">&times;</span>\n      <h2 id=\"vkeyname\" style=\"background-color: aliceblue !important; color: black !important; text-align: center;\">V-Keyboard</h2>\n    </div>\n    <div class=\"modal-body\">\n        <div id=\"keyboard\"></div>\n        <div>\n        </div>\n    </div>\n  </div>\n</div>\n\n\n<script>\n    // Get the modal\nvar modal = document.getElementById('myModal');\n\n/*\n$('input[type=text]').click(function () {\n    $('#keyboard').unbind().removeData();\n        $('#keyboard').jkeyboard({\n            layout: \"english\",\n            input: $('#'+$(this).attr('id'))\n    });\n});\n\n$('input[type=number]').click(function () {\n    $('#keyboard').unbind().removeData();\n        $('#keyboard').jkeyboard({\n            layout: \"numbers_only\",\n            input: $('#'+$(this).attr('id'))\n    });\n});\n*/\n\nvar inputTags;\nvar inputType;\n\nvar getinputs = function() {\n    inputTags = document.getElementsByTagName(\"input\");\n    for (var i = 0; i < inputTags.length; i++) {\n        inputTags[i].addEventListener('click', openModal, false)\n    }\n}\n\nsetTimeout(function(){ getinputs(); }, 1000);\n\nvar inputTarget;\n\nvar openModal = function() {\n    inputType = event.target.type\n    inputTarget = event.target\n    var layoutName;\n    if (inputType == \"number\"){\n        //console.log(event.target)\n        inputTarget.type = \"text\" //hack because chrome doesn't allow setselection in number inputs\n        layoutName = \"numbers_only\"\n    }else{\n        layoutName = \"english\"\n    }\n    $('#keyboard').unbind().removeData();\n    modal.style.display = \"block\";\n    $('#keyboard').jkeyboard({\n        layout: layoutName,\n        input: $('#'+$(this).attr('id'))\n    });\n}\n\n\n// Get the <span> element that closes the modal\nvar span = document.getElementsByClassName(\"close\")[0];\n\n// When the user clicks on <span> (x), close the modal\n//span.onclick = function(event) {\n  //closeModal()\n//}\n\n// When the user clicks anywhere outside of the modal, close it\nwindow.onclick = function(event) {\n    var source = event.target;\n    if (source == modal || source == span) {\n        closeModal(source)\n    }\n};\n\nvar closeModal = function(source){\n    //console.log(\"closing\")\n    modal.style.display = \"none\";\n   \n    if (inputType == \"number\"){\n        inputTarget.type = \"number\" //hack because chrome doesn't allow selectionstart on number inputs\n    }\n}\n\n</script>\n<script>\n\nvar clickState = 1;\nvar btn = document.querySelector('.VK');\n\nbtn.addEventListener('click', function(){\n\n  if (clickState == 0) {\n    this.textContent = 'KB On';\n    modal = document.getElementById('myModal');\n    clickState = 1;\n  } else {\n    this.textContent = 'KB Off';\n    modal = document.getElementById('empty');\n    clickState = 0;\n  }\n\n});\n</script>\n\n<style>\n.VK{\n    position: fixed;\n    top: 10px;\n    right: 10px;\n    height: 20px;\n}\n</style>\n\n<div id=\"empty\"></div>\n<button class=\"VK\">KB On</button> \n\n\n","storeOutMessages":false,"fwdInMessages":false,"templateScope":"global","x":120,"y":560,"wires":[[]]},{"id":"f6803ec3.11274","type":"ui_group","z":"","name":"System","tab":"b39ff3ca.78b4c","order":1,"disp":true,"width":"8","collapse":false},{"id":"939dc22f.a707","type":"ui_group","z":"","name":"Command","tab":"b39ff3ca.78b4c","order":2,"disp":true,"width":"4","collapse":false},{"id":"f1072094.d80a5","type":"ui_group","z":"","name":"Trends","tab":"b39ff3ca.78b4c","order":3,"disp":true,"width":"6","collapse":false},{"id":"e5937d40.2e4a1","type":"mqtt-broker","z":"","name":"","broker":"test.mosquitto.org","port":"1883","clientid":"","usetls":false,"compatmode":true,"keepalive":"60","cleansession":true,"birthTopic":"raspberry_do_gama_alive","birthQos":"0","birthRetain":"true","birthPayload":"Raspberry ativo","closeTopic":"","closePayload":"","willTopic":"","willQos":"0","willPayload":""},{"id":"ffc1aa9d.ac4028","type":"ui_group","z":"","name":"EZO RTD over i2c","tab":"ba830589.55bb38","order":1,"disp":false,"width":"5","collapse":false},{"id":"b39ff3ca.78b4c","type":"ui_tab","z":"9295cc86.8ff8e","name":"Rpi4 SysCtrl","icon":"dashboard","order":1,"disabled":false,"hidden":false},{"id":"ba830589.55bb38","type":"ui_tab","z":"","name":"Sensor Display","icon":"dashboard","disabled":false,"hidden":false}]
1 Like

Hi @SonoraTechnical, would you be willing to provide the following...

  • Parts listing
  • Circuit diagrams
  • Details (in non technical terms) of the control system. E.g
    what and why each component is chosen, it's function in the grand scheme. For example, why a PH sensor?
  • Any future plans (like measuring nitrates or ammonia, or administering additives)

The reason I ask is, a friend of mine is keen to get into automation. He's good with fish, I'm good with programming & reasonably ok with hardware & so the additional info would give us (and anyone else following) a head start

I will follow your progress. Thanks. :+1:

1 Like

Steve,
I'll get to all of that. Presently, just trying to muddle through so much of the difficulty of getting the Atlas-Scientific Circuits to accept complex commands over i2c and for Node-RED to recieve complex buffer data from those same circuits over i2c (a.k.a. stop my kludges).

Complex command issue appears to be solved. I'm attaching a 'quick-n-dirty' flow I created for sending a temperature compensation value to the pH circuit so that the pH readings are...well compensated for temperature... Then I verify that the pH circuit has successfully received/accepted that compensation value.

What I learned is the Node-RED-contrib-i2c i2c-out node only accepts a single character command in ascii format. However, it will append additonal parts of the command that you place in the payload... You must construct the Payload as a 'buffer' type.

Let's look at the Atlas-Sci pH temp compensation example:

Basically, it wants to see the ASCII equivalent of 'T,19.5' come down the wire after the i2c-out node has issue a Write....

So, Set it all up in a change node... and then push it to the i2c-out node...

See, I set the command to be a number: 084 ('T')
then I set the payload to be a Buffer of: [44,49,57,46,53,48] (',19.50')
It all gets delivered as 'T,19.50', but in ASCII Decimal representation of each character (minus the quotes... I just put them there for clarification as a string representation).
If you look at the flow, you will see that to verify... I just do another change node to feed ',?' as the payload to a 2nd i2c-out, which is asking it to return the presently stored compensation value. Of course you then have to use an i2c-in node to see the returned value..

[{"id":"fec2b683.6cc178","type":"tab","label":"PHTempCompAndVerify","disabled":false,"info":""},{"id":"baf25b1.c1636a8","type":"i2c out","z":"fec2b683.6cc178","name":"pHTempCompCmnd","address":"","command":"","payload":"payload","payloadType":"msg","count":"8","x":540,"y":40,"wires":[["905011ff.832e3","f4b8d3bf.d2e9f"]]},{"id":"302a678b.5ef1a8","type":"debug","z":"fec2b683.6cc178","name":"pHTempCompVerifyDebug","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","x":800,"y":160,"wires":[]},{"id":"861f297b.aecde8","type":"inject","z":"fec2b683.6cc178","name":"pushonce","topic":"","payload":"","payloadType":"num","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":100,"y":40,"wires":[["d3747aca.af6988"]]},{"id":"d3747aca.af6988","type":"change","z":"fec2b683.6cc178","name":"SetComplexCommand","rules":[{"t":"set","p":"address","pt":"msg","to":"100","tot":"str"},{"t":"set","p":"command","pt":"msg","to":"084","tot":"num"},{"t":"set","p":"payload","pt":"msg","to":"[44,49,57,46,53,48]","tot":"bin"}],"action":"","property":"","from":"","to":"","reg":false,"x":300,"y":40,"wires":[["baf25b1.c1636a8"]]},{"id":"bcade3d3.d087e","type":"i2c in","z":"fec2b683.6cc178","name":"pHTempCompVerify","address":"100","command":"","count":"9","x":540,"y":160,"wires":[["302a678b.5ef1a8"]]},{"id":"905011ff.832e3","type":"delay","z":"fec2b683.6cc178","name":"delay1sec","pauseType":"delay","timeout":"1","timeoutUnits":"seconds","rate":"1","nbRateUnits":"1","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":false,"x":100,"y":100,"wires":[["ae81536e.7faab"]]},{"id":"c77e8c77.1963a","type":"i2c out","z":"fec2b683.6cc178","name":"pHTempCompCmndVerify","address":"","command":"","payload":"payload","payloadType":"msg","count":"8","x":550,"y":100,"wires":[["fac97ac.3a34588"]]},{"id":"fac97ac.3a34588","type":"delay","z":"fec2b683.6cc178","name":"delay1sec","pauseType":"delay","timeout":"1","timeoutUnits":"seconds","rate":"1","nbRateUnits":"1","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":false,"x":100,"y":160,"wires":[["bcade3d3.d087e"]]},{"id":"ae81536e.7faab","type":"change","z":"fec2b683.6cc178","name":"SetVerify","rules":[{"t":"set","p":"address","pt":"msg","to":"100","tot":"str"},{"t":"set","p":"command","pt":"msg","to":"084","tot":"num"},{"t":"set","p":"payload","pt":"msg","to":"[44,63]","tot":"bin"}],"action":"","property":"","from":"","to":"","reg":false,"x":260,"y":100,"wires":[["c77e8c77.1963a"]]},{"id":"f4b8d3bf.d2e9f","type":"debug","z":"fec2b683.6cc178","name":"pHTempCompSetDebug","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","x":790,"y":40,"wires":[]}]

Obviously, if you are reading the compensated value from an RTD, you'll have to push that real-time value into the payload...which will involve a hefty function node to parse everything together.. and pass it as a 'buffer' type to the payload of the i2c-out node. I'll do that.. but for now, I was delighted that I succeeded in sending a hard-coded value...

This same method allowed me to pass calibration commands as well. I'll do a whole flow for that, because you really need to monitor the probe in solution for a bit and then issue the calibrate command once the readings are stablized.... So, I'll need to do some kind of continuous reading... plotting the values so that the user (a.k.a. ME) can determine at what moment things are stable enough to issue the command...More on that later....

2 Likes

Just a small update. I've obtained an EZO-PMP (Parastalic dosign pump) , and updated EZO-CON, EZO-DO circuits. It all communicates over i2c, so these devices will be implemented in this project as well.

Often the EZO circuits are initially setup with a PC sending the commands out of the Usb virtual comm port that is connected to an Arduino, that is wired to the circuits. There is an easier way. For anyone using the Atlas-Scientific i2c enabled devices/circuits, consider a purchase of the following for ease of configuration.

  1. i2c Toggler.. Allows for one touch selection of i2c or UART modes. In case you have to remove it from your circuit and reconfigure again.... $12 ... and you don't risk shorting out the circuit being impatient.

  2. USB EZO Carrier Board (Electrically Isolated). It's spendy at $51, but it allows you to hook up the circuit directly to a PC for configuration.. Later you can use the same carrier board to hook the device up to a Raspberry Pi via USB too.


I'm employing a total of 6 EZO i2c circuits (temp, ph, orp, flow, conductivity, do) across 2 projects, so these pieces take the grief out of intial setups and changeovers..

Just go through your misc cable drawer to make sure you have a cable with a USB B to USB mini B connector.

@SonoraTechnical thanks for posting this. Really helping me out to get a similar set up going.

I've got the pH and EC probes connected to a T3 Pi Tentacle for a hydroponics set up.
Similar situation as you, where the Tentacle "limits" me to i2c, so using the other UART flow was not going to work.
Your flow and some of the details you are mentioning are really helping me along, so thanks!

Seems I have some work ahead converting all the commands to ASCII decimal representation. I have read a few ways to do this via a command but the formatting of the variable length output to land up as [xx,yy,zz...] would be another function and this one may go past my ability, so for now the manual conversions are where I am.

Rob,

Yeah my way is quick and dirty.

A hobbyist by the name of Maddy (whom I mentioned earlier in this thread), has moved his Node-RED on Raspberry Pi with Atlas EZO circuits development to a new thread:

He is just killing it. His work is so superior to mine!

1 Like

This topic was automatically closed after 60 days. New replies are no longer allowed.

Wanted to upload a new version of the primary flow for the Atlas-Scientific EZO devices contained in this project. Allows for configuration and calibration.

EZODevicesFlows.json (57.2 KB)

I saw my temperatures climbing over time. The fix was easy. I was collecting allot of performance data from the Broadcom SOC every second. I lowered that to 5 seconds.... Within 30 seconds my CPU temps went from 55degC to 47degC.

I did not include system performance metrics in the last flow that I posted... However, I did have them in an earlier version I posted upstream in this thread.

Sometimes it's nice to see how someone is actually deploying a software/hardware solution. I've put a group of aquariums on-line that share common wet/dry sump, fertilizer dosing, CO2 dosing, heating, and UV Filtration. I'm controlling the temp and the pH (CO2 injection) with this Aquarium Controller. I have not yet automated water changes / top-offs...as I'm still running the system in with a very light bio-load. I also haven't automated my dosing; same reason..running the tank in.... (and the fact I haven't finished the code for the EZO Dosers....). I also haven't automated the dimming of the lights as I'm still selecting hardware for that. Here's a photo of my FIsh Nook after a few days of startup.

I'm wanting to post an updated Flow. But apparantly the flow is to large to share the code.. So I just have to attach the flow ?

AtlasScientificNCDDeviceFlows.json (80.6 KB)

Had an issue on my project Startup, I couldn't just feed an automatic start for the pump... The i2c Relay Baord wouldn't do it. I had to do a series of carefully timed toggles off, and then back on at Startup to set the Sump Pump and UV Filter Cmnd to ON and actually get them to start.

Interesting. i2c is always a bit tricky.... and relays are as well too...

Using a node called node-red-contrib-light-scheduler to turn on the lights and the UV Filter.

Upon start-up, it does take a bit more than an entire minute for this scheduler to run and turn on the assigned devices, so you have to be patient when using it. So far has proved to be very reliable.

Anywhere, here is a new copy of the Flow, with this new scheduler node in use.

AtlasScientificNCDDeviceFlows.json (86.2 KB)

I have added another piece of hardware: MCP23008 8-Channel Digital Input Output with I2C Interface

(http://store.ncd.io/product/mcp23008-8-channel-digital-input-output-with-i2c-interface/)

I am attaching 8 level sensors.. presently testing with two. I will enable Auto-Top Off, Automatic Water changes.. and make sure that heaters and pumps do not run when vessels are low... May even add a feed mode to halt the pump momentarily (but that has nothing to do with the levels).

Very pleased with the integration of the digital I/O board. It's true, I could have wired all of these digital inputs directly to the Raspberry PI GPIO.. .but my goal is no to wire any i/o directly to the Raspberry PI.. all sensors/devices communicate via I2C.

on another topic.. I changed out Flow Meters.. and I am having touble getting this one setup i the Atlas-Scientific EZO-FLO circuit.... Hence... Flow is zero, despite the pump running in the attached photo.

1 Like

Sump Pump Flow Meter is working fine. I discovered three things:

  1. I misread the spec sheet and needed to convert to lpm to configure the EZO FLO transmitter prior to use in Node-RED. (one day I will create an entire interface for sending commands to all EZO devices over i2c for configuration, but for now....it's not needed). the EZO circuit required a truth table of 6 entries.
  2. on the EZO circuit, I had a loose solder joint for the ground pin.
  3. on the EZO circuit, several of my solder joints were a bit sloppy with flux, so I cleaned them up with acetone.

SumpFlowWorks

Flow is so tricky, that I might publish a guide for things to do.... and the order in whcih they are performed, should anyone want to use the EZO Flow circuit with Node-RED. Personally, it's wonderful.. It takes so much load off of the controller. The built-in totalizing function is nice too.. and easy to enable on the fly. Again, I have to write all of this up and neaten the flow up a little before re-publishing... Just wanted to demonstrate that progress is being made.

2 Likes

I've been making some progress.. ready to add in the automated water change cycles:

AllLevelsWorking

I'm going to try out a new set of nodes that mimic ladder-logic over the weekend .. the RedPLC collection:
RedPLC Collection of Nodes

1 Like

i will have to read through this later on, just wanted to tell you how happy i am that i found your project.

throwing 300+ bucks at sensors without knowing if i even could get them working with node-red was worrying me a little to say the least.

from what i see, there's working sensors with calibration, so i will have to read upon your progress tomorrow.