Heat Network Designer
From Heatweb Wiki
Jump to navigationJump to search
Link to Online Heat Network Designer
https://hw7.ddns.net/ui/hndesign
Node-RED Flow
[{"id":"875294b.9c94968","type":"http in","z":"cca63413.fec0b8","name":"","url":"/ui/hndesign","method":"get","upload":false,"swaggerDoc":"","x":220,"y":4680,"wires":[["3dc7abcb.50da24"]]},{"id":"3dc7abcb.50da24","type":"template","z":"cca63413.fec0b8","name":"Heat Network Designer","field":"payload.body","fieldType":"msg","format":"handlebars","syntax":"mustache","template":"\n<script src=\"https://bossanova.uk/jspreadsheet/v4/jexcel.js\"></script>\n<link rel=\"stylesheet\" href=\"https://bossanova.uk/jspreadsheet/v4/jexcel.css\" type=\"text/css\" />\n \n<script src=\"https://jsuites.net/v4/jsuites.js\"></script>\n<link rel=\"stylesheet\" href=\"https://jsuites.net/v4/jsuites.css\" type=\"text/css\" />\n\n<script src=\"https://cdnjs.cloudflare.com/ajax/libs/jquery-validate/1.19.1/jquery.validate.min.js\"></script>\n{{!--https://www.sitepoint.com/basic-jquery-form-validation-tutorial/--}}\n\n<script src=\"https://unpkg.com/mustache@latest\"></script>\n\n<style>\n\n.section {\n margin: auto;\n min-height: 20px;\n max-width: 900px;\n padding: 19px;\n margin-bottom: 20px;\n background-color: #f5f5f5;\n border: 1px solid #e3e3e3;\n border-radius: 4px;\n box-shadow: inset 0 1px 1px rgb(0 0 0 / 5%);\n}\n\n.question {\n min-height: 20px;\n padding: 19px;\n margin-bottom: 20px;\n background-color: #a8e5a8;\n border: 1px solid #e3e3e3;\n border-radius: 4px;\n box-shadow: inset 0 1px 1px rgb(0 0 0 / 5%);\n}\n\n\n.qtext {\n \n padding: 6px 0px 12px;\n font-size: 18px;\n font-weight: bold;\n}\n.notes {\n \n padding: 0px 0px 12px;\n font-size: 16px;\n}\n.qunits {\n \n padding: 10px 10px 0px;\n}\n\n.video-wrapper {\n\tposition: relative;\n\tpadding-bottom: 56.25%; /* 16:9 */\n\tpadding-top: 15px;\n\theight: 0;\n\tmargin: 10px 0;\n}\n.video-wrapper iframe {\n\tposition: absolute;\n\ttop: 0;\n\tleft: 0;\n\twidth: 100%;\n\theight: 100%;\n}\n\nform .error {\n color: #ff0000;\n font-size: 16px;\n}\n\n::-webkit-input-placeholder { /* Edge */\n color: yellow;\n}\n\n:-ms-input-placeholder { /* Internet Explorer 10-11 */\n color: yellow;\n}\n\n::placeholder {\n color: yellow;\n}\n\n</style>\n<script type=\"text/javascript\">\n\nvar a = new Date();\nvar qdata;\nvar calculated = {};\n\t\n\t$(document).ready(function(e) {\n \n // $(\"form[ajax=true]\").submit(function(e) {\n \n // //alert(\"submit\");\n \n // e.preventDefault();\n \n \n var form_data = {};\n var form_url = \"/ui/qdata\";\n var form_target = $(this).attr(\"target\") || \"result\";\n var ftarget = \"#\" + form_target;\n var form_method = \"GET\";\n \n // $(\"#loadingimg\").show();\n \n $.ajax({\n url: form_url, \n type: form_method, \n data: form_data, \n cache: false,\n success: function(results){ \n \n qdata = results;\n //qdata = JSON.parse(returnhtml);\n console.log(qdata);\n \n document.getElementById(\"section1\").innerHTML = parseQuestions(qdata); //JSON.stringify(qdata) ;\n \n validform();\n \n } \n }); \n \n // });\n \n \n \n });\n\t\nfunction parseQuestions(qdata) {\n \n console.log(qdata);\n console.log(calculated);\n \n var oot = \"\";\n for (var s in qdata[\"sections\"]) {\n console.log(s);\n console.log(qdata.sections[s]);\n oot += \"<div class='section'><h2>\" + qdata.sections[s].title + \"</h2>\\n\";\n \n for (var q in qdata.sections[s].questions) {\n \n \n var qif = qdata.sections[s].questions[q].if || true;\n var qift = Mustache.render(qif, calculated);\n \n var goq = true;\n try {\n goq = looseJsonParse(qift); \n }\n catch(err) {\n goq = false;\n }\n \n var v=\"\";\n if (calculated[qdata.sections[s].questions[q].id]) { v= calculated[qdata.sections[s].questions[q].id] ; }\n //else if (qdata.sections[s].questions[q].default) { v= qdata.sections[s].questions[q].default ; }\n \n if(goq) {\n \n if (v!==\"\") { oot += '<div class=\"question\" style=\"background-color: #d2efd2;\">'; } else { oot += '<div class=\"question\">'; }\n \n //oot += \"<div class='qtext'>\" + qdata.sections[s].questions[q].title + '</div>';\n oot += '<label for=\"' + qdata.sections[s].questions[q].id + '\" class=\"qtext\">' + (qdata.sections[s].questions[q].question||qdata.sections[s].questions[q].title) + '</label>';\n \n if (qdata.sections[s].questions[q].notes) { oot += '<div class=\"notes\">' + convertLinks(qdata.sections[s].questions[q].notes) + '</div>'; }\n oot += '<table width=\"100%\"><tr><td width=\"80%\"><input id=\"' + qdata.sections[s].questions[q].id + '\" name=\"' + qdata.sections[s].questions[q].id + '\" class=\"form-control\" type=\"text\"';\n \n if (v!==\"\") { oot += ' value=\"' + v + '\"'; }\n // if (calculated[qdata.sections[s].questions[q].id]) { oot += ' value=\"' + calculated[qdata.sections[s].questions[q].id] + '\"'; }\n // else if (qdata.sections[s].questions[q].default) { oot += ' value=\"' + qdata.sections[s].questions[q].default + '\"'; }\n if (qdata.sections[s].questions[q].default) { oot += ' placeholder=\"' + qdata.sections[s].questions[q].default + \" \" + (qdata.sections[s].questions[q].units||\"\") + ' ... please confirm\"'; }\n \n oot += '></td><td valign=\"top\"><span class=\"qunits\">' + (qdata.sections[s].questions[q].units||\"\") + '</span></td></tr></table></div>\\n';\n }\n }\n \n oot += \"</div>\\n\";\n \n }\n \n \n return oot;\n}\n\nfunction looseJsonParse(obj) {\n return Function('\"use strict\";return (' + obj + ')')();\n}\n// console.log(looseJsonParse(\n// \"{a:(4-1), b:function(){}, c:new Date()}\"\n// ))\n\nfunction runCalcs() {\n\n\n \n \n // run through form inputs and store current values into calculated.\n var cells = document.getElementsByClassName(\"form-control\");\n\tvar i;\n\tfor (i = 0; i < cells.length; i++) {\n\t\n\t\tcalcsOut += cells[i].value + \"<br>\\n\";\n \n if (\"\" + cells[i].value !== \"\") { \n \n if (isNaN(cells[i].value)) { \n \n calculated[cells[i].id] = (cells[i].value);\n \n } else {\n \n calculated[cells[i].id] = parseFloat(cells[i].value);\n }\n }\n\t}\n \n var calcsOut = '<table class=\"table\">';\n for (var calc in (qdata[\"calculations\"]||[])) {\n \n console.log(calc);\n console.log(qdata.calculations[calc]);\n var f = qdata.calculations[calc].function;\n var ft = Mustache.render(f, calculated);\n \n calcsOut += \"<tr>\"; //<td>\" + qdata.calculations[calc].id + \"</td>\";\n calcsOut += \"<td>\" + (qdata.calculations[calc].title||qdata.calculations[calc].id) + \"</td>\";\n \n //calcsOut += f + \" = \";\n calcsOut += \"<td><small>\" + ft + \"</small></td>\"; //\"<br>\\n\";\n \n var cv = \"\";\n try {\n cv = looseJsonParse(ft); // looseJsonParse(\"{a:(4-1), b:function(){}, c:new Date()}\");\n }\n catch(err) {\n \n }\n \n \n if (!isNaN(cv)) { cv = parseInt(cv*1000)/1000; }\n \n calcsOut += '<td align=\"right\">' + cv + \"</td>\";\n \n calcsOut += \"<td>\" + (qdata.calculations[calc].units||\"\") + \"</td></tr>\\n\";\n \n calculated[qdata.calculations[calc].id] = cv;\n \n }\n \n calcsOut += \"</table>\\n\";\n \n // for (var calc in calculated) {\n \n \n // }\n \n document.getElementById(\"calcs\").innerHTML = calcsOut; //JSON.stringify(qdata) ;\n \n // rebuild form\n document.getElementById(\"section1\").innerHTML = parseQuestions(qdata); //JSON.stringify(qdata) ;\n validform();\n \n}\n\n\nconst convertLinks = ( input ) => {\n\n let text = input;\n const linksFound = text.match( /(?:www|https?)[^\\s]+/g );\n const aLink = [];\n\n if ( linksFound != null ) {\n\n for ( let i=0; i<linksFound.length; i++ ) {\n let replace = linksFound[i];\n if ( !( linksFound[i].match( /(http(s?)):\\/\\// ) ) ) { replace = 'http://' + linksFound[i] }\n let linkText = replace.split( '/' )[2];\n if ( linkText.substring( 0, 3 ) == 'www' ) { linkText = linkText.replace( 'www.', '' ) }\n if ( linkText.match( /youtu/ ) ) {\n\n let youtubeID = replace.split( '/' ).slice(-1)[0];\n aLink.push( '<div class=\"video-wrapper\"><iframe src=\"https://www.youtube.com/embed/' + youtubeID + '\" frameborder=\"0\" allow=\"accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture\" allowfullscreen></iframe></div>' )\n }\n else if ( linkText.match( /vimeo/ ) ) {\n let vimeoID = replace.split( '/' ).slice(-1)[0];\n aLink.push( '<div class=\"video-wrapper\"><iframe src=\"https://player.vimeo.com/video/' + vimeoID + '\" frameborder=\"0\" webkitallowfullscreen mozallowfullscreen allowfullscreen></iframe></div>' )\n }\n else {\n aLink.push( '<a href=\"' + replace + '\" target=\"_blank\">' + linkText + '</a>' );\n }\n text = text.split( linksFound[i] ).map(item => { return aLink[i].includes('iframe') ? item.trim() : item } ).join( aLink[i] );\n }\n return text;\n\n }\n else {\n return input;\n }\n}\n\nfunction validform() {\n// Initialize form validation on the registration form.\n // It has the name attribute \"registration\"\n $(\"form[name='qform']\").validate({\n // Specify validation rules\n rules: {\n // The key name on the left side is the name attribute\n // of an input field. Validation rules are defined\n // on the right side\n nProperties: {\n required: true,\n digits: true,\n min: 1\n },\n lastname: \"required\",\n email: {\n required: true,\n // Specify that email should be validated\n // by the built-in \"email\" rule\n email: true\n },\n password: {\n required: true,\n minlength: 5\n }\n },\n // Specify validation error messages\n messages: {\n nProperties: \"Please enter a number.\",\n lastname: \"Please enter your lastname\",\n password: {\n required: \"Please provide a password\",\n minlength: \"Your password must be at least 5 characters long\"\n },\n email: \"Please enter a valid email address\"\n },\n // Make sure the form is submitted to the destination defined\n // in the \"action\" attribute of the form when valid\n submitHandler: function(form) {\n //form.submit();\n runCalcs();\n //alert(\"hello\");\n }\n });\n}\n\n</script>\n\n<form action=\"\" name=\"qform\" id=\"qform\">\n<div id=\"section1\"></div>\n\n<div class='section'>\n<button type=\"submit\">Submit</button>\n\n</div>\n\n\n<div class='section'>\n<p><b>Numeric Values and Calculations</b></p>\n<div id=\"calcs\" class=\"notes\"></div>\n\n</div>\n</form>","output":"str","x":490,"y":4740,"wires":[["3b164ec0.e53212","eb87b56c.b4ec18"]],"info":"https://bossanova.uk/jspreadsheet/v4/"},{"id":"66c1eca7.8027f4","type":"inject","z":"cca63413.fec0b8","name":"","topic":"","payload":"{\"title\":\"Heat Network Designer\",\"body\":\"\"}","payloadType":"json","repeat":"","crontab":"","once":true,"onceDelay":0.1,"x":190,"y":4740,"wires":[["3dc7abcb.50da24"]]},{"id":"3b164ec0.e53212","type":"change","z":"cca63413.fec0b8","name":"","rules":[{"t":"set","p":"sections.hndesign","pt":"global","to":"payload","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":800,"y":4740,"wires":[[]]},{"id":"eb87b56c.b4ec18","type":"template","z":"cca63413.fec0b8","name":"","field":"heads","fieldType":"msg","format":"handlebars","syntax":"plain","template":"<!doctype html>\n<html lang=\"en\">\n <head>\n <!-- Required meta tags -->\n <meta charset=\"utf-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1, shrink-to-fit=no\">\n\n <!-- Bootstrap CSS -->\n <link rel=\"stylesheet\" href=\"https://cdn.jsdelivr.net/npm/bootstrap@4.5.3/dist/css/bootstrap.min.css\" integrity=\"sha384-TX8t27EcRE3e/ihU7zmQxVncDAy5uIKz4rEkgIXeMed4M0jlfIDPvg6uqKI2xXr2\" crossorigin=\"anonymous\">\n\n \n <script src=\"https://kit.fontawesome.com/c6b21b7a8f.js\" crossorigin=\"anonymous\"></script>\n\n \n <title>Thermal Integration Heatweb Node</title>\n\n\n <script src=\"https://code.jquery.com/jquery-3.5.1.js\"></script>\n <script src=\"https://cdn.datatables.net/1.10.22/js/jquery.dataTables.min.js\" type=\"text/javascript\"></script>\n <link rel=\"stylesheet\" href=\"https://cdn.datatables.net/1.10.22/css/jquery.dataTables.min.css\">\n\n\n \n <style>\n \n body, textarea, input, select {\n font-size: 18px;\n}\n\nsection {\n border-bottom: 1px solid rgb(255, 255, 255);\n padding: 20px 50px;\n /* height: 100vh; */\n scroll-snap-align: start;\n /* text-align: center; */\n position: relative;\n background: #ffffff;\n font-size: 18px;\n }\n\nsection .table {\n font-size: 16px;\n }\n\ntable.dataTable.compact tbody td {\n \n padding: 2px;\n} \n\nsection .jstree-node {\n font-size: 16px;\n}\n\n.nowrap {\n white-space: nowrap;\n}\n\n@media (max-width: 1480px) {\n\n section {\n \n padding: 20px 20px;\n \n }\n\n}\n\n@media (max-width:480px) {\n\n section {\n \n padding: 10px 8px;\n \n }\n\n}\n\n \n .maxsiz {\n\n width: 100%; \n height: 100vh;\n\n }\n\n .maxsiz2 {\n\n width: 100%; \n height: calc(100vh - 70px);\n\n }\n\n .dot {\n height: 13px;\n width: 13px;\n background-color: rgb(52, 58, 52);\n border-radius: 20%;\n border: 2px;\n border-color: rgb(5, 5, 20);\n display: inline-block;\n }\n\n .thindot {\n height: 13px;\n width: 6px;\n background-color: rgb(52, 58, 52);\n border-radius: 20%;\n border: 2px;\n border-color: rgb(5, 5, 20);\n display: inline-block;\n }\n\n .dotspacer {\n height: 13px;\n width: 2px;\n display: inline-block;\n }\n\n .navReadings {\n /* border-bottom: 1px solid rgb(32, 28, 28); */\n padding: 20px 50px 0px 50px;\n /* background: #6d6d70; */\n font-size: 80%; \n /* font-weight: bold; */\n text-align: left;\n color: #212529; \n }\n\n .navReadings a {\n color: #212529; \n }\n\n \n\n \n </style>","output":"str","x":740,"y":4680,"wires":[["50dc8f01.495d2"]]},{"id":"50dc8f01.495d2","type":"function","z":"cca63413.fec0b8","name":"","func":"msg.payload = msg.heads + \"<section>\" + msg.payload.body + \"</section></body></html>\"\n\nreturn msg;","outputs":1,"noerr":0,"x":930,"y":4680,"wires":[["8ef6666b.cb0d08"]]},{"id":"8ef6666b.cb0d08","type":"http response","z":"cca63413.fec0b8","name":"","statusCode":"","headers":{},"x":1130,"y":4680,"wires":[]},{"id":"c2688db4.30369","type":"http in","z":"cca63413.fec0b8","name":"","url":"/ui/qdata","method":"get","upload":false,"swaggerDoc":"","x":210,"y":4820,"wires":[["b7f7b607.f883f8"]]},{"id":"23e0f808.2b2c58","type":"http response","z":"cca63413.fec0b8","name":"","statusCode":"","headers":{"content-type":"application/json"},"x":730,"y":4820,"wires":[]},{"id":"b7f7b607.f883f8","type":"change","z":"cca63413.fec0b8","name":"Heat Network Design","rules":[{"t":"set","p":"payload","pt":"msg","to":"{\"title\":\"My Form\",\"sections\":[{\"title\":\"General\",\"questions\":[{\"title\":\"Number of properties\",\"question\":\"How many properties are there?\",\"id\":\"nProperties\",\"if\":\"true\",\"required\":true,\"type\":\"integer\",\"notes\":\"The total number of properties connected to the heat network.\",\"units\":\"properties\"},{\"title\":\"Number of buildings\",\"question\":\"How many buildings are there?\",\"id\":\"nBuildings\",\"if\":\"true\",\"required\":true,\"type\":\"integer\",\"notes\":\"The total number of buildings connected to the heat network. See https://hw7.ddns.net/ui/svgexplode?svg=HeatNetwork6.svg&display=g16444,g11206,bulkhm,g14636,landlordload,g16320,g5006,bufferstore,lowgrade,heatpump2,heatpump2-2,qton1,fbyp,hiu,g68266,singlemainpump,backupmainpump,g130841-3,g14610-0,g14636-2,g14610,finestrainer,cwsvcs,g14124,g14362.\",\"units\":\"buildings\"},{\"title\":\"Total number of people\",\"question\":\"How many people are there in total?\",\"id\":\"nPeople\",\"if\":\"true\",\"required\":false,\"type\":\"integer\",\"units\":\"people\"},{\"title\":\"DHW network return temperature\",\"question\":\"What is the network return temperature for DHW?\",\"notes\":\"The temperature fed back from a property to the network when running peak load domestic hot water.\",\"id\":\"tPriRtnDHW\",\"if\":\"true\",\"required\":false,\"default\":19,\"type\":\"number\",\"units\":\"°C\"},{\"title\":\"Central heating output per property\",\"question\":\"What is the central heating output per property?\",\"id\":\"kwCHPP\",\"if\":\"true\",\"required\":false,\"type\":\"number\",\"units\":\"kW\"},{\"title\":\"Central heating network return temperature\",\"question\":\"What is the network return temperature for central heating?\",\"notes\":\"The temperature fed back from a property to the network when running peak load central heating.\",\"id\":\"tPriRtnCH\",\"if\":\"{{kwCHPP}}\",\"required\":false,\"default\":45,\"type\":\"number\",\"units\":\"°C\"},{\"title\":\"Central heating diversity\",\"question\":\"What diversity is applied to central heating output?\",\"id\":\"divCH\",\"if\":\"{{kwCHPP}}\",\"required\":false,\"default\":70,\"type\":\"number\",\"units\":\"%\"},{\"title\":\"Peak network flow temperature\",\"question\":\"What is the peak network flow temperature?\",\"id\":\"tPeak\",\"if\":\"true\",\"required\":true,\"default\":80,\"type\":\"number\",\"units\":\"°C\"}]}],\"calculations\":[{\"id\":\"pPP\",\"title\":\"Average people per property\",\"if\":\"nPeople, nProperties\",\"function\":\"{{nPeople}} / {{nProperties}}\",\"units\":\"people\"},{\"id\":\"kwDHWEst\",\"title\":\"Typical HIU rating for DHW\",\"if\":\"{{pPP}}\",\"function\":\"{{pPP}}<3?37.5:({{pPP}}<5?45:60)\",\"units\":\"kW\"},{\"id\":\"peepDS439\",\"title\":\"People per standard DS439 property\",\"if\":\"true\",\"function\":\"2.3\",\"units\":\"people\"},{\"id\":\"eqPropDS439\",\"title\":\"Equivalent number of DS439 properties\",\"if\":\"{{peepDS439}}\",\"function\":\"{{nPeople}}/{{peepDS439}}\",\"units\":\"properties\"},{\"id\":\"kwDS439\",\"title\":\"Peak DHW Load (DS439)\",\"if\":\"{{eqPropDS439}}\",\"function\":\"Math.ceil((1.19* {{eqPropDS439}}) + (18.8* Math.pow({{eqPropDS439}},0.5)) + 17.6)\",\"units\":\"kW\"},{\"id\":\"vPPEST\",\"title\":\"Volume DHW used per property per day\",\"if\":\"true\",\"function\":\"40\",\"units\":\"litres\"},{\"id\":\"vPHEST\",\"title\":\"Volume DHW used per person per day\",\"if\":\"true\",\"function\":\"28\",\"units\":\"litres\"},{\"id\":\"tRiseEST\",\"title\":\"Average temperature Rise on DHW\",\"if\":\"true\",\"function\":\"35\",\"units\":\"°C\"},{\"id\":\"vDHWEST\",\"title\":\"Volume used per day for DHW\",\"if\":\"true\",\"function\":\"({{nProperties}}*{{vPPEST}})+({{nPeople}}*{{vPHEST}})\",\"units\":\"litres\"},{\"id\":\"kwhDHWEST\",\"title\":\"Energy used per day for DHW\",\"if\":\"true\",\"function\":\"({{vDHWEST}} * 4.2 * {{tRiseEST}}) / 3600\",\"units\":\"kWh\"},{\"id\":\"kwCH\",\"title\":\"Peak central heating load\",\"if\":\"true\",\"function\":\"({{kwCHPP}} * {{divCH}} * {{nProperties}}) / 100\",\"units\":\"kW\"},{\"id\":\"kwPeak\",\"title\":\"Peak load\",\"if\":\"true\",\"function\":\"{{kwCH}}+{{kwDS439}}\",\"units\":\"kW\"},{\"id\":\"kwPeak24\",\"title\":\"Average 24h peak load\",\"if\":\"true\",\"function\":\"{{kwCH}}+({{kwhDHWEST}}/24)\",\"units\":\"kW\"},{\"id\":\"m3hDHWPeak\",\"title\":\"Primary flow rate at peak DHW load\",\"if\":\"true\",\"function\":\"3.6 * {{kwDS439}} / (4.2 * ({{tPeak}}-{{tPriRtnDHW}}))\",\"units\":\"m3/h\"},{\"id\":\"m3hCHPeak\",\"title\":\"Primary flow rate at peak central heating load\",\"if\":\"true\",\"function\":\"3.6 * {{kwCH}} / (4.2 * ({{tPeak}}-{{tPriRtnCH}}))\",\"units\":\"m3/h\"},{\"id\":\"m3hPeak\",\"title\":\"Primary flow rate at peak load\",\"if\":\"true\",\"function\":\"{{m3hDHWPeak}}+{{m3hCHPeak}}\",\"units\":\"m3/h\"},{\"id\":\"tPriRtnPeak\",\"title\":\"Primary return temperature at peak load\",\"if\":\"true\",\"function\":\"{{tPeak}} - ({{kwPeak}} / (4.2 * {{m3hPeak}} / 3.6))\",\"units\":\"°C\"}]}","tot":"json"}],"action":"","property":"","from":"","to":"","reg":false,"x":480,"y":4820,"wires":[["23e0f808.2b2c58"]]}]