OOD custom smart devices design with tuyaDAEMON: a fuzzy watering timer
Grady Booch has defined object-oriented design as “a method of design encompassing the process of object-oriented decomposition and a notation for depicting both logical and physical as well as static and dynamic models of the system under design”.
tuyaDEAMON is a rich framework for IoT, offering to the user-programmer a powerful event processor, and all features required for an OO custom devices design.
A tuyaDAEMOM custom device study case shows in detail the realization of an advanced watering system in OO style.
Final requirements:
- Remote control from any distance for emergencies.
- Weekly programming.
- Irrigation conditioned by the weather with fuzzy control.
- Fault tolerance.
This project is built starting from 3 smart devices: a breaker (composition) a second switch (aggregation) and a temperature and humidity sensor (use).
A events and messages exchange, handled by tuyaDAEMON event processor, implements the evocation of methods, allowing heritage and methods override.

The OOD simplifies and rationalizes the development, helps in the 'dp' definitions, allows good documentation in the early phases.
- For a documentation sheet about the custom device watering_sys see github
- For watering_sys details, accident scenarios, etc: see here.
- For tuyaDAEMON OO capabilities, see Wiki.
[{"id":"81d3b584.14c888","type":"subflow","name":"do logging","info":"This node simulate data received from a tuya device. I.e. the output must go to \"to logging\" input.\nUsed to build an update message for 'mirror' devices.\n\nUsed to process tuyaTRIGGERS.\n\n- `deviceID`: the 'mirror' device ID or CID\n- `fakeDP`: the data point id. Convention: user defined, equal to related tuyaTRIGGER, number (1000-2000). \n- `value`: the new value. If the value starts with '@', then it is eval()ued: exemple '@msg.payoad.set' => eval('msg.payoad.set'))\n\nnote: The 'mirror' device MUST exist in the `fake` branch of **alldevices**.\n\n","category":"","in":[{"x":120,"y":80,"wires":[{"id":"6d1b2b8f.c5e554"}]}],"out":[{"x":460,"y":80,"wires":[{"id":"6d1b2b8f.c5e554","port":0}]}],"env":[{"name":"deviceID","type":"str","value":""},{"name":"propertyDP","type":"str","value":""},{"name":"value","type":"str","value":""}],"color":"#87A980","icon":"node-red/debug.svg"},{"id":"6d1b2b8f.c5e554","type":"function","z":"81d3b584.14c888","name":"do mirror msg","func":"// local function\n// note: the value param can also be a @variable:\n//e.g. \"@msg.payload.set\" => eval(msg.payload.set) \n\n // builds a fake OUT message\n var newMsg = {\n \"payload\": {\n \"deviceId\": env.get(\"deviceID\"),\n \"data\": {\n \"dps\": {}\n // \"dps\":[dpname = value]\n }\n }\n };\n \n var xvalue = env.get(\"value\").toString().trim();\n if (xvalue.startsWith('@')){\n xvalue = eval(xvalue.substring(1));\n }\n var dpname = env.get(\"propertyDP\").toString().trim();\n if (dpname.startsWith('@')){\n dpname = eval(dpname.substring(1));\n }\n \n newMsg.payload.data.dps[dpname.toString()] = xvalue;\n return newMsg;\n","outputs":1,"noerr":0,"initialize":"","finalize":"","x":300,"y":80,"wires":[[]]},{"id":"f30e8140.1bed9","type":"subflow","name":"do command","info":"This node creates a command for a tuya device. I.e. the output must go to \"IN command\" or \"fast IN\" inputs.\n\nUsed to process tuyaTRIGGERS.\n\nParameters:\n- `device`: the target device\n- `property`: the data point name or id. \n- `value`: the new value. If the value starts with '@', it is eval()ued: exemple '@msg.payoad.set' => eval('msg.payoad.set').\n\n- The values `msg.payload.device`, `msg.payload.property` and `msg.payload.value` replaces the parameters.\n\n","category":"","in":[{"x":120,"y":60,"wires":[{"id":"cf230930.f37938"}]}],"out":[{"x":540,"y":60,"wires":[{"id":"cf230930.f37938","port":0}]}],"env":[{"name":"device","type":"str","value":""},{"name":"property","type":"str","value":""},{"name":"value","type":"str","value":""}],"color":"#DDAA99"},{"id":"cf230930.f37938","type":"function","z":"f30e8140.1bed9","name":"","func":"\nfunction _sendCmd(adev, adp, aval) {\n var res = {\n payload :{\n device: adev,\n property: adp,\n value: aval\n }\n }\n if (msg.payload !== undefined){\n if (msg.payload.device !== undefined)\n res.payload.device = msg.payload.device;\n if (msg.payload.property !== undefined)\n res.payload.property = msg.payload.property;\n if (msg.payload.value !== undefined)\n res.payload.value = msg.payload.value;\n }\n return res;\n}\n\nvar value = env.get(\"value\").toString().trim();\n if (value.startsWith('@')){\n value = eval(value.substring(1));\n }\n\nreturn _sendCmd(env.get(\"device\"), env.get(\"property\"), value);\n\n","outputs":1,"noerr":0,"initialize":"","finalize":"","x":320,"y":60,"wires":[[]]},{"id":"66e0cde4.b696f4","type":"tab","label":"derived.watering_sys","disabled":true,"info":"This is a OO derived device to watering terraces.\nBase devices:\n- Smart_breaker (S3077)\n- Smart_switch01 (MOES QS-WIFI-S03)\n- Temperature_Humidity_Sensor (WSDCGQ11LM)\n\nsee: https://github.com/msillano/tuyaDAEMON/wiki/derived-device-'watering_sys':-case-study"},{"id":"c3a7162d.b98bf8","type":"ui_text_input","z":"66e0cde4.b696f4","name":"","label":"start","tooltip":"","group":"8a16e2fd.a07e2","order":1,"width":0,"height":0,"passthru":false,"mode":"time","delay":300,"topic":"start1","topicType":"str","x":730,"y":280,"wires":[["221aa24e.28742e"]]},{"id":"170d5f65.7e9f11","type":"ui_text_input","z":"66e0cde4.b696f4","name":"","label":"start","tooltip":"","group":"e04eed03.d3291","order":1,"width":0,"height":0,"passthru":false,"mode":"time","delay":300,"topic":"start2","topicType":"str","x":730,"y":440,"wires":[["221aa24e.28742e"]]},{"id":"52ad810c.bc96a","type":"ui_button","z":"66e0cde4.b696f4","name":"","group":"2e287b5a.1b1814","order":1,"width":"4","height":1,"passthru":false,"label":"ALARM STOP","tooltip":"","color":"","bgcolor":"{{msg.background}}","icon":"","payload":"{\"main\":\"OFF\"}","payloadType":"json","topic":"topic","topicType":"msg","x":760,"y":1000,"wires":[["428d4751.8ce6d8"]]},{"id":"9f4ad3b6.35136","type":"ui_text_input","z":"66e0cde4.b696f4","name":"","label":"start","tooltip":"","group":"31f79248.17674e","order":1,"width":0,"height":0,"passthru":false,"mode":"time","delay":300,"topic":"start3","topicType":"str","x":730,"y":600,"wires":[["221aa24e.28742e"]]},{"id":"f457f873.6d4038","type":"ui_button","z":"66e0cde4.b696f4","name":"","group":"194a6329.0b062d","order":6,"width":"1","height":1,"passthru":true,"label":"RST","tooltip":"","color":"","bgcolor":"{{background}}","icon":"","payload":"{\"reset\":\"ON\"}","payloadType":"json","topic":"topic","topicType":"msg","x":910,"y":260,"wires":[["c83809c2.5cdc98","9dd4cc52.10ca8"]]},{"id":"f0e81f37.f91a6","type":"ui_button","z":"66e0cde4.b696f4","name":"","group":"2e287b5a.1b1814","order":2,"width":"4","height":1,"passthru":false,"label":"OUTPUT TOGGLE","tooltip":"","color":"","bgcolor":"{{msg.background}}","icon":"","payload":"any","payloadType":"str","topic":"topic","topicType":"msg","x":770,"y":960,"wires":[["61881e9a.9e2c7"]]},{"id":"221aa24e.28742e","type":"function","z":"66e0cde4.b696f4","name":"flow store data","func":"\n \nvar data = flow.get(\"watering\");\ndata[msg.topic] = msg.payload;\nflow.set(\"watering\", data);\n\nlet newmsg = {in_msg: msg,\n background: \"yellow\"};\nreturn newmsg;","outputs":1,"noerr":0,"initialize":"// Code added here will be run once\n// whenever the node is deployed.\n var data ={};\n flow.set(\"watering\", data);","finalize":"","x":960,"y":680,"wires":[[]]},{"id":"8e6204b9.933f58","type":"ui_chart","z":"66e0cde4.b696f4","name":"chart","group":"2e287b5a.1b1814","order":5,"width":"9","height":"6","label":"Irrigation and weather {{msg.title}}","chartType":"line","legend":"false","xformat":"HH","interpolate":"step","nodata":"","dot":false,"ymin":"0","ymax":"50","removeOlder":"24","removeOlderPoints":"","removeOlderUnit":"3600","cutout":0,"useOneColor":false,"useUTC":false,"colors":["#89a5d2","#68a9fd","#ed9b9b","#3ccd3c","#98df8a","#ca5858","#912827","#9467bd","#c5b0d5"],"outputs":1,"useDifferentColor":false,"x":730,"y":180,"wires":[[]]},{"id":"f665d47.2df4f28","type":"ui_button","z":"66e0cde4.b696f4","name":"GET CIRCULATE","group":"194a6329.0b062d","order":2,"width":"1","height":"1","passthru":false,"label":"","tooltip":"","color":"","bgcolor":"","icon":"undo","payload":"any","payloadType":"str","topic":"get","topicType":"str","x":770,"y":800,"wires":[["c790c9ed.3cc7f8"]]},{"id":"bcd4f07b.e4fd1","type":"ui_button","z":"66e0cde4.b696f4","name":"SET CIRCULATE","group":"194a6329.0b062d","order":4,"width":"1","height":"1","passthru":false,"label":"","tooltip":"","color":"--nr-dashboard-widgetColor","bgcolor":"{{msg.background}}","icon":"save","payload":"ok","payloadType":"str","topic":"set","topicType":"str","x":770,"y":900,"wires":[["38e4cc0b.beb5f4"]]},{"id":"fff45c98.8d5c4","type":"ui_slider","z":"66e0cde4.b696f4","name":"","label":"water","tooltip":"max week water (liters)","group":"194a6329.0b062d","order":1,"width":"8","height":"1","passthru":true,"outs":"end","topic":"waterweek","topicType":"str","min":0,"max":"1000","step":"10","x":730,"y":760,"wires":[["221aa24e.28742e","f9fdcebd.a1fda"]]},{"id":"151ed4ad.6794eb","type":"ui_dropdown","z":"66e0cde4.b696f4","name":"","label":"","tooltip":"","place":"Select days","group":"194a6329.0b062d","order":3,"width":"8","height":"1","passthru":false,"multiple":true,"options":[{"label":"Dom.","value":1,"type":"num"},{"label":"Lun.","value":"2","type":"str"},{"label":"Mar.","value":"3","type":"str"},{"label":"Mer.","value":"4","type":"str"},{"label":"Gio.","value":"5","type":"str"},{"label":"Ven.","value":"6","type":"str"},{"label":"Sab.","value":"7","type":"str"}],"payload":"","topic":"days1","topicType":"str","x":740,"y":240,"wires":[["221aa24e.28742e"]]},{"id":"428d4751.8ce6d8","type":"subflow:f30e8140.1bed9","z":"66e0cde4.b696f4","name":"cmd main-switch","env":[{"name":"device","value":"watering main","type":"str"},{"name":"property","value":"switch","type":"str"},{"name":"value","value":"@msg.payload.main","type":"str"}],"x":1390,"y":1000,"wires":[["739fba48.a46514"]]},{"id":"739fba48.a46514","type":"link out","z":"66e0cde4.b696f4","name":"to core.fast_cmds","links":["c084a743.290b28"],"x":1555,"y":960,"wires":[]},{"id":"c790c9ed.3cc7f8","type":"subflow:f30e8140.1bed9","z":"66e0cde4.b696f4","name":"cmd timer-circulate GET","env":[{"name":"device","value":"watering timer","type":"str"},{"name":"property","value":"circulate","type":"str"},{"name":"value","value":"NULL","type":"str"}],"x":1370,"y":800,"wires":[["8834ba78.eef318"]]},{"id":"8834ba78.eef318","type":"link out","z":"66e0cde4.b696f4","name":"to core.fast_cmds","links":["c084a743.290b28"],"x":1555,"y":800,"wires":[]},{"id":"b5aba5f4.6a3778","type":"link in","z":"66e0cde4.b696f4","name":"from core.fake_cmds","links":["18817677.061b9a"],"x":95,"y":680,"wires":[["e1cc1c52.293fd","11e9733f.2d36bd","6f3ab810.a4d9d8"]]},{"id":"38e4cc0b.beb5f4","type":"function","z":"66e0cde4.b696f4","name":"process data","func":"\nfunction hmTomin(hm) { // local function\n if (typeof hm === 'string') {\n pieces = hm.split(':');\n return (parseInt(pieces[0]) * 60 + parseInt(pieces[1]));\n }\n return 0;\n}\n\nfunction minTohm(min) { // local function\n h = (~~(min / 60));\n m = min % 60;\n if (isNaN(m))\n return \"00:00\";\n return (h > 9 ? h : '0' + h) + \":\" + (m > 9 ? m : '0' + m);\n}\n\nfunction sumHmHm(t1, t2) { // local function\n var tot = hmTomin(t1) + hmTomin(t2);\n return (minTohm(tot));\n}\n\nfunction formatDayArr(dayArr) {\n var dt = \"-------\";\n for (var i = 1; i < 8; i++) {\n if (dayArr.includes(i) || dayArr.includes(i.toString()) )\n dt = dt.substring(0, i- 1) + i + dt.substring(i);\n }\n// node.warn([\"DAY process \", dayArr,dt]);\n return dt;\n}\n\nvar data = flow.get(\"watering\");\n\nvar tobj = [{\n active: false,\n days: \"-------\",\n start: \"00:00\",\n end: \"00:00\",\n on: \"00:00\",\n off: \"00:00\"\n }, {\n active: false,\n days: \"-------\",\n start: \"00:00\",\n end: \"00:00\",\n on: \"00:00\",\n off: \"00:00\"\n }, {\n active: false,\n days: \"-------\",\n start: \"00:00\",\n end: \"00:00\",\n on: \"00:00\",\n off: \"00:00\"\n }, {\n active: false,\n days: \"-------\",\n start: \"00:00\",\n end: \"00:00\",\n on: \"00:00\",\n off: \"00:00\"\n }\n];\n\nvar daycount = 0;\nif (data.days1 == undefined) {\n // node.warn(\"Noting to save. Do a GET\");\n return null;\n}\n\nfor (var i = 1; i < 4; i++) {\n if (data[\"start\" + i] > data[\"end\" + i]) {\n // node.warn(\"ERROR on Time \"+i+\": check start, end. Abort.\");\n return [{\n topic: \"bad\" + i\n }\n ];\n }\n\n\nif (msg.topic === \"set\") {\n // restored by hand\n data.nowatering = false;\n}\n\n//------------------- data to circulate\n\ndaycount = data.days1.length;\nvar totaldaytime = (data.store1 ? (data[\"end1\"] - data[\"start1\"]) / 60000 : 0) +\n (data.store2 ? (data[\"end2\"] - data[\"start2\"]) / 60000 : 0) +\n (data.store3 ? (data[\"end3\"] - data[\"start3\"]) / 60000 : 0);\n\nif ((daycount === 0) || (totaldaytime === 0)) {\n // node.warn(\"WARNING: null day/times, no watering \");\n node.send([{topic: \"bad0\"}]);\n node.warn(\" NO watering: daycount: \" + daycount + \" totaldaytime: \" + totaldaytime);\n data[\"nowatering\"] = true;\n}\n// default values in case of bad values\nvar offtime = 0;\nvar wetdaytime = 0;\n\nif ((daycount > 0) && (totaldaytime > 0)) {\n let weekwettime = (data.waterweek * data.adjust) / data.litresminute;\n wetdaytime = weekwettime / daycount;\n let daycycles = Math.round(wetdaytime / data.ontime);\n if (daycycles > 0)\n offtime = Math.round((totaldaytime - wetdaytime) / daycycles);\n if (offtime < 0)\n offtime = 0;\n}\n\ndata.wetdaytime = wetdaytime;\ndata.offtime = offtime;\n\n tobj[i - 1].days = formatDayArr(data.days1);\n tobj[i - 1].active = data.nowatering ? false : data[\"store\" + i];\n tobj[i - 1].start = minTohm(data[\"start\" + i] / 60000);\n tobj[i - 1].end = minTohm(data[\"end\" + i] / 60000);\n tobj[i - 1].on = minTohm(data.ontime);\n tobj[i - 1].off = minTohm(offtime);\n}\n\ndata.active1 = tobj[0].active;\ndata.active2 = tobj[1].active;\ndata.active3 = tobj[2].active;\n\nif ((msg.topic === \"set\") && (data.nowatering === false)) {\n // restored by hand\n data.store1 = tobj[0].active;\n data.store2 = tobj[1].active;\n data.store3 = tobj[2].active;\n}\n\n\n// ---- extra storage\nlet dx = data.store3 ? 100 : 0;\ndx *= 2;\ndx += data.store2 ? 100 : 0;\ndx *= 2;\ndx += data.store1 ? 100 : 0;\ndx *= 2;\ndx += data.nowatering ? 100 : 0;\ndx += data.adjust < 1 ? Math.round(data.adjust * 100) : 99;\n//\ntobj[3].on = minTohm(dx);\ntobj[3].off = minTohm(data.waterweek);\n\n//-------------- save\n// node.warn([\" saved ...\", data, tobj])\nflow.set(\"watering\", data)\nmsg.payload = tobj;\nreturn [{ topic: \"set\"}, msg];\n","outputs":2,"noerr":0,"initialize":"","finalize":"","x":1090,"y":900,"wires":[["3de474ea.ca7c1c"],["f1472bd1.d3ee88"]],"inputLabels":["trigger"],"outputLabels":["status","set"]},{"id":"74005ff3.2ae0e","type":"ui_text_input","z":"66e0cde4.b696f4","name":"","label":"end","tooltip":"","group":"8a16e2fd.a07e2","order":1,"width":0,"height":0,"passthru":false,"mode":"time","delay":300,"topic":"end1","topicType":"str","x":730,"y":320,"wires":[["221aa24e.28742e"]]},{"id":"7ef6e664.a5b0f8","type":"ui_text_input","z":"66e0cde4.b696f4","name":"","label":"end","tooltip":"","group":"e04eed03.d3291","order":1,"width":0,"height":0,"passthru":false,"mode":"time","delay":300,"topic":"end2","topicType":"str","x":730,"y":480,"wires":[["221aa24e.28742e"]]},{"id":"f5cf7ffe.7db69","type":"ui_text_input","z":"66e0cde4.b696f4","name":"","label":"end","tooltip":"","group":"31f79248.17674e","order":1,"width":0,"height":0,"passthru":true,"mode":"time","delay":300,"topic":"end3","topicType":"str","x":730,"y":640,"wires":[["221aa24e.28742e"]]},{"id":"6ff96dca.116a74","type":"config","z":"66e0cde4.b696f4","name":"PARAMETERS","properties":[{"p":"watering","pt":"flow","to":"{\"maxwater\":1000,\"litresminute\":1.5,\"ontime\":5,\"OFFtemperature\":4,\"OFFRH\":90,\"nowatering\":false,\"adjust\":0.5,\"waterweek\":100,\"more01\":\"Update time (18:30) is on [*SET TIME] node\"}","tot":"json"},{"p":"fuzzyset","pt":"flow","to":"{\"comment\":\" the result is in the range 12..90 %\",\"crisp_input\":[10,50],\"variables_input\":[{\"name\":\"Temperature\",\"setsName\":[\"Frost\",\"Cold\",\"Medium\",\"Hot\",\"Very hot\"],\"sets\":[[0,0,5,10],[5,10,10,20],[10,20,20,30],[20,30,30,35],[30,35,40,40]]},{\"name\":\"Relative Humidity\",\"setsName\":[\"Poor\",\"Fair\",\"Good\",\"High\",\"Very high\"],\"sets\":[[0,0,20,30],[10,30,30,50],[35,45,60,70],[55,75,75,90],[75,90,100,100]]}],\"variable_output\":{\"name\":\"Watering factor\",\"setsName\":[\"None\",\"Low\",\"Medium\",\"Heavy\"],\"sets\":[[0,0,15,30],[15,30,40,55],[40,55,75,90],[75,90,100,100]]},\"inferences\":[[0,0,1,2,3],[3,2,2,1,0]]}","tot":"json"}],"active":true,"x":220,"y":220,"wires":[],"info":"parameters\n \"litresminute\": 2.5, // the water output\n \"ontime\": 6 // the cycle time ON \ndefaults \n \"waterweek\": 1000, // if not user defined \n // note: the limit is the MAX value in the 'water' slider.\n \"adjust\": 0.8, // range 0..1\n"},{"id":"466b8db9.753984","type":"ui_switch","z":"66e0cde4.b696f4","name":"","label":"","tooltip":"","group":"8a16e2fd.a07e2","order":4,"width":"1","height":"1","passthru":true,"decouple":"false","topic":"store1","topicType":"str","style":"","onvalue":"true","onvalueType":"bool","onicon":"","oncolor":"","offvalue":"false","offvalueType":"bool","officon":"","offcolor":"","animate":false,"x":730,"y":400,"wires":[["221aa24e.28742e","e83f263e.366d48"]]},{"id":"2353cc16.5f1da4","type":"ui_switch","z":"66e0cde4.b696f4","name":"","label":"","tooltip":"","group":"31f79248.17674e","order":4,"width":"1","height":"1","passthru":true,"decouple":"false","topic":"store3","topicType":"str","style":"","onvalue":"true","onvalueType":"bool","onicon":"","oncolor":"","offvalue":"false","offvalueType":"bool","officon":"","offcolor":"","animate":false,"x":730,"y":720,"wires":[["221aa24e.28742e","6aa907d.0232ef8"]]},{"id":"1bfd2efe.6ebe51","type":"ui_switch","z":"66e0cde4.b696f4","name":"","label":"","tooltip":"","group":"e04eed03.d3291","order":4,"width":"1","height":"1","passthru":true,"decouple":"false","topic":"store2","topicType":"str","style":"","onvalue":"true","onvalueType":"bool","onicon":"","oncolor":"","offvalue":"false","offvalueType":"bool","officon":"","offcolor":"","animate":false,"x":730,"y":560,"wires":[["221aa24e.28742e","f19a9bd9.a8bd48"]]},{"id":"6aa907d.0232ef8","type":"ui_led","z":"66e0cde4.b696f4","order":3,"group":"31f79248.17674e","width":"2","height":"1","label":"active","labelPlacement":"right","labelAlignment":"left","colorForValue":[{"color":"#808080","value":"none","valueType":"str"},{"color":"#008000","value":"true","valueType":"bool"},{"color":"#ff0000","value":"false","valueType":"bool"}],"allowColorForValueInMessage":false,"shape":"circle","showGlow":true,"name":"","x":730,"y":680,"wires":[]},{"id":"f19a9bd9.a8bd48","type":"ui_led","z":"66e0cde4.b696f4","order":3,"group":"e04eed03.d3291","width":"2","height":"1","label":"active","labelPlacement":"right","labelAlignment":"left","colorForValue":[{"color":"#ff0000","value":"false","valueType":"bool"},{"color":"#008000","value":"true","valueType":"bool"},{"color":"#808080","value":"none","valueType":"str"}],"allowColorForValueInMessage":false,"shape":"circle","showGlow":true,"name":"","x":730,"y":520,"wires":[]},{"id":"e83f263e.366d48","type":"ui_led","z":"66e0cde4.b696f4","order":3,"group":"8a16e2fd.a07e2","width":"2","height":"1","label":"active","labelPlacement":"right","labelAlignment":"left","colorForValue":[{"color":"#ff0000","value":"false","valueType":"bool"},{"color":"#008000","value":"true","valueType":"bool"},{"color":"#808080","value":"none","valueType":"str"}],"allowColorForValueInMessage":false,"shape":"circle","showGlow":true,"name":"","x":730,"y":360,"wires":[]},{"id":"f1472bd1.d3ee88","type":"subflow:f30e8140.1bed9","z":"66e0cde4.b696f4","name":"cmd circulate SET","env":[{"name":"device","value":"watering_sys","type":"str"},{"name":"property","value":"circulate","type":"str"},{"name":"value","value":"@msg.payload","type":"str"}],"x":1390,"y":900,"wires":[["925fb316.bd054"]],"inputLabels":["trigger"],"outputLabels":["to (fast) command"]},{"id":"925fb316.bd054","type":"link out","z":"66e0cde4.b696f4","name":"to core.std_cmd","links":["1d0eea8.b33d616"],"x":1555,"y":900,"wires":[]},{"id":"5f502c79.31b9f4","type":"ui_template","z":"66e0cde4.b696f4","group":"194a6329.0b062d","name":"","order":5,"width":"8","height":"1","format":"<div ng-bind-html=\"msg.payload\"></div>","storeOutMessages":false,"fwdInMessages":false,"resendOnRefresh":false,"templateScope":"local","x":740,"y":840,"wires":[[]]},{"id":"9c2034a2.c37a78","type":"comment","z":"66e0cde4.b696f4","name":"Level 2 device 'watering_sys'","info":"","x":200,"y":80,"wires":[]},{"id":"b157e6e4.9de758","type":"ui_button","z":"66e0cde4.b696f4","name":"","group":"2e287b5a.1b1814","order":4,"width":"1","height":"1","passthru":false,"label":"clr","tooltip":"clear graphic ","color":"","bgcolor":"","icon":"","payload":"[]","payloadType":"json","topic":"topic","topicType":"msg","x":730,"y":120,"wires":[["c83809c2.5cdc98","8e6204b9.933f58"]]},{"id":"61881e9a.9e2c7","type":"subflow:f30e8140.1bed9","z":"66e0cde4.b696f4","name":"cmd timer-relay TOGGLE","env":[{"name":"device","value":"watering timer","type":"str"},{"name":"property","value":"countdown","type":"str"},{"name":"value","value":"1","type":"str"}],"x":1370,"y":960,"wires":[["739fba48.a46514"]]},{"id":"8bead4b6.de9e98","type":"function","z":"66e0cde4.b696f4","name":"timer button","func":"// logic buttons status, color\n// and storage for suppression of redundant commands\n// output:\n// 1: more buttons\n// 2: toggle\nvar status = flow.get(\"timer\");\n// stored:\n// timer_lastcolor : ONCOLOR|OFFCOLOR\n// timer_enabled: true|false|undefined\n// main_enabled: true|false|undefined\n\n// inputs:\n// {topic = \"Timer\"; newval = ON|OFF}\n// {topic = \"Main\"; newval = ON|OFF}\n// {topic = \"timer\"; enabled = true|false}\n// {topic = \"main\"; enabled = true|false}\nconst ONCOLOR = \"blue\";\nconst OFFCOLOR = \"green\";\n// filter extra CONNECTED messages, because extra 'enabled' can change color\n// in any case, if enabled == true restores lastcolor\nif (msg.topic === \"timer\") {\n if ((msg.enabled !== undefined) && (msg.enabled !== status.timer_enabled)) {\n status.timer_enabled = msg.enabled;\n flow.set(\"timer\", status);\n if (msg.enabled)\n return ([{\n enabled: true\n }, {\n enabled: true,\n background: status.timer_lastcolor\n }\n ]);\n else\n return ([{\n enabled: false\n }, {\n enabled: false\n }\n ]);\n } else {\n return [null];\n }\n}\n\n// if Main OFF, the timer is fast OFF + disabled\nif ((msg.topic === \"Main\") && (msg.newval === \"OFF\")) {\n status.timer_enabled = false;\n status.timer_lastcolor = OFFCOLOR;\n flow.set(\"timer\", status);\n return [{\n enabled: false\n }, {\n enabled: false,\n background: OFFCOLOR\n }\n ];\n}\n\n// change color ON/OFF\nif (msg.topic === \"Timer\") {\n // test: is enabled? if not enable it\n if (!status.timer_enabled) {\n status.timer_enabled = true;\n node.send([{\n enabled: true\n }, {\n enabled: true\n }\n ]);\n }\n\n if (msg.newval === \"OFF\") {\n status.timer_lastcolor = OFFCOLOR;\n flow.set(\"timer\", status);\n return [null, {\n background: OFFCOLOR\n }\n ];\n } else {\n status.timer_lastcolor = ONCOLOR;\n flow.set(\"timer\", status);\n return [null, {\n background: ONCOLOR\n }\n ];\n }\n}\n\n// reset to set color\nif (msg.payload.reset !== undefined){\n// status.enabled = true ;\n// flow.set(\"timer\", status);\n// msg.enabled = true;\n// node.send([msg, null]);\n// msg.background = status.lastcolor;\n return [null, {\n background: status.timer_lastcolor }];\n}\n \nreturn ([null]);\n","outputs":2,"noerr":0,"initialize":"// Code added here will be run once\n// whenever the node is deployed.\nflow.set(\"timer\", {\n timer_lastcolor : undefined,\n timer_enabled: undefined,\n main_enabled: undefined });\n","finalize":"","x":570,"y":920,"wires":[["bcd4f07b.e4fd1","f665d47.2df4f28"],["f0e81f37.f91a6"]],"outputLabels":["buttons","toggle"]},{"id":"c30ab453.0d0c18","type":"function","z":"66e0cde4.b696f4","name":"main button","func":"// logic\n// color: MAIN_ON/FF\nif (msg.topic === \"Main\") {\n if (msg.newval == \"OFF\")\n msg.background = \"black\";\n else\n msg.background = \"red\";\nreturn msg;\n}\nreturn null;\n\n","outputs":1,"noerr":0,"initialize":"","finalize":"","x":570,"y":1000,"wires":[["52ad810c.bc96a"]]},{"id":"3de474ea.ca7c1c","type":"function","z":"66e0cde4.b696f4","name":"status msg","func":"// generates user messages\n\n var show ={\n payload: \" tuyaDAEMON Watering system v. 1.0\"\n }; \n\nvar data = flow.get(\"watering\");\nlet perc = Math.round( data.adjust *100);\n\n// from timer relay\nif (msg.topic === \"Timer\"){\n if (msg.newval === \"OFF\")\n show.payload = \" Watering OFF Litres(week) = \" + Math.round(data.waterweek * data.adjust) +\" (\"+perc+\"%)\";\n else \n show.payload = \" Watering ON \"+data.ontime+\"/\"+data.offtime +\" min. Litres(today): \" + Math.round(data.wetdaytime * data.litresminute);\n}\n\n// from SET button (process data), good\nif (msg.topic === \"set\"){\n if (data.nowatering)\n show.payload = \" No irrigation today\";\n else\n show.payload = \" Updated - ON/OFF: \"+data.ontime+\"/\"+data.offtime +\" min. Litres(day): \"+ Math.round(data.wetdaytime * data.litresminute) + \" (\"+perc+\"%)\";\n}\n\n// from SET button (process data), bad\nif (msg.topic === \"badv\"){\n show.payload = \"Failed: set more time or less water.\";\n}\nif (msg.topic === \"bad0\"){\n show.payload = \"Failed: null day/time, check Time. \";\n}\nif (msg.topic === \"bad1\"){\n show.payload = \"ERROR on 'Time1': check start, end.\";\n}\nif (msg.topic === \"bad2\"){\n show.payload = \"ERROR on 'Time2': check start, end.\";\n}\nif (msg.topic === \"bad3\"){\n show.payload = \"ERROR on 'Time3': check start, end.\";\n}\n\nreturn show;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":570,"y":840,"wires":[["5f502c79.31b9f4"]]},{"id":"dfb93a42.cf51c8","type":"subflow:f30e8140.1bed9","z":"66e0cde4.b696f4","name":"cmd timer-restart ","env":[{"name":"device","value":"watering timer","type":"str"},{"name":"property","value":"restart status","type":"str"},{"name":"value","value":"off","type":"str"}],"x":1390,"y":440,"wires":[["4242c140.fdbb2"]]},{"id":"4242c140.fdbb2","type":"link out","z":"66e0cde4.b696f4","name":"to core.fast_cmds","links":["c084a743.290b28"],"x":1555,"y":300,"wires":[]},{"id":"3c9f6d43.8d11e2","type":"subflow:f30e8140.1bed9","z":"66e0cde4.b696f4","name":"cmd main ON","env":[{"name":"device","value":"watering main","type":"str"},{"name":"property","value":"switch","type":"str"},{"name":"value","value":"ON","type":"str"}],"x":1400,"y":320,"wires":[["4242c140.fdbb2"]]},{"id":"5604c052.5ca1a","type":"subflow:f30e8140.1bed9","z":"66e0cde4.b696f4","name":"cmd main-restart","env":[{"name":"device","value":"watering main","type":"str"},{"name":"property","value":"on reset","type":"str"},{"name":"value","value":"ON","type":"str"}],"x":1390,"y":360,"wires":[["4242c140.fdbb2"]]},{"id":"e3071913.55fb98","type":"link in","z":"66e0cde4.b696f4","name":"from system.start_DAEMON","links":["37306de6.0a7f42"],"x":955,"y":320,"wires":[["9dd4cc52.10ca8"]]},{"id":"9dd4cc52.10ca8","type":"delay","z":"66e0cde4.b696f4","name":"","pauseType":"delay","timeout":"300","timeoutUnits":"milliseconds","rate":"1","nbRateUnits":"1","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":false,"x":1090,"y":320,"wires":[["3c9f6d43.8d11e2","5604c052.5ca1a","4622fe72.9c05f","76616ca5.815b44"]]},{"id":"4622fe72.9c05f","type":"delay","z":"66e0cde4.b696f4","name":"","pauseType":"delay","timeout":"3","timeoutUnits":"seconds","rate":"1","nbRateUnits":"1","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":false,"x":1100,"y":400,"wires":[["c790c9ed.3cc7f8","dfb93a42.cf51c8","1b640c14.dc81c4"]]},{"id":"6f08696a.8d64d8","type":"function","z":"66e0cde4.b696f4","name":"init chart","func":"// to fix order and colors\nmsg = {\n topic:\"Main\",\n payload:10\n}\nnode.send(msg);\nmsg = {\n topic:\"Timer\",\n payload:0\n}\nnode.send(msg);\nmsg = {\n topic:\"Temp\",\n payload:null\n}\nnode.send(msg);\nmsg = {\n topic:\"RH/2\",\n payload:null\n}\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":580,"y":160,"wires":[["8e6204b9.933f58"]]},{"id":"c83809c2.5cdc98","type":"delay","z":"66e0cde4.b696f4","name":"","pauseType":"delay","timeout":"600","timeoutUnits":"milliseconds","rate":"1","nbRateUnits":"1","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":false,"x":1090,"y":120,"wires":[["6f08696a.8d64d8"]]},{"id":"f9fdcebd.a1fda","type":"subflow:81d3b584.14c888","z":"66e0cde4.b696f4","name":"","env":[{"name":"deviceID","value":"watering_sys","type":"str"},{"name":"propertyDP","value":"5","type":"str"},{"name":"value","value":"@msg.payload","type":"str"}],"x":1410,"y":660,"wires":[["9a343958.4999a8"]]},{"id":"9a343958.4999a8","type":"link out","z":"66e0cde4.b696f4","name":"to core.logging","links":["9fe80f7e.f3f7e"],"x":1555,"y":600,"wires":[]},{"id":"44351050.b577b","type":"link out","z":"66e0cde4.b696f4","name":"to core.logging","links":["9fe80f7e.f3f7e"],"x":495,"y":720,"wires":[]},{"id":"a131d6fe.bff008","type":"link out","z":"66e0cde4.b696f4","name":"to core.fast_cmds","links":["c084a743.290b28"],"x":355,"y":380,"wires":[]},{"id":"1547be76.90a842","type":"link out","z":"66e0cde4.b696f4","name":"to core.logging","links":["9fe80f7e.f3f7e"],"x":435,"y":380,"wires":[]},{"id":"cc1ae669.a8cba8","type":"inject","z":"66e0cde4.b696f4","name":"chart refresh","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"180","crontab":"","once":true,"onceDelay":"8","topic":"","payload":"","payloadType":"date","x":1080,"y":220,"wires":[["70460352.c6299c","6304fdbf.4d8864"]]},{"id":"70460352.c6299c","type":"subflow:f30e8140.1bed9","z":"66e0cde4.b696f4","name":"refresh main","env":[{"name":"device","value":"watering main","type":"str"},{"name":"property","value":"switch","type":"str"},{"name":"value","value":"NULL","type":"str"}],"x":1410,"y":200,"wires":[["4242c140.fdbb2"]]},{"id":"6304fdbf.4d8864","type":"subflow:f30e8140.1bed9","z":"66e0cde4.b696f4","name":"refresh timer","env":[{"name":"device","value":"watering timer","type":"str"},{"name":"property","value":"relay","type":"str"},{"name":"value","value":"NULL","type":"str"}],"x":1410,"y":240,"wires":[["4242c140.fdbb2"]]},{"id":"77dbccc9.ba9b64","type":"link in","z":"66e0cde4.b696f4","name":"internal: to timer","links":["76616ca5.815b44"],"x":415,"y":920,"wires":[["8bead4b6.de9e98"]],"icon":"font-awesome/fa-expand"},{"id":"7251becd.aab9d","type":"file","z":"66e0cde4.b696f4","name":"save path","filename":"D:\\xampp\\htdocs\\fuzzytool\\lastset.json","appendNewline":false,"createDir":true,"overwriteFile":"true","encoding":"utf8","x":420,"y":280,"wires":[[]]},{"id":"1d88ce30.e7f502","type":"inject","z":"66e0cde4.b696f4","name":"SAVE FUZZYSET","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"$flowContext(\"fuzzyset\")\t","payloadType":"jsonata","x":230,"y":280,"wires":[["7251becd.aab9d"]]},{"id":"4e356044.8a194","type":"function","z":"66e0cde4.b696f4","name":"Fuzzy","func":"// Fuzzy process the temp, RH values\n// FuzzyLogic library by Marco Lanaro, see https://github.com/marcolanaro/JS-Fuzzy\nvar FuzzyLogic=function(){\"use strict\";var a=function(){};return a.prototype={getResult:function(a){var b=this.construct(a.variables_input),c=this.fuzzification(a.crisp_input,b),d=this.output_combination(c,a.inferences,a.variable_output),e=this.takeMaxOfArraySet(d),f=this.defuzzification(e,this.construct_variable(a.variable_output.sets));return f},construct:function(a){var c,b=[];for(c=a.length-1;c>=0;c-=1)b[c]=this.construct_variable(a[c].sets);return b},construct_variable:function(a){var c,b=[];for(c=a.length-1;c>=0;c-=1)b[c]={a:a[c],firstPoint:a[c][0]===a[c][1]?1:0,lastPoint:a[c][2]===a[c][3]?1:0,mUp:1/(a[c][1]-a[c][0]),mDown:1/(a[c][3]-a[c][2])};return b},fuzzification:function(a,b){var d,c=[];for(d=b.length-1;d>=0;d-=1)c[d]=this.fuzzification_variable(a[d],b[d]);return c},fuzzification_variable:function(a,b){var d,c=[];for(d=b.length-1;d>=0;d-=1)c[d]=this.fuzzification_function(a,b[d]);return c},fuzzification_function:function(a,b){var c=0;return b.a[0]>=a?c=b.firstPoint:b.a[1]>a?c=b.mUp*(a-b.a[0]):b.a[2]>=a?c=1:b.a[3]>a?c=1-b.mDown*(a-b.a[2]):a>=b.a[3]&&(c=b.lastPoint),c},output_combination:function(a,b,c){var e,f,d=[];for(e=c.sets.length-1;e>=0;e-=1)d[e]=[];for(e=b.length-1;e>=0;e-=1)for(f=b[e].length-1;f>=0;f-=1)b[e][f]>=0&&d[b[e][f]].push(a[e][f]);return d},defuzzification:function(a,b){var e,f,g,h,i,j,k,l,m,n,o,p,q,r,c=0,d=0;for(e=a.length-1;e>=0;e-=1)f=b[e],g=f.a,h=a[e],i=g[3]-g[0],k=g[0],g[0]!==g[1]&&(k+=h/f.mUp),l=g[3],g[2]!==g[3]&&(l-=h/f.mDown),m=0,g[0]!==k&&(m+=(k-g[0])*a[e]/2),k!==l&&(m+=(l-k)*a[e]),l!==g[3]&&(m+=(g[3]-l)*a[e]/2),j=l-k,n=h/3*(i+2*j)/(j+i),p=k+(l-k)/2,q=g[0]+(g[3]-g[0])/2,r=0,0!==p-q&&(r=h/(p-q)),o=q,0!==r&&(o+=n/r),c+=m*o,d+=m;return 0===d?0:c/d},takeMaxOfArraySet:function(a){var c,b=[];for(c=a.length-1;c>=0;c-=1)b[c]=this.takeMaxOfArray(a[c]);return b},takeMaxOfArray:function(a){var c,b=a[0];for(c=1;a.length>c;c+=1)b=a[c]>b?a[c]:b;return b}},a}();\n//\nfunction fuzzy(crisp){\n let ai = new FuzzyLogic();\n let data = flow.get(\"fuzzyset\");\n data.crisp_input =crisp;\n let res = ai.getResult(data);\n // check some bounds:\n if ((res < 12) || ( msg.payload[0] <5))\n return 0;\nreturn res/100; \n}\n\nvar data = flow.get(\"watering\");\nvar weather = flow.get(\"weather\");\n//\nfunction okFuzzy(){\n if (weather.minT <= data.OFFtemperature) return false;\n if (weather.maxRH >= data.OFFRH) return false;\n return true;\n}\n//\nfunction doAdjust(){\n// adjost extremes, if no data, to 20°, 50%\nlet t = (weather.maxT < -30)? 20: weather.maxT;\nlet rh = (weather.minRH > 90)? 50: weather.minRH;\nlet crisp_data = [\n t,\n rh ]; \n \ndata.nowatering = !(okFuzzy());\ndata.adjust = fuzzy(crisp_data); \nflow.set(\"watering\", data);\nreturn data.adjust;\n}\n\nif (msg.topic === \"adjust\"){\n msg.payload = doAdjust();\n node.warn([\"In test \", data, msg]);\n return([msg,null]);\n}\n\nif (msg.topic === \"restart\"){\n let weather = {\n minT: 100,\n maxT: -40,\n minRH: 100,\n maxRH: 0\n } \n flow.set(\"weather\", weather); \n return ([null,{topic:\"save\"}]);\n}\n\n\n","outputs":2,"noerr":0,"initialize":"","finalize":"","x":990,"y":580,"wires":[["221aa24e.28742e","86fc9daa.fb378"],["38e4cc0b.beb5f4"]],"outputLabels":["update","save"]},{"id":"86fc9daa.fb378","type":"subflow:81d3b584.14c888","z":"66e0cde4.b696f4","name":"","env":[{"name":"deviceID","value":"watering_sys","type":"str"},{"name":"propertyDP","value":"4","type":"str"},{"name":"value","value":"@Math.round(msg.payload * 100)","type":"str"}],"x":1410,"y":600,"wires":[["9a343958.4999a8"]]},{"id":"e1cc1c52.293fd","type":"function","z":"66e0cde4.b696f4","name":"dp inheritance & override","func":"// implements some watering_sys dp by inheritace + local override\nconst DEVID = \"watering_sys\";\n// --------------- generic stuff\n\n// get from global.tuyastatus last value\nfunction _getGlobalValue(msx) {\n // access global.tuyastatus\n let tuyastatus = global.get(\"tuyastatus\");\n return (tuyastatus[msx.payload.device][msx.payload.property]);\n}\n\n// test: idDev GET command?\nfunction _isGetCommand(msx) {\n return ((msx.to === DEVID) && (msx.infodp !== undefined) && (msx.payload.value === undefined));\n}\n\n// test: idDev SET command?\nfunction _isSetCommand(msx) {\n return ((msx.to === DEVID) && (msx.infodp !== undefined) && (msx.payload.value !== undefined));\n}\n\n// ---------------------------- locals\nfunction hmTomin(hm) { // local function\n let pieces = hm.split(':');\n return (parseInt(pieces[0]) * 60 + parseInt(pieces[1]));\n}\nfunction string2DayArr(dayStr) {\n let dt = [];\n for (let i = 0; i < 7; i++) {\n if (dayStr.charAt(i) !== '-')\n dt.push(i + 1);\n }\n return dt;\n}\n// -----------------------------------------\n// node outputs:\n// 1 : Fast in\n// 2 : Logging\n// 3 : to chart (ON/OFF)\n// 4 : circulate.days1\n// 5 : circulate.start1\n// 6 : circulate.end1\n// 7 : circulate.active1\n// 8 : circulate.start2\n// 9 : circulate.end2\n// 10 : circulate.active2\n// 11 : circulate.start3\n// 12 : circulate.end3\n// 13 : circulate.active3\n// 14 : data.waterweek\n//\n// the logging message (for 'to logging' IN)\nvar logMsg = {\n \"payload\": {\n \"deviceId\": DEVID,\n \"data\": {\n \"dps\": {}\n // [dp = value]\n }\n }\n};\n\n// =================================================\n\nif (_isSetCommand(msg)) { // test: is SET\n var data = flow.get(\"watering\");\n switch (msg.infodp) {\n // from \"watering timer\", dp='relay'\n case \"1\":\n // sends to level 1 device\n return [{\n payload: {\n device: \"watering timer\",\n property: \"relay\",\n value: msg.payload.value\n }\n }\n ];\n case \"1ans\":\n // for logging, as dp 1\n logMsg.payload.data.dps[\"1\"] = msg.payload.value;\n // side effect: buttons color control\n msg.topic = \"Timer\";\n msg.newval = msg.payload.value;\n // side effect: update chart: hchart = water scaled (0..50)\n let hchart = 33;\n if (data.waterweek !== undefined) {\n hchart = Math.round((data.waterweek * data.adjust * 50) / data.maxwater);\n }\n msg.payload = (msg.payload.value == \"ON\") ? hchart : 0; // for chart\n weather = flow.get(\"weather\");\n if (weather.minT < 100 ) // for refresh title\n msg.title = \"Tmin \"+weather.minT+\"° Tmax \"+weather.maxT+\"°\";\n return [null, logMsg, msg];\n // from \"watering main\", dp='switch'\n case \"2\":\n // sends to level 1 device\n return [{\n payload: {\n device: \"watering main\",\n property: \"switch\",\n value: msg.payload.value\n }\n }\n ];\n case \"2ans\":\n // for logging, as dp 2\n logMsg.payload.data.dps[\"2\"] = msg.payload.value;\n // side effect: buttons color control\n msg.topic = \"Main\";\n msg.newval = msg.payload.value;\n // side effect: if Main OFF, also Timer off\n if (msg.payload.value == \"OFF\") {\n node.send([null, null,{topic : \"Timer\", payload:0, newval:\"OFF\"}]);\n }\n // side effect: update chart: h fix, 10\n msg.payload = (msg.payload.value == \"ON\") ? 10 : 0; // for chart\n weather = flow.get(\"weather\");\n if (weather.minT < 100 ) // for refresh title\n msg.title = \"Tmin \"+weather.minT+\"° Tmax \"+weather.maxT+\"°\";\n return [null, logMsg, msg];\n // from \"watering timer\", dp='schedule'\n case \"42\":\n // sends to level 1 device\n return [{\n payload: {\n device: \"watering timer\",\n property: \"circulate\",\n value: msg.payload.value\n }\n }\n ];\n case \"42ans\":\n // for logging, as dp 42\n logMsg.payload.data.dps[\"42\"] = msg.payload.value;\n // side effect: circulate explode, update var 'data' and UI\n // time[3] used as permanent storage location: decode\n let dx = hmTomin(msg.payload.value[3].on);\n data.adjust = ((dx%100)/100).toString();\n dx = Math.floor(dx/100);\n data.nowatering = (dx % 2 == 0) ? false : true;\n dx = Math.floor(dx/2);\n data.store1 = (dx % 2 == 0) ? false : true;\n dx = Math.floor(dx/2);\n data.store2 = (dx % 2 == 0) ? false : true;\n dx = Math.floor(dx/2);\n data.store3 = (dx == 0) ? false : true;\n \n if(isNaN(data.adjust))\n data.adjust = 0.6; // fallback\n \n data.waterweek = hmTomin(msg.payload.value[3].off);\n if(isNaN(data.waterweek))\n data.waterweek = 600; // fallback\n \n //time[0..2] used as irrigation intervals, used only days[0]\n for (let i = 0; i < 3; i++) {\n data[\"days\" + (i + 1)] = (msg.payload.value[i]) ? string2DayArr(msg.payload.value[1].days) : [];\n data[\"active\" + (i + 1)]= (msg.payload.value[i]) ? msg.payload.value[i].active : false;\n data[\"start\" + (i + 1)] = (msg.payload.value[i]) ? hmTomin(msg.payload.value[i].start) * 60000 : 0;\n data[\"end\" + (i + 1)] = (msg.payload.value[i]) ? hmTomin(msg.payload.value[i].end) * 60000 : 0;\n data[\"on\" + (i + 1)] = (msg.payload.value[i]) ? hmTomin(msg.payload.value[i].on) : 0;\n data[\"off\" + (i + 1)] = (msg.payload.value[i]) ? hmTomin(msg.payload.value[i].off) : 0;\n }\n\n // time calculations\n var daycount = 1;\n if (data.days1 !== undefined)\n daycount = data.days1.length;\n\n var totaldaytime = (data[\"store1\"] ? (data[\"end1\"] - data[\"start1\"]) / 60000 : 0) +\n (data[\"store2\"] ? (data[\"end2\"] - data[\"start2\"]) / 60000 : 0) +\n (data[\"store3\"] ? (data[\"end3\"] - data[\"start3\"]) / 60000 : 0);\n\n var offtime = 0;\n \n if ((daycount > 0) && (totaldaytime > 0) && (data.nowatering === false)) {\n var weekwettime = (data.waterweek * data.adjust) / data.litresminute;\n var wetdaytime = weekwettime / daycount;\n var daycycles = Math.round(wetdaytime / data.ontime);\n offtime = Math.round((totaldaytime - wetdaytime) / daycycles);\n if (offtime < 0) offtime = 0;\n data.wetdaytime = wetdaytime;\n data.offtime = offtime;\n } else {\n // case OFF\n data.wetdaytime = 0;\n data.offtime = offtime;\n data.active1 = false;\n data.active2 = false;\n data.active3 = false;\n }\n // data update\n flow.set(\"watering\", data);\n// node.warn([\"Updated data: \", data]) ; \n // logging and UI update\n return [\n null,\n logMsg,\n null, {\n payload: data.days1\n }, {\n payload: data.start1\n }, {\n payload: data.end1\n }, {\n payload: (data.store1?data.active1:\"none\")\n }, {\n payload: data.start2\n }, {\n payload: data.end2\n }, {\n payload: (data.store2?data.active2:\"none\")\n }, {\n payload: data.start3\n }, {\n payload: data.end3\n }, {\n payload: (data.store3?data.active3:\"none\")\n }, {\n payload: data.waterweek\n }\n ];\n }\n}\n\nif (_isGetCommand(msg)) { // test: is GET\n // returns the value in global.tuyastatus\n switch (msg.infodp) {\n case \"1\":\n case \"2\":\n case \"42\":\n logMsg.payload.data.dps[msg.infodp] = _getGlobalValue(msg);\n return [null, logMsg];\n }\n}\n\nreturn [null];\n","outputs":14,"noerr":0,"initialize":"","finalize":"","x":290,"y":520,"wires":[["a131d6fe.bff008"],["1547be76.90a842"],["8e6204b9.933f58","8bead4b6.de9e98","c30ab453.0d0c18","3de474ea.ca7c1c"],["151ed4ad.6794eb"],["c3a7162d.b98bf8"],["74005ff3.2ae0e"],["e83f263e.366d48"],["170d5f65.7e9f11"],["7ef6e664.a5b0f8"],["f19a9bd9.a8bd48"],["9f4ad3b6.35136"],["f5cf7ffe.7db69"],["6aa907d.0232ef8"],["fff45c98.8d5c4"]],"inputLabels":["fake msg"],"outputLabels":["to Fast IN","to Logging","chart","days","start1","end1","active1","start2","end2","active2","start3","end3","active3","waterweek"]},{"id":"6f3ab810.a4d9d8","type":"function","z":"66e0cde4.b696f4","name":"dp use & process","func":"// implements some watering_sys dp by 'use' + local processing\nconst DEVID = \"watering_sys\";\n// test: idDev SET command?\nfunction _isSetCommand(msx) {\n return ((msx.to === DEVID) && (msx.infodp!== undefined) && (msx.payload.value !== undefined));\n}\n\n// =================================================\nif (_isSetCommand(msg)) { // test: is PUSH?\n switch (msg.infodp) {\n case \"111\":\n // for a button\n return [{\n topic: \"timer\",\n enabled: msg.payload.value\n }\n ];\n case \"112\":\n // for a button\n return [null, {\n topic: \"main\",\n enabled: msg.payload.value\n }\n ];\n case \"201\":\n // store values\n weather = flow.get(\"weather\");\n if (msg.payload.value > weather.maxT )\n weather.maxT = msg.payload.value;\n if (msg.payload.value < weather.minT )\n weather.minT = msg.payload.value;\n flow.set(\"weather\", weather);\n // for the graph\n return [null, null, {\n title: \"Tmin \"+weather.minT+\"° Tmax \"+weather.maxT+\"°\",\n topic: \"Temp\",\n payload: msg.payload.value\n }\n ];\n case \"202\":\n // store values\n weather = flow.get(\"weather\");\n if (msg.payload.value > weather.maxRH )\n weather.maxRH = msg.payload.value;\n if (msg.payload.value < weather.minRH )\n weather.minRH = msg.payload.value;\n flow.set(\"weather\", weather);\n // for the graph\n return [null, null, {\n title: \"Tmin \"+weather.minT+\"° Tmax \"+weather.maxT+\"°\",\n topic: \"RH/2\",\n payload: msg.payload.value / 2\n }\n ];\n }\n}\nreturn [null];\n","outputs":3,"noerr":0,"initialize":"// Code added here will be run once\n// whenever the node is deployed.\nlet weather = {\n minT: 100,\n maxT: -40,\n minRH: 100,\n maxRH: 0\n} \n flow.set(\"weather\", weather);","finalize":"","x":270,"y":660,"wires":[["8bead4b6.de9e98"],["8bead4b6.de9e98"],["8e6204b9.933f58"]],"inputLabels":["fake msg"],"outputLabels":["to timer","to main","to chart"]},{"id":"11e9733f.2d36bd","type":"function","z":"66e0cde4.b696f4","name":"custom dp","func":"// implements some watering_sys dp by inheritace + local override\nconst DEVID = \"watering_sys\";\n\n// --------------- generic stuff\n// test: idDev GET command?\nfunction _isGetCommand(msx) {\n return ((msx.to === DEVID) && (msx.infodp !== undefined) && (msx.payload.value === undefined));\n}\n// test: idDev SET command?\nfunction _isSetCommand(msx) {\n return ((msx.to === DEVID) && (msx.infodp !== undefined) && (msx.payload.value !== undefined));\n}\nfunction _getGlobalValue(mx){\nvar tuyastatus = global.get(\"tuyastatus\");\nif ((tuyastatus !== undefined) && (tuyastatus[mx.payload.device] !== undefined)) { // ok device field\n if (msg.infodp === \"schema\") \n return tuyastatus[mx.payload.device];\n return tuyastatus[mx.payload.device][mx.payload.property];\n }\nreturn undefined;\n}\n// -----------------------------------------\n// node outputs:\n// 1 : Logging\n// 2 : Toggle\n// 3 : dataupdated\n// 4 : slider\n// 5 : reset\n// 6 : store\n// 7 : fast cmds\n\n// the logging message (for 'to logging' IN)\nvar logMsg = {\n \"payload\": {\n \"deviceId\": DEVID,\n \"data\": {\n \"dps\": {}\n // [dp = value]\n }\n }\n};\n// =================================================\n\nif (_isSetCommand(msg)) { // test: is SET\n // for RW/PUSH custom dp\n// node.warn([\"fake SET:\", msg]);\n switch (msg.infodp) {\n case \"3\":\n // trigger: logging + output\n logMsg.payload.data.dps[\"3\"] = \"done\";\n return [logMsg, {\n toggle: true\n }\n ];\n case \"5\":\n // send logging msg + update slider + dataupdate\n logMsg.payload.data.dps[\"5\"] = msg.payload.value;\n return [logMsg, null, {\n topic: \"dataupdate\",\n value: true\n }, {\n \"payload\": msg.payload.value\n }\n ];\n case \"6\":\n logMsg.payload.data.dps[\"6\"] = \"done\";\n return [logMsg, null, null, null, {\n \"payload\": msg.payload.value\n }\n ];\n case \"7\":\n logMsg.payload.data.dps[\"7\"] = \"done\";\n return [logMsg, null, null, null, null, {\n \"payload\": msg.payload.value\n }\n ];\n case \"8\":\n logMsg.payload.data.dps[\"8\"] = \"done\";\n return [logMsg, null, null, null, null, null, {\n \"payload\": msg.payload.value\n }\n ];\n\n // It is really not necessary to implement \"multiple\" in a fake device. \n // In any case, this is an example of how to do it. \n case \"multiple\": \n let newmsg = {\n payload: {\n device : msg.payload.device,\n property : null,\n value: null\n }\n }\n Object.entries(msg.payload.value).forEach(([key, value]) => {\n \n newmsg.payload.property = key,\n newmsg.payload.value = value,\n node.send([null, null, null, null, null, null, null, newmsg]); });\n \n }\n}\nif (_isGetCommand(msg)) { // test: is GET\n // for RW custom dp: returns the value in global.tuyastatus\n// node.warn([\"fake GET:\", msg]);\n switch (msg.infodp) {\n case \"4\":\n case \"5\":\n logMsg.payload.data.dps[msg.infodp] = _getGlobalValue(msg);\n return [logMsg];\n// Maybe it is not useful to implement \"schema\" in a fake device. \n // In any case, this is an example of how to do it (see _getGlobalValue()). \n case \"schema\":\n logMsg.payload.data.dps = _getGlobalValue(msg);\n return [logMsg];\n }\n}\n\nreturn [null];\n","outputs":8,"noerr":0,"initialize":"// Code added here will be run once\n// whenever the node is deployed.\n//\n// Stuff to save data in tuyastatus\n// --------------- locals\nfunction _callJSONMethod(obj, fname, a, b, c, d) {\n // see: https://stackoverflow.com/questions/49125059/how-to-pass-parameters-to-an-eval-based-function-injavascript\n var wrap = s => \"{ return \" + obj[fname] + \" };\" //return the block having function expression\n var func = new Function(wrap(obj[fname])); // ignore ⚠\n return func.call(null).call(obj, a, b, c, d); //invoke the function using arguments\n}\n\n// ---------------- main\nconst DEVID = \"watering_sys\"; // can have friendly name\n// stuff to access data\nvar alld = global.get(\"alldevices\");\nif (alld === undefined)\n return null;\n// find object dp\nvar oDev = _callJSONMethod(alld, \"__getODev\", DEVID, \"fake\");\nvar pmDevName = _callJSONMethod(alld, \"__getDevName\", oDev);\n//\ntuyastatus = global.get(\"tuyastatus\");\nif (tuyastatus === undefined)\n tuyastatus = {};\nif (tuyastatus[pmDevName] === undefined)\n tuyastatus[pmDevName] = {};\n// init\ntuyastatus[pmDevName][\"_connected\"] = true;\nglobal.set(\"tuyastatus\", tuyastatus);\n","finalize":"","x":250,"y":760,"wires":[["44351050.b577b"],["61881e9a.9e2c7"],[],["fff45c98.8d5c4"],["f457f873.6d4038"],["38e4cc0b.beb5f4"],["c790c9ed.3cc7f8"],["1af71950.31eac7"]],"inputLabels":["fake msg"],"outputLabels":["to logging","toggle","update","slider","reset","","",""]},{"id":"3398909b.63312","type":"inject","z":"66e0cde4.b696f4","name":"TEST OFF CMD","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"{\"device\":\"watering_sys\",\"property\":\"switch\",\"value\":\"OFF\"}","payloadType":"json","x":200,"y":920,"wires":[["1782306c.d003c"]]},{"id":"1782306c.d003c","type":"link out","z":"66e0cde4.b696f4","name":"to core.std_cmd","links":["8a1da02d.424ae"],"x":395,"y":1020,"wires":[]},{"id":"76616ca5.815b44","type":"link out","z":"66e0cde4.b696f4","name":"internal: to timer","links":["77dbccc9.ba9b64"],"x":1235,"y":280,"wires":[],"icon":"font-awesome/fa-expand"},{"id":"8eca55de.c3a998","type":"timerswitch","z":"66e0cde4.b696f4","name":"*SET TIME","ontopic":"adjust","offtopic":"restart","onpayload":"ON","offpayload":"OFF","disabled":false,"schedules":[{"on_h":"19","on_m":"20","on_s":"00","off_h":"19","off_m":"20","off_s":"05","valid":true}],"x":970,"y":520,"wires":[["4e356044.8a194"]]},{"id":"6586ab3a.f655b4","type":"switch","z":"66e0cde4.b696f4","name":"*triggers selector","property":"payload.tuyatrigger","propertyType":"msg","rules":[{"t":"eq","v":"1700","vt":"str"},{"t":"eq","v":"1710","vt":"str"},{"t":"eq","v":"1720","vt":"str"}],"checkall":"false","repair":false,"outputs":3,"x":1090,"y":1100,"wires":[["40426aa2.5deb84"],["9a84034c.73364"],["f39d819a.6494c"]],"info":"for my device\ndp = 102 // countdown"},{"id":"750bdf3d.199ce","type":"link in","z":"66e0cde4.b696f4","name":"from triggers.triggers_OUT","links":["9efb8156.f33d4"],"x":915,"y":1100,"wires":[["6586ab3a.f655b4"]]},{"id":"40426aa2.5deb84","type":"subflow:f30e8140.1bed9","z":"66e0cde4.b696f4","name":"1700: main OFF (alarm)","env":[{"name":"device","value":"watering_sys","type":"str"},{"name":"property","value":"switch","type":"str"},{"name":"value","value":"OFF","type":"str"}],"x":1370,"y":1060,"wires":[["881bf4dd.364008"]]},{"id":"9a84034c.73364","type":"subflow:f30e8140.1bed9","z":"66e0cde4.b696f4","name":"1710: TOGGLE ","env":[{"name":"device","value":"watering_sys","type":"str"},{"name":"property","value":"toggle timer","type":"str"},{"name":"value","value":"any","type":"str"}],"x":1400,"y":1100,"wires":[["881bf4dd.364008"]]},{"id":"881bf4dd.364008","type":"link out","z":"66e0cde4.b696f4","name":"to core.std_cmd","links":["8a1da02d.424ae"],"x":1555,"y":1060,"wires":[]},{"id":"1b640c14.dc81c4","type":"subflow:f30e8140.1bed9","z":"66e0cde4.b696f4","name":"cmd timer OFF ","env":[{"name":"device","value":"watering timer","type":"str"},{"name":"property","value":"relay","type":"str"},{"name":"value","value":"OFF","type":"str"}],"x":1400,"y":400,"wires":[["4242c140.fdbb2"]]},{"id":"548cf190.91f5","type":"inject","z":"66e0cde4.b696f4","name":"TEST TOGGLE CMD","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"{\"device\":\"watering_sys\",\"property\":\"toggle timer\",\"value\":\"any\"}","payloadType":"json","x":220,"y":960,"wires":[["1782306c.d003c"]]},{"id":"c9dc690c.9f8a78","type":"inject","z":"66e0cde4.b696f4","name":"TEST RESET CMD","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"{\"device\":\"watering_sys\",\"property\":\"reset\",\"value\":\"any\"}","payloadType":"json","x":210,"y":1000,"wires":[["1782306c.d003c"]]},{"id":"f76deed4.8303a","type":"inject","z":"66e0cde4.b696f4","name":"TEST WATER CMD","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"{\"device\":\"watering_sys\",\"property\":\"waterweek\",\"value\":\"600\"}","payloadType":"json","x":210,"y":1040,"wires":[["1782306c.d003c"]]},{"id":"f39d819a.6494c","type":"subflow:f30e8140.1bed9","z":"66e0cde4.b696f4","name":"1720: main RESET","env":[{"name":"device","value":"watering_sys","type":"str"},{"name":"property","value":"reset","type":"str"},{"name":"value","value":"OFF","type":"str"}],"x":1390,"y":1140,"wires":[["881bf4dd.364008"]]},{"id":"1af71950.31eac7","type":"link out","z":"66e0cde4.b696f4","name":"to core.fast_cmds","links":["c084a743.290b28"],"x":375,"y":840,"wires":[]},{"id":"39e7abb2.3938a4","type":"inject","z":"66e0cde4.b696f4","name":"TEST SAVE","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"{\"device\":\"watering_sys\",\"property\":\"store\",\"value\":\"any\"}","payloadType":"json","x":190,"y":1120,"wires":[["1782306c.d003c"]]},{"id":"5ef61d1.48446e4","type":"inject","z":"66e0cde4.b696f4","name":"TEST RESTORE","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"{\"device\":\"watering_sys\",\"property\":\"circulate\"}","payloadType":"json","x":200,"y":1080,"wires":[["1782306c.d003c"]]},{"id":"dd5ae54c.7ce6b8","type":"inject","z":"66e0cde4.b696f4","name":"TEST SCHEMA","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"{\"device\":\"watering_sys\"}","payloadType":"json","x":200,"y":1160,"wires":[["1782306c.d003c"]]},{"id":"e04a800e.51363","type":"comment","z":"66e0cde4.b696f4","name":"Actions from smartLife app","info":"","x":970,"y":1040,"wires":[]},{"id":"8c7df6be.fba948","type":"comment","z":"66e0cde4.b696f4","name":"local tests","info":"","x":140,"y":880,"wires":[]},{"id":"18f4cd1a.6ed903","type":"comment","z":"66e0cde4.b696f4","name":"UI interface","info":"","x":890,"y":200,"wires":[]},{"id":"82a7c123.f619e","type":"comment","z":"66e0cde4.b696f4","name":"restart","info":"","x":1110,"y":260,"wires":[]},{"id":"8a16e2fd.a07e2","type":"ui_group","name":"Time 1","tab":"58c9bd26.dba2c4","order":3,"disp":true,"width":"3","collapse":true},{"id":"e04eed03.d3291","type":"ui_group","name":"Time 2","tab":"58c9bd26.dba2c4","order":4,"disp":true,"width":"3","collapse":true},{"id":"2e287b5a.1b1814","type":"ui_group","name":"Immediate control","tab":"58c9bd26.dba2c4","order":1,"disp":true,"width":"9","collapse":true},{"id":"31f79248.17674e","type":"ui_group","name":"Time 3","tab":"58c9bd26.dba2c4","order":5,"disp":true,"width":"3","collapse":true},{"id":"194a6329.0b062d","type":"ui_group","name":"week program","tab":"58c9bd26.dba2c4","order":2,"disp":true,"width":"9","collapse":true},{"id":"58c9bd26.dba2c4","type":"ui_tab","name":"Watering","icon":"dashboard","order":1,"disabled":false,"hidden":false}]