Using openHASP with Node-Red

same goal, other design :wink:

grafik

The window consists of 2 pictures, each of them with a open/close picture, so 4 different PNGs.

3 Likes

All nice pictures :grinning:

Looks a bit like Flickr, also only pictures.

Maybe it is possible to show some code, not for copying but for ideas and education.
How did you code the routine.
Just parts of code to show how things are done is already fine.
A piece of code if for me often an eyeopener (the ones I see are mostly more smart then how I do the coding)
Or is it not something for open domain?

Very nice, somewhat brighter than my colour scheme :wink:
I'm also experimenting with hidden elements for controls e.g. changing the central content of the thermostat for settings.

image image
image

1 Like

@smcgann99 - question!

Do you save the pages in the device OR do you build your UI entirely via MQTT?

@yogy This is the jsonl for the my thermostat.

The NR code is somewhat embedded into my other flows at the moment as I'm still very much at the experimentation stage. So it's not that easy to share a working version at the moment.
At some point I plan to make all my "widgets" suitable for public consumption and will share on the forum.

The main point of my posts are to show what can be done with openHasp on these relatively cheap displays. I'm thinking that for me, these will likely become the main method for interacting with NR rather than via a web based dashboard.

 //Thermostat
    { "page": 12, "id": 0, "bg_color": "#000000", "click": 0 }, // page background
    // circle object with gradient fill
    { "id": 1, "obj": "obj", "x": 90, "y": 95, "w": 300, "h": 300, "radius": 300, "border_side": 15, "border_width": 15, "bg_opa": 255, "border_color": "#B26619", "bg_grad_color": "BLACK", "bg_color": "grey", "bg_grad_dir": 1, "bg_grad_stop": 200, "bg_main_stop": 0, "click": 0 },
    // horizontal divider
    { "parentid": 1, "id": 2, "obj": "obj", "x": 35, "y": 165, "w": 230, "h": 2, "radius": 0, "border_side": 1, "border_width": 2, "bg_opa": 0, "border_color": "#737373", "click": 0 },

    // main content parent
    { "id": 10, "obj": "obj", "x": 90, "y": 95, "w": 300, "h": 300, "radius": 300, "bg_opa": 0, "click": 0, "hidden": "false" },
    // main content
    { "parentid": 10, "id": 11, "obj": "label", "x": 0, "y": 30, "w": 300, "h": 30, "text_font": 26, "text_color": textColor, "align": "center", "text": "AMBIENT", "click": 0 },
    { "parentid": 10, "id": 12, "obj": "label", "x": 27, "y": 50, "w": 150, "h": 100, "text_font": 100, "text_color": textColor, "align": "right", "text": "20", "click": 0 },
    { "parentid": 10, "id": 17, "obj": "label", "x": 177, "y": 98, "w": 50, "h": 50, "text_font": 50, "text_color": textColor, "align": "left", "text": ".8", "click": 0 },
    { "parentid": 10, "id": 13, "obj": "label", "x": 50, "y": 180, "w": 100, "h": 30, "text_font": 26, "text_color": textColor, "align": "center", "text": "SET" },
    { "parentid": 10, "id": 14, "obj": "label", "x": 64, "y": 210, "w": 50, "h": 40, "text_font": 40, "text_color": "#B26619", "align": "right", "text": "18" },
    { "parentid": 10, "id": 18, "obj": "label", "x": 114, "y": 227, "w": 25, "h": 20, "text_font": 20, "text_color": "#B26619", "align": "left", "text": ".8" },
    { "parentid": 10, "id": 15, "obj": "label", "x": 150, "y": 180, "w": 100, "h": 30, "text_font": 26, "text_color": textColor, "align": "center", "text": "MODE" },
    { "parentid": 10, "id": 16, "obj": "label", "x": 150, "y": 210, "w": 100, "h": 40, "text_font": 40, "text_color": "green", "align": "center", "text": "\uE493" },

    // set parent
    { "id": 20, "obj": "obj", "x": 90, "y": 95, "w": 300, "h": 300, "radius": 300, "bg_opa": 0, "click": 0, "hidden": "true" },
    { "parentid": 20, "id": 21, "obj": "label", "x": 0, "y": 30, "w": 300, "h": 30, "text_font": 26, "text_color": textColor, "align": "center", "text": "SET", "click": 0 },
    { "parentid": 20, "id": 22, "obj": "label", "x": 27, "y": 50, "w": 150, "h": 100, "text_font": 100, "text_color": textColor, "align": "right", "text": "20", "click": 0 },
    { "parentid": 20, "id": 25, "obj": "label", "x": 177, "y": 98, "w": 50, "h": 50, "text_font": 50, "text_color": textColor, "align": "left", "text": ".8", "click": 0 },
    { "parentid": 20, "id": 23, "obj": "label", "x": 50, "y": 180, "w": 100, "h": 60, "text_font": 46, "text_color": textColor, "align": "center", "text": "\uE045" },
    { "parentid": 20, "id": 24, "obj": "label", "x": 150, "y": 180, "w": 100, "h": 60, "text_font": 46, "text_color": textColor, "align": "center", "text": "\uE05D" },

    // mode parent
    { "id": 30, "obj": "obj", "x": 90, "y": 95, "w": 300, "h": 300, "radius": 300, "bg_opa": 0, "click": 0, "hidden": "true" },
    { "parentid": 30, "id": 31, "obj": "label", "x": 0, "y": 30, "w": 300, "h": 30, "text_font": 26, "text_color": textColor, "align": "center", "text": "MODE", "click": 0 },
    { "parentid": 30, "id": 32, "obj": "label", "x": 0, "y": 50, "w": 300, "h": 100, "text_font": 100, "text_color": "green", "align": "center", "text": "\uE493", "click": 0 },
    { "parentid": 30, "id": 33, "obj": "label", "x": 50, "y": 180, "w": 100, "h": 60, "text_font": 46, "text_color": textColor, "align": "center", "text": "\uE04D" },
    { "parentid": 30, "id": 34, "obj": "label", "x": 150, "y": 180, "w": 100, "h": 60, "text_font": 46, "text_color": textColor, "align": "center", "text": "\uE054" },

    //hidden close button (drawn last to be on top layer)
    { "parentid": 1, "id": 3, "obj": "obj", "x": 0, "y": 0, "w": 300, "h": 160, "bg_opa": 0, "border_side": 0, }

@Steve-Mcl It can be done either way, but since I plan to have multiple screens around the house eventually I'm designing it so that everything comes from NR.

This means all I have to do with a new screen is connect to Wi-Fi and set MQTT topic.
As soon as it comes online everything needed is sent via MQTT to set up all the page content.

Initial download is very quick since the jsonl is not much data.

There after my flows handle all the interactions.

Yeah, i know. I was more curious about your approach specifically.

For example, how do you get the images to the device?

I found the docs to be somewhat disjointed and it took a lot of play to make something usable.

I use the file editor of the web interface and upload the initial jsonl file and the images.

grafik

@smcgann99 The idle issue is solved for me with the latest build of the rc9.

1 Like

So far I'm deliberately avoiding using any additional content like images, as I want to see how much can be done just using the standard capabilities. I'm slowly getting my head around some of the tricks to building complex widgets by layering and tweaking standard objects.

The icons in my stat are just unicode characters in the standard built in font. (so text not images) This will also make it easier to share the widgets.
Additionally just sending a bit of jsonl is much more efficient and uses hardly any memory on the device.

Although images can easily be hosted on NR, and then displayed by using a source tag, as an alternative to storing them on the device.

I'm not sure if you know but you can also use FTP to upload files to the screens storage ?

If / probably when I need to have images on the plate I plan to create a simple FTP flow.

So when device comes online, it can check if the images / other content already exists and if not upload them.

This look as a good approach

My JSONL file for THIS page is:
Pay attention, that this page is for 480x480 displays like the GS T3E. My Lanbon L8 has only 320x240, so that page would not fit there.

{"page":2,"comment":"--------------------------------"}

{"id":99,"obj":"img","src":"L:/back-heizung.png","auto_size":true,"x":0,"y":0,"w":480,"h":480}

{"id":1,"obj":"label","x":10,"y":5,"h":100,"w":200,"text":"00:00","text_font":64,"align":0,"text_color":"#FFFFFF"}

{"id":5,"obj":"label","x":370,"y":90,"h":60,"w":60,"text":"\uE58C","text_font":32,"align":0,"text_color":"#66FFFF"}
{"id":6,"obj":"label","x":390,"y":90,"h":60,"w":80,"text":"","text_font":32,"align":"right","text_color":"#66FFFF"}

{"id":10,"obj":"label","x":260,"y":15,"h":80,"w":50,"text":"","text_font":48,"align":1}
{"id":11,"obj":"label","x":290,"y":5,"h":80,"w":120,"text":"0","align":2,"text_font":64,"text_color":"#FFFFFF"}
{"id":12,"obj":"label","x":440,"y":5,"h":80,"w":40,"text":"C","text_font":64,"align":0,"text_color":"#FFFFFF"}
{"id":13,"obj":"label","x":420,"y":2,"h":80,"w":50,"text":"o","text_font":32,"align":0,"text_color":"#FFFFFF"}

{"id":2,"obj":"obj","x":300,"y":5,"h":80,"w":180,"click":true,"opacity":0,"hidden":false}


{"id":20,"obj":"slider","x":70,"y":270,"w":270,"h":30,"min":-15,"max":40,"bg_grad_color": "#FF0000","bg_color":"#0000FF","bg_grad_dir":2}
{"id":21,"obj":"label","x":10,"y":260,"h":50,"w":50,"text":"\uE50F","text_font":32,"align":0,"text_color":"#5F33FF"}

{"id":22,"obj":"bar","x":70,"y":320,"w":270,"h":20,"min":-15,"max":40}
{"id":23,"obj":"label","x":10,"y":310,"h":50,"w":50,"text":"\uE438","text_font":32,"align":0,"text_color":"#FFFF22"}

{"id":24,"obj":"bar","x":70,"y":370,"w":270,"h":20,"min":-15,"max":40}
{"id":25,"obj":"label","x":10,"y":360,"h":50,"w":50,"text":"\uE6A1","text_font":32,"align":0,"text_color":"#00FFFF"}

{"id":30,"obj":"img","src":"L:/1flz.png","auto_size":true,"x":20,"y":90,"w":100,"h":157}
{"id":31,"obj":"img","src":"L:/1frz.png","auto_size":true,"x":120,"y":90,"w":100,"h":157}

{"id":40,"obj":"btn","x":375,"y":160,"w":100,"h":55,"toggle":true,"text":"\uE210","text_color01":"yellow","text_font":32,"align":1}
{"id":42,"obj":"btn","x":375,"y":240,"w":100,"h":55,"toggle":false,"text":"22 C","text_color01":"yellow","text_font":32,"align":1}
{"id":44,"obj":"btn","x":375,"y":310,"w":100,"h":55,"toggle":false,"text":"14 C","text_color01":"yellow","text_font":32,"align":1}
{"id":46,"obj":"btn","x":375,"y":380,"w":100,"h":55,"toggle":false,"text":" 5 C","text_color01":"yellow","text_font":32,"align":1}

And in Node-RED:

[{"id":"5a1cda5786c898fd","type":"tab","label":"Forum","disabled":false,"info":"","env":[]},{"id":"f0e725733beab2fb","type":"inject","z":"5a1cda5786c898fd","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"25","crontab":"","once":true,"onceDelay":"6","topic":"","payload":"","payloadType":"date","x":390,"y":260,"wires":[["aa5eb93e803a09cd"]]},{"id":"aa5eb93e803a09cd","type":"moment","z":"5a1cda5786c898fd","name":"","topic":"","input":"","inputType":"msg","inTz":"Europe/Berlin","adjAmount":0,"adjType":"days","adjDir":"add","format":"HH:mm","locale":"de-DE","output":"","outputType":"msg","outTz":"Europe/Berlin","x":620,"y":260,"wires":[["22a84b2c78686fdb"]]},{"id":"57ae62428292ec3d","type":"comment","z":"5a1cda5786c898fd","name":"temperature","info":"","x":170,"y":100,"wires":[]},{"id":"834cf450bdf4f80e","type":"comment","z":"5a1cda5786c898fd","name":"time","info":"","x":150,"y":260,"wires":[]},{"id":"2bb484acf9c23744","type":"change","z":"5a1cda5786c898fd","name":"","rules":[{"t":"set","p":"payload","pt":"msg","to":"$round(payload, 0)","tot":"jsonata"}],"action":"","property":"","from":"","to":"","reg":false,"x":590,"y":100,"wires":[["e22874e27d4b7496"]]},{"id":"22a84b2c78686fdb","type":"mqtt out","z":"5a1cda5786c898fd","name":"","topic":"hasp/plate3/command/p2b1.text","qos":"","retain":"","respTopic":"","contentType":"","userProps":"","correl":"","expiry":"","broker":"7f211795.773398","x":890,"y":260,"wires":[]},{"id":"e22874e27d4b7496","type":"mqtt out","z":"5a1cda5786c898fd","name":"","topic":"hasp/plate3/command/p2b11.text","qos":"","retain":"","respTopic":"","contentType":"","userProps":"","correl":"","expiry":"","broker":"7f211795.773398","x":900,"y":100,"wires":[]},{"id":"75b86a8a033e5f91","type":"inject","z":"5a1cda5786c898fd","name":"","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"15.5","payloadType":"num","x":350,"y":100,"wires":[["2bb484acf9c23744"]]},{"id":"799eb6476d4a2c45","type":"mqtt in","z":"5a1cda5786c898fd","name":"","topic":"hasp/plate3/state/p1b22","qos":"2","datatype":"auto-detect","broker":"7f211795.773398","nl":false,"rap":true,"rh":0,"inputs":0,"x":230,"y":480,"wires":[["8429211e335cca5c"]]},{"id":"4ddf8e46fdfb8d8a","type":"mqtt in","z":"5a1cda5786c898fd","name":"","topic":"hasp/plate3/state/p1b23","qos":"2","datatype":"auto-detect","broker":"7f211795.773398","nl":false,"rap":true,"rh":0,"inputs":0,"x":230,"y":520,"wires":[["8429211e335cca5c"]]},{"id":"a7063bde153cd72f","type":"mqtt in","z":"5a1cda5786c898fd","name":"","topic":"hasp/plate3/state/p1b20","qos":"2","datatype":"auto-detect","broker":"7f211795.773398","nl":false,"rap":true,"rh":0,"inputs":0,"x":230,"y":400,"wires":[["8429211e335cca5c"]]},{"id":"133e0bbc47f4c24c","type":"mqtt in","z":"5a1cda5786c898fd","name":"","topic":"hasp/plate3/state/p1b21","qos":"2","datatype":"auto-detect","broker":"7f211795.773398","nl":false,"rap":true,"rh":0,"inputs":0,"x":230,"y":440,"wires":[["8429211e335cca5c"]]},{"id":"8429211e335cca5c","type":"debug","z":"5a1cda5786c898fd","name":"debug 382","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":630,"y":440,"wires":[]},{"id":"15925f7cfc763294","type":"comment","z":"5a1cda5786c898fd","name":"some buttons on page 1","info":"","x":130,"y":360,"wires":[]},{"id":"c9b44c8e8befb64d","type":"mqtt out","z":"5a1cda5786c898fd","name":"","topic":"hasp/plate1/command/output27","qos":"","retain":"","respTopic":"","contentType":"","userProps":"","correl":"","expiry":"","broker":"7f211795.773398","x":1010,"y":640,"wires":[]},{"id":"9d455c9df0dd320a","type":"mqtt in","z":"5a1cda5786c898fd","name":"","topic":"hasp/plate1/state/p2b1","qos":"2","datatype":"auto-detect","broker":"7f211795.773398","nl":false,"rap":true,"rh":0,"inputs":0,"x":180,"y":700,"wires":[["9bebd844cf20a57c"]]},{"id":"9bebd844cf20a57c","type":"function","z":"5a1cda5786c898fd","name":"","func":"\nif (msg.payload.event == \"up\")\n  { if (msg.payload.val == 0)\n    {msg.payload.state = \"off\";\n    }\n    else\n    {msg.payload.state = \"on\";\n    }\n    \n  }\n\nreturn msg;\n\n","outputs":1,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":520,"y":640,"wires":[["a1d58440d3abd700"]]},{"id":"a1d58440d3abd700","type":"json","z":"5a1cda5786c898fd","name":"","property":"payload","action":"","pretty":false,"x":750,"y":640,"wires":[["c9b44c8e8befb64d"]]},{"id":"3fc03d0fd7d122d2","type":"mqtt out","z":"5a1cda5786c898fd","name":"","topic":"hasp/plate1/command/p1b1.val","qos":"","retain":"","respTopic":"","contentType":"","userProps":"","correl":"","expiry":"","broker":"7f211795.773398","x":810,"y":720,"wires":[]},{"id":"448f32b41ff62ef7","type":"mqtt in","z":"5a1cda5786c898fd","name":"","topic":"hasp/plate1/state/p1b1","qos":"2","datatype":"auto-detect","broker":"7f211795.773398","nl":false,"rap":true,"rh":0,"inputs":0,"x":180,"y":640,"wires":[["9bebd844cf20a57c"]]},{"id":"fceb501004e570c8","type":"mqtt out","z":"5a1cda5786c898fd","name":"","topic":"hasp/plate1/command/p2b1.val","qos":"","retain":"","respTopic":"","contentType":"","userProps":"","correl":"","expiry":"","broker":"7f211795.773398","x":810,"y":780,"wires":[]},{"id":"a557fccdc089d553","type":"change","z":"5a1cda5786c898fd","name":"","rules":[{"t":"change","p":"payload.state","pt":"msg","from":"on","fromt":"str","to":"1","tot":"num"},{"t":"change","p":"payload.state","pt":"msg","from":"off","fromt":"str","to":"0","tot":"num"},{"t":"set","p":"butt","pt":"flow","to":"payload.state","tot":"msg"},{"t":"set","p":"payload","pt":"msg","to":"butt","tot":"flow"}],"action":"","property":"","from":"","to":"","reg":false,"x":480,"y":780,"wires":[["fceb501004e570c8","3fc03d0fd7d122d2"]]},{"id":"bea0355b8bfa42e1","type":"mqtt in","z":"5a1cda5786c898fd","name":"","topic":"hasp/plate1/state/output27","qos":"2","datatype":"auto-detect","broker":"7f211795.773398","nl":false,"rap":true,"rh":0,"inputs":0,"x":190,"y":780,"wires":[["a557fccdc089d553"]]},{"id":"6364a3e51e5d7dd2","type":"comment","z":"5a1cda5786c898fd","name":"examples with switching one output with two buttons on two pages from my Lanbon L8 plate","info":"","x":360,"y":600,"wires":[]},{"id":"1ba101fab6cc8732","type":"comment","z":"5a1cda5786c898fd","name":"updating the button state, when output is changed somewhere else","info":"","x":300,"y":820,"wires":[]},{"id":"7f211795.773398","type":"mqtt-broker","name":"Mosquitto","broker":"localhost","port":"1883","clientid":"","usetls":false,"compatmode":false,"keepalive":"60","cleansession":true,"birthTopic":"","birthQos":"0","birthPayload":"","closeTopic":"","closeQos":"0","closePayload":"","willTopic":"","willQos":"0","willPayload":""}]

picture of code above:

2 Likes

Yes, I saw it in the configuration screen, but I have no requirement for FTP.

@yogy This is "extracted" from my flows for you see how the thermostat logic works.
I cannot easily test it at the moment but feel free to have a play with it, it may give you some ideas :wink:

[{"id":"141215bdcc5dac6a","type":"group","z":"fb8e012451d1e803","name":"Hasp Thermostat","style":{"fill":"#ffcf3f","label":true,"label-position":"n","color":"#001f60","fill-opacity":"0.47"},"nodes":["5e97dc571319a7b9","db35f753b61dde8a","145c5dca233af984","20771bc8c7433df8","14ec9e33b2f74b20","6f4a18fbb1ab256e","bc3899b00dfc1639","949801b38d9438fa","2b3afc242522f593","3de70dc003a542b4","02f48e6d8567da9f"],"x":734,"y":64,"w":1092,"h":217},{"id":"5e97dc571319a7b9","type":"function","z":"fb8e012451d1e803","g":"141215bdcc5dac6a","name":"Handle Thermostat input","func":"const topicParts = msg.topic.split(\"/\");\nmsg.topic = `hasp/${topicParts[1]}/command/json`\nconst buttonId = topicParts[3]\nif (!buttonId.includes(\"p12\")) { return }                                       // only want input from page 12\n\nconst setPressed = [\"p12b14\", \"p12b15\", \"p12b16\"].includes(buttonId);           // set area pressed ?\nconst modePressed = [\"p12b17\", \"p12b18\"].includes(buttonId);                    // mode area pressed ?\nconst modeName = [\"auto\", \"user\", \"off\"]                                        // list of valid modes\nconst setPointStep = 0.2                                                        // amount to change setpoint by on each click\n\nlet currentSet = parseFloat(global.get(\"nest1_target_temperature\").toFixed(1))  //point to source of setpoint value    \nlet currentMode = global.get(\"heating_current_mode\")                            //point to source of mode value\nlet modeIndex = modeName.indexOf(currentMode);\nif (modeIndex === -1) { node.warn(\"Invalid mode\"); return };\n\n// Show set or mode menu\nif (setPressed || modePressed) {\n    msg.payload = [`p12b10.hidden=true`, `p12b${setPressed ? 20 : 30}.hidden=false`]\n    node.send([msg, msg])                                                       // update page and trigger data refresh\n}\n\n// Show ambient display (hide others)\nif (buttonId === \"p12b3\") {\n    msg.payload = [\"p12b20.hidden=true\", \"p12b30.hidden=true\", \"p12b10.hidden=false\"]\n    node.send([msg, msg])                                                       // update page and trigger data refresh\n}\n\n// Setpoint DOWN or UP button\nif (buttonId === \"p12b24\" || buttonId === \"p12b25\") {\n    const isUpButton = buttonId === \"p12b25\";\n    currentSet = parseFloat((currentSet + (isUpButton ? setPointStep : -setPointStep)).toFixed(1));\n    if (currentSet < 16) { currentSet = 16; }\n    else if (currentSet > 23) { currentSet = 23; }\n    modeIndex = 1;                                                              // Manual adjustment, so set mode to user\n    global.set(\"heating_user_started\", Date.now());                             // used to track time of override, so mode could be set back to auto\n    node.send([null, msg])                                                      // Trigger data refresh\n}\n\n// Mode prev or next button\nif (buttonId === \"p12b33\" || buttonId === \"p12b34\") {\n    modeIndex += (buttonId === \"p12b34\") ? 1 : -1;\n    if (modeIndex < 0) { modeIndex = modeName.length - 1; }\n    else if (modeIndex > modeName.length - 1) { modeIndex = 0; }\n    node.send([null, msg])                                                     // Trigger data refresh\n}\n\nglobal.set(\"nest1_target_temperature\", currentSet);                            //point to source of setpoint value  \nglobal.set(\"heating_current_mode\", modeName[modeIndex]);                       //point to source of mode value\nnode.status({ text: `Last mode change ${modeName[modeIndex]}` });\nreturn;\n","outputs":2,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":1360,"y":240,"wires":[["14ec9e33b2f74b20","db35f753b61dde8a"],["145c5dca233af984"]],"outputLabels":["MQTT","Refresh Data"],"info":"const topicParts = msg.topic.split(\"/\");\r\nconst page = topicParts[1];\r\nconst buttonId = topicParts[3];\r\n\r\nmsg.topic = `hasp/${page}/command/json`;\r\n\r\nif (!buttonId.includes(\"p12\")) {\r\n    return; // Only want input from page 12\r\n}\r\n\r\nconst setPressed = [\"p12b14\", \"p12b15\", \"p12b16\"].includes(buttonId);\r\nconst modePressed = [\"p12b17\", \"p12b18\"].includes(buttonId);\r\nconst modeName = [\"auto\", \"user\", \"off\"];\r\nconst setPointStep = 0.2;\r\n\r\nlet currentSet = parseFloat(global.get(\"nest1_target_temperature\").toFixed(1));\r\nlet currentMode = global.get(\"heating_current_mode\");\r\nlet modeIndex = modeName.indexOf(currentMode);\r\n\r\n// show set menu\r\nif (setPressed) {\r\n    msg.payload = [\"p12b10.hidden=true\", \"p12b20.hidden=false\"]\r\n    node.send([msg, msg]) // update page and trigger data refresh\r\n}\r\n\r\n// show mode menu\r\nif (modePressed) {\r\n    msg.payload = [\"p12b10.hidden=true\", \"p12b30.hidden=false\"]\r\n    node.send([msg, msg]) // update page and trigger data refresh\r\n}\r\n\r\n// show ambient display (hide others)\r\nif (topicParts[3] === \"p12b3\") {\r\n    msg.payload = [\"p12b20.hidden=true\", \"p12b30.hidden=true\", \"p12b10.hidden=false\"]\r\n    node.send([msg, msg]) // update page and trigger data refresh\r\n}\r\n\r\n// setpoint DOWN button\r\nif (topicParts[3] === \"p12b24\") {\r\n    currentSet = parseFloat((currentSet - setPointStep).toFixed(1));\r\n    if (currentSet < 16) { currentSet = 16 }\r\n    modeIndex = 1 // manual adjustment so set mode to user\r\n    global.set(\"heating_user_started\", Date.now()) // manual adjustment so reset countdown to auto mode\r\n    node.send([null, msg]) // trigger data refresh\r\n}\r\n\r\n// setpoint UP button\r\nif (topicParts[3] === \"p12b25\") {\r\n    currentSet = parseFloat((currentSet + setPointStep).toFixed(1));\r\n    if (currentSet > 23) { currentSet = 23 }\r\n    modeIndex = 1 // manual adjustment so set mode to user\r\n    global.set(\"heating_user_started\", Date.now()) // manual adjustment so reset countdown to auto mode\r\n    node.send([null, msg]) // trigger data refresh\r\n}\r\n\r\n// mode prev button\r\nif (topicParts[3] === \"p12b33\") {\r\n    modeIndex -= 1\r\n    if (modeIndex < 0) {\r\n        modeIndex = modeName.length - 1;\r\n    }\r\n    node.send([null, msg]) // trigger data refresh\r\n}\r\n\r\n// mode next button\r\nif (topicParts[3] === \"p12b34\") {\r\n    modeIndex += 1\r\n    if (modeIndex > modeName.length - 1) {\r\n        modeIndex = 0\r\n    }\r\n    node.send([null, msg]) // trigger data refresh\r\n}\r\n\r\n\r\nglobal.set(\"nest1_target_temperature\", currentSet)\r\nglobal.set(\"heating_current_mode\", modeName[modeIndex])\r\nnode.status({ text: `last mode change ${modeName[modeIndex]}` });\r\nreturn;\r\n"},{"id":"db35f753b61dde8a","type":"function","z":"fb8e012451d1e803","g":"141215bdcc5dac6a","name":"Update Thermostat","func":"msg.topic = \"hasp/broadcast/command/json\"\n\nlet currentSet = parseFloat(global.get(\"nest1_target_temperature\").toFixed(1))                  // point to source of setpoint value\nconst currentMode = global.get(\"heating_current_mode\")                                          // point to source of mode value\nconst modeIcons = { auto: \"#008000 \\uE493#\", user: \"#B26619 \\uE004#\", off: \"#FF0000 \\uE425#\" }  // define mode icons and colours\nconst modeIcon = modeIcons[currentMode]\nlet currentAmbient = parseFloat(global.get(\"nest1_ambient_temperature\").toFixed(1))             // point to source of ambient value\nconst currentBolier = global.get(\"heating_boiler\") === \"off\" ? \"#393939\" : \"#B26619\"            // orange ring when calling for heat\n\nfunction splitFloat(floatNumber) {\n    const integerValue = Math.floor(floatNumber)\n    const decimalValue = Math.round((floatNumber - integerValue) * 10)\n    return { integer: integerValue, decimal: decimalValue }\n}\n\ncurrentSet = splitFloat(currentSet)\ncurrentAmbient = splitFloat(currentAmbient)\n\nmsg.payload = [\n    `p12b18.text=${modeIcon}`, `p12b32.text=${modeIcon}`, `p12b1.border_color=${currentBolier}`, `p12b22.text=${currentSet.integer}`, `p12b23.text=.${currentSet.decimal}`,\n    `p12b15.text=${currentSet.integer}`, `p12b16.text=.${currentSet.decimal}`, `p12b12.text=${currentAmbient.integer}`,`p12b13.text=.${currentAmbient.decimal}`\n]\n\nnode.status({ text: `Mode:${currentMode}  Set:${currentSet.integer}.${currentSet.decimal}  Amb:${currentAmbient.integer}.${currentAmbient.decimal}` });\nreturn msg;\n\n","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":1340,"y":180,"wires":[["14ec9e33b2f74b20"]]},{"id":"145c5dca233af984","type":"link out","z":"fb8e012451d1e803","g":"141215bdcc5dac6a","name":"trigger - heating changed via hasp","mode":"link","links":[],"x":1660,"y":240,"wires":[],"l":true},{"id":"20771bc8c7433df8","type":"link in","z":"fb8e012451d1e803","g":"141215bdcc5dac6a","name":"trigger from heating flow to update Hasp Stat","links":[],"x":935,"y":180,"wires":[["db35f753b61dde8a"]],"l":true},{"id":"14ec9e33b2f74b20","type":"mqtt out","z":"fb8e012451d1e803","g":"141215bdcc5dac6a","name":"","topic":"","qos":"","retain":"","respTopic":"","contentType":"","userProps":"","correl":"","expiry":"","broker":"","x":1585,"y":180,"wires":[]},{"id":"6f4a18fbb1ab256e","type":"switch","z":"fb8e012451d1e803","g":"141215bdcc5dac6a","name":"By type","property":"payload.event","propertyType":"msg","rules":[{"t":"eq","v":"up","vt":"str"}],"checkall":"true","repair":false,"outputs":1,"x":1160,"y":240,"wires":[["5e97dc571319a7b9"]]},{"id":"bc3899b00dfc1639","type":"switch","z":"fb8e012451d1e803","g":"141215bdcc5dac6a","name":"Events only","property":"payload","propertyType":"msg","rules":[{"t":"hask","v":"event","vt":"str"}],"checkall":"true","repair":false,"outputs":1,"x":1000,"y":240,"wires":[["6f4a18fbb1ab256e"]]},{"id":"949801b38d9438fa","type":"mqtt in","z":"fb8e012451d1e803","g":"141215bdcc5dac6a","name":"","topic":"hasp/+/state/#","qos":"2","datatype":"auto-detect","broker":"","nl":false,"rap":true,"rh":0,"inputs":0,"x":840,"y":240,"wires":[["bc3899b00dfc1639"]]},{"id":"2b3afc242522f593","type":"function","z":"fb8e012451d1e803","g":"141215bdcc5dac6a","name":"Send jsonl Chunks","func":"// Limit msg.payloads to 2048 bytes to fit with OpenHasp buffer size\n\nlet message = msg.payload;\nlet topicParts = msg.topic.split(\"/\");\nlet topic = `hasp/${topicParts[1]}/command/jsonl`;\n\n// send in chunks\nconst maxLength = 2000;\nlet payload = \"\";\n\nfor (let index = 0; index < message.length; index++) {\n    const jsonString = JSON.stringify(message[index]) + \"\\n\";\n\n    if (payload.length + jsonString.length <= maxLength) {\n        payload += jsonString;\n    } else {\n        sendPayload(topic, payload);\n        payload = jsonString;\n    }\n}\n\n// send any remaining payload\nsendPayload(topic, payload);\n\nfunction sendPayload(topic, payload) {\n    if (payload.length > 0) {\n        node.send({\n            \"topic\": topic,\n            \"payload\": payload\n        });\n    }\n}\n\n","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":1335,"y":105,"wires":[["14ec9e33b2f74b20"]],"info":"let message=msg.payload\r\nlet topicParts = msg.topic.split(\"/\")\r\nlet topic = `hasp/${topicParts[1]}/command/jsonl`\r\n\r\n// send in chunks\r\nlet payload = \"\";\r\nconst maxLength = 2048;\r\nfor (let index = 0; index < message.length; index++) {\r\n    const jsonString = JSON.stringify(message[index]) + \"\\n\";\r\n    \r\n    if (payload.length + jsonString.length <= maxLength) {\r\n        payload += jsonString;\r\n    } else {\r\n        node.send({\r\n            \"topic\": topic,\r\n            \"payload\": payload\r\n        });\r\n        payload = jsonString;\r\n    }\r\n}\r\n// anything remaining to send?\r\nif (payload.length > 0) {\r\n    node.send({\r\n        \"topic\": topic,\r\n        \"payload\": payload\r\n    });\r\n}"},{"id":"3de70dc003a542b4","type":"function","z":"fb8e012451d1e803","g":"141215bdcc5dac6a","name":"scalable thermostat","func":"msg.topic = \"hasp/1/command/jsonl\" // change to suit\n\nconst text_color = \"#bfbfbf\"\n\nconst diameter = 300; // change to required diameter\nconst x = 0 // coordinates of top left\nconst y = 0 // coordinates of top left\nconst scale = diameter / 300; // leave as 300 - original design was 300 x 300\n\nmsg.payload = [\n    // page background\n    { \"page\": 12, \"id\": 0, \"bg_color\": \"#000000\", \"click\": 0 },\n    // circle object with gradient fill\n    { \"id\": 1, \"obj\": \"obj\", \"x\": 0, \"y\": 0, \"w\": 300 * scale, \"h\": 300 * scale, \"radius\": 300 * scale, \"border_side\": 15, \"border_width\": 15 * scale, \"bg_opa\": 255, \"border_color\": \"#B26619\", \"bg_grad_color\": \"BLACK\", \"bg_color\": \"grey\", \"bg_grad_dir\": 1, \"bg_grad_stop\": 200, \"bg_main_stop\": 0, \"click\": 0 },\n    // horizontal divider\n    { \"parentid\": 1, \"id\": 2, \"obj\": \"obj\", \"x\": 35 * scale, \"y\": 165 * scale, \"w\": 230 * scale, \"h\": 2 * scale, \"radius\": 0, \"border_side\": 1, \"border_width\": 2 * scale, \"bg_opa\": 0, \"border_color\": text_color, \"click\": 0 },\n\n    // main content parent\n    { \"id\": 10, \"parentid\": 1, \"obj\": \"obj\", \"x\": 0, \"y\": 0, \"w\": 300 * scale, \"h\": 300 * scale, \"radius\": 300 * scale, \"bg_opa\": 0, \"click\": 0, \"hidden\": \"false\" },\n    // main content\n    { \"parentid\": 10, \"id\": 11, \"obj\": \"label\", \"x\": 0, \"y\": 30 * scale, \"w\": 300 * scale, \"h\": 30 * scale, \"text_font\": 26 * scale, \"text_color\": text_color, \"align\": \"center\", \"text\": \"AMBIENT\", \"click\": 0 },\n    { \"parentid\": 10, \"id\": 12, \"obj\": \"label\", \"x\": 27 * scale, \"y\": 50 * scale, \"w\": 150 * scale, \"h\": 100 * scale, \"text_font\": 100 * scale, \"text_color\": text_color, \"align\": \"right\", \"text\": \"20\", \"click\": 0 },\n    { \"parentid\": 10, \"id\": 13, \"obj\": \"label\", \"x\": 177 * scale, \"y\": 98 * scale, \"w\": 50 * scale, \"h\": 50 * scale, \"text_font\": 50 * scale, \"text_color\": text_color, \"align\": \"left\", \"text\": \".8\", \"click\": 0 },\n    { \"parentid\": 10, \"id\": 14, \"obj\": \"label\", \"x\": 50 * scale, \"y\": 180 * scale, \"w\": 100 * scale, \"h\": 30 * scale, \"text_font\": 26 * scale, \"text_color\": text_color, \"align\": \"center\", \"text\": \"SET\" },\n\n    { \"parentid\": 10, \"id\": 15, \"obj\": \"label\", \"x\": 60 * scale, \"y\": 210 * scale, \"w\": 50 * scale, \"h\": 40 * scale, \"text_font\": 40 * scale, \"text_color\": \"#B26619\", \"align\": \"right\", \"text\": \"18\" },\n    { \"parentid\": 10, \"id\": 16, \"obj\": \"label\", \"x\": 110 * scale, \"y\": 227 * scale, \"w\": 25 * scale, \"h\": 20 * scale, \"text_font\": 20 * scale, \"text_color\": \"#B26619\", \"align\": \"left\", \"text\": \".8\" },\n\n    { \"parentid\": 10, \"id\": 17, \"obj\": \"label\", \"x\": 150 * scale, \"y\": 180 * scale, \"w\": 100 * scale, \"h\": 30 * scale, \"text_font\": 26 * scale, \"text_color\": text_color, \"align\": \"center\", \"text\": \"MODE\" },\n    { \"parentid\": 10, \"id\": 18, \"obj\": \"label\", \"x\": 150 * scale, \"y\": 210 * scale, \"w\": 100 * scale, \"h\": 40 * scale, \"text_font\": 40 * scale, \"text_color\": \"green\", \"align\": \"center\", \"text\": \"\\uE493\" },\n\n    // set parent\n    { \"id\": 20, \"parentid\": 1, \"obj\": \"obj\", \"x\": 0, \"y\": 0, \"w\": 300 * scale, \"h\": 300 * scale, \"radius\": 300 * scale, \"bg_opa\": 0, \"click\": 0, \"hidden\": \"true\" },\n    { \"parentid\": 20, \"id\": 21, \"obj\": \"label\", \"x\": 0, \"y\": 30 * scale, \"w\": 300 * scale, \"h\": 30 * scale, \"text_font\": 26 * scale, \"text_color\": text_color, \"align\": \"center\", \"text\": \"SET\", \"click\": 0 },\n    { \"parentid\": 20, \"id\": 22, \"obj\": \"label\", \"x\": 27 * scale, \"y\": 50 * scale, \"w\": 150 * scale, \"h\": 100 * scale, \"text_font\": 100 * scale, \"text_color\": text_color, \"align\": \"right\", \"text\": \"20\", \"click\": 0 },\n    { \"parentid\": 20, \"id\": 23, \"obj\": \"label\", \"x\": 177 * scale, \"y\": 98 * scale, \"w\": 50 * scale, \"h\": 50 * scale, \"text_font\": 50 * scale, \"text_color\": text_color, \"align\": \"left\", \"text\": \".8\", \"click\": 0 },\n    { \"parentid\": 20, \"id\": 24, \"obj\": \"label\", \"x\": 50 * scale, \"y\": 180 * scale, \"w\": 100 * scale, \"h\": 60 * scale, \"text_font\": 46 * scale, \"text_color\": text_color, \"align\": \"center\", \"text\": \"\\uE045\" },\n    { \"parentid\": 20, \"id\": 25, \"obj\": \"label\", \"x\": 150 * scale, \"y\": 180 * scale, \"w\": 100 * scale, \"h\": 60 * scale, \"text_font\": 46 * scale, \"text_color\": text_color, \"align\": \"center\", \"text\": \"\\uE05D\" },\n\n    // mode parent\n    { \"id\": 30, \"parentid\": 1, \"obj\": \"obj\", \"x\": 0, \"y\": 0, \"w\": 300 * scale, \"h\": 300 * scale, \"radius\": 300 * scale, \"bg_opa\": 0, \"click\": 0, \"hidden\": \"true\" },\n    { \"parentid\": 30, \"id\": 31, \"obj\": \"label\", \"x\": 0, \"y\": 30 * scale, \"w\": 300 * scale, \"h\": 30 * scale, \"text_font\": 26 * scale, \"text_color\": text_color, \"align\": \"center\", \"text\": \"MODE\", \"click\": 0 },\n    { \"parentid\": 30, \"id\": 32, \"obj\": \"label\", \"x\": 0, \"y\": 50 * scale, \"w\": 300 * scale, \"h\": 100 * scale, \"text_font\": 100 * scale, \"text_color\": \"green\", \"align\": \"center\", \"text\": \"\\uE493\", \"click\": 0 },\n    { \"parentid\": 30, \"id\": 33, \"obj\": \"label\", \"x\": 50 * scale, \"y\": 180 * scale, \"w\": 100 * scale, \"h\": 60 * scale, \"text_font\": 46 * scale, \"text_color\": text_color, \"align\": \"center\", \"text\": \"\\uE04D\" },\n    { \"parentid\": 30, \"id\": 34, \"obj\": \"label\", \"x\": 150 * scale, \"y\": 180 * scale, \"w\": 100 * scale, \"h\": 60 * scale, \"text_font\": 46 * scale, \"text_color\": text_color, \"align\": \"center\", \"text\": \"\\uE054\" },\n\n    // hidden close button (drawn last to be on top layer)\n    { \"parentid\": 1, \"id\": 3, \"obj\": \"obj\", \"x\": 0, \"y\": 0, \"w\": 300 * scale, \"h\": 160 * scale, \"bg_opa\": 0, \"border_side\": 0 },\n];\n\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":1035,"y":105,"wires":[["2b3afc242522f593"]],"info":"msg.topic = \"hasp/1/command/jsonl\"\r\nconst text_color = \"#bfbfbf\"\r\n\r\nvar targetWidth = 350;\r\nvar targetHeight = 350;\r\n\r\nvar scaleFactor = Math.min(targetWidth / 300, targetHeight / 300);\r\n\r\nmsg.payload = [\r\n    // page background\r\n    { \"page\": 12, \"id\": 0, \"bg_color\": \"#000000\", \"click\": 0 },\r\n    // circle object with gradient fill\r\n    { \"id\": 1, \"obj\": \"obj\", \"x\": 90 * scaleFactor, \"y\": 95 * scaleFactor, \"w\": 300 * scaleFactor, \"h\": 300 * scaleFactor, \"radius\": 300 * scaleFactor, \"border_side\": 15 * scaleFactor, \"border_width\": 15 * scaleFactor, \"bg_opa\": 255, \"border_color\": \"#B26619\", \"bg_grad_color\": \"BLACK\", \"bg_color\": \"grey\", \"bg_grad_dir\": 1, \"bg_grad_stop\": 200, \"bg_main_stop\": 0, \"click\": 0 },\r\n    // horizontal divider\r\n    { \"parentid\": 1, \"id\": 2, \"obj\": \"obj\", \"x\": 35 * scaleFactor, \"y\": 165 * scaleFactor, \"w\": 230 * scaleFactor, \"h\": 2 * scaleFactor, \"radius\": 0, \"border_side\": 1 * scaleFactor, \"border_width\": 2 * scaleFactor, \"bg_opa\": 0, \"border_color\": text_color, \"click\": 0 },\r\n\r\n    // main content parent\r\n    { \"id\": 10, \"obj\": \"obj\", \"x\": 90 * scaleFactor, \"y\": 95 * scaleFactor, \"w\": 300 * scaleFactor, \"h\": 300 * scaleFactor, \"radius\": 300 * scaleFactor, \"bg_opa\": 0, \"click\": 0, \"hidden\": \"false\" },\r\n    // main content\r\n    { \"parentid\": 10, \"id\": 11, \"obj\": \"label\", \"x\": 0, \"y\": 30 * scaleFactor, \"w\": 300 * scaleFactor, \"h\": 30 * scaleFactor, \"text_font\": 26 * scaleFactor, \"text_color\": text_color, \"align\": \"center\", \"text\": \"AMBIENT\", \"click\": 0 },\r\n    { \"parentid\": 10, \"id\": 12, \"obj\": \"label\", \"x\": 27 * scaleFactor, \"y\": 50 * scaleFactor, \"w\": 150 * scaleFactor, \"h\": 100 * scaleFactor, \"text_font\": 100 * scaleFactor, \"text_color\": text_color, \"align\": \"right\", \"text\": \"20\", \"click\": 0 },\r\n    { \"parentid\": 10, \"id\": 17, \"obj\": \"label\", \"x\": 177 * scaleFactor, \"y\": 98 * scaleFactor, \"w\": 50 * scaleFactor, \"h\": 50 * scaleFactor, \"text_font\": 50 * scaleFactor, \"text_color\": text_color, \"align\": \"left\", \"text\": \".8\", \"click\": 0 },\r\n    { \"parentid\": 10, \"id\": 13, \"obj\": \"label\", \"x\": 50 * scaleFactor, \"y\": 180 * scaleFactor, \"w\": 100 * scaleFactor, \"h\": 30 * scaleFactor, \"text_font\": 26 * scaleFactor, \"text_color\": text_color, \"align\": \"center\", \"text\": \"SET\" },\r\n\r\n    { \"parentid\": 10, \"id\": 14, \"obj\": \"label\", \"x\": 60 * scaleFactor, \"y\": 210 * scaleFactor, \"w\": 50 * scaleFactor, \"h\": 40 * scaleFactor, \"text_font\": 40 * scaleFactor, \"text_color\": \"#B26619\", \"align\": \"right\", \"text\": \"18\" },\r\n    { \"parentid\": 10, \"id\": 18, \"obj\": \"label\", \"x\": 110 * scaleFactor, \"y\": 227 * scaleFactor, \"w\": 25 * scaleFactor, \"h\": 20 * scaleFactor, \"text_font\": 20 * scaleFactor, \"text_color\": \"#B26619\", \"align\": \"left\", \"text\": \".8\" },\r\n\r\n    { \"parentid\": 10, \"id\": 15, \"obj\": \"label\", \"x\": 150 * scaleFactor, \"y\": 180 * scaleFactor, \"w\": 100 * scaleFactor, \"h\": 30 * scaleFactor, \"text_font\": 26 * scaleFactor, \"text_color\": text_color, \"align\": \"center\", \"text\": \"MODE\" },\r\n    { \"parentid\": 10, \"id\": 16, \"obj\": \"label\", \"x\": 150 * scaleFactor, \"y\": 210 * scaleFactor, \"w\": 100 * scaleFactor, \"h\": 40 * scaleFactor, \"text_font\": 40 * scaleFactor, \"text_color\": \"green\", \"align\": \"center\", \"text\": \"\\uE493\" },\r\n\r\n    // set parent\r\n    { \"id\": 20, \"obj\": \"obj\", \"x\": 90 * scaleFactor, \"y\": 95 * scaleFactor, \"w\": 300 * scaleFactor, \"h\": 300 * scaleFactor, \"radius\": 300 * scaleFactor, \"bg_opa\": 0, \"click\": 0, \"hidden\": \"true\" },\r\n    { \"parentid\": 20, \"id\": 21, \"obj\": \"label\", \"x\": 0, \"y\": 30 * scaleFactor, \"w\": 300 * scaleFactor, \"h\": 30 * scaleFactor, \"text_font\": 26 * scaleFactor, \"text_color\": text_color, \"align\": \"center\", \"text\": \"SET\", \"click\": 0 },\r\n    { \"parentid\": 20, \"id\": 22, \"obj\": \"label\", \"x\": 27 * scaleFactor, \"y\": 50 * scaleFactor, \"w\": 150 * scaleFactor, \"h\": 100 * scaleFactor, \"text_font\": 100 * scaleFactor, \"text_color\": text_color, \"align\": \"right\", \"text\": \"20\", \"click\": 0 },\r\n    { \"parentid\": 20, \"id\": 25, \"obj\": \"label\", \"x\": 177 * scaleFactor, \"y\": 98 * scaleFactor, \"w\": 50 * scaleFactor, \"h\": 50 * scaleFactor, \"text_font\": 50 * scaleFactor, \"text_color\": text_color, \"align\": \"left\", \"text\": \".8\", \"click\": 0 },\r\n    { \"parentid\": 20, \"id\": 23, \"obj\": \"label\", \"x\": 50 * scaleFactor, \"y\": 180 * scaleFactor, \"w\": 100 * scaleFactor, \"h\": 60 * scaleFactor, \"text_font\": 46 * scaleFactor, \"text_color\": text_color, \"align\": \"center\", \"text\": \"\\uE045\" },\r\n    { \"parentid\": 20, \"id\": 24, \"obj\": \"label\", \"x\": 150 * scaleFactor, \"y\": 180 * scaleFactor, \"w\": 100 * scaleFactor, \"h\": 60 * scaleFactor, \"text_font\": 46 * scaleFactor, \"text_color\": text_color, \"align\": \"center\", \"text\": \"\\uE05D\" },\r\n\r\n    // mode parent\r\n    { \"id\": 30, \"obj\": \"obj\", \"x\": 90 * scaleFactor, \"y\": 95 * scaleFactor, \"w\": 300 * scaleFactor, \"h\": 300 * scaleFactor, \"radius\": 300 * scaleFactor, \"bg_opa\": 0, \"click\": 0, \"hidden\": \"true\" },\r\n    { \"parentid\": 30, \"id\": 31, \"obj\": \"label\", \"x\": 0, \"y\": 30 * scaleFactor, \"w\": 300 * scaleFactor, \"h\": 30 * scaleFactor, \"text_font\": 26 * scaleFactor, \"text_color\": text_color, \"align\": \"center\", \"text\": \"MODE\", \"click\": 0 },\r\n    { \"parentid\": 30, \"id\": 32, \"obj\": \"label\", \"x\": 0, \"y\": 50 * scaleFactor, \"w\": 300 * scaleFactor, \"h\": 100 * scaleFactor, \"text_font\": 100 * scaleFactor, \"text_color\": \"green\", \"align\": \"center\", \"text\": \"\\uE493\", \"click\": 0 },\r\n    { \"parentid\": 30, \"id\": 33, \"obj\": \"label\", \"x\": 50 * scaleFactor, \"y\": 180 * scaleFactor, \"w\": 100 * scaleFactor, \"h\": 60 * scaleFactor, \"text_font\": 46 * scaleFactor, \"text_color\": text_color, \"align\": \"center\", \"text\": \"\\uE04D\" },\r\n    { \"parentid\": 30, \"id\": 34, \"obj\": \"label\", \"x\": 150 * scaleFactor, \"y\": 180 * scaleFactor, \"w\": 100 * scaleFactor, \"h\": 60 * scaleFactor, \"text_font\": 46 * scaleFactor, \"text_color\": text_color, \"align\": \"center\", \"text\": \"\\uE054\" },\r\n\r\n    // hidden close button (drawn last to be on top layer)\r\n    { \"parentid\": 1, \"id\": 3, \"obj\": \"obj\", \"x\": 0, \"y\": 0, \"w\": 300 * scaleFactor, \"h\": 160 * scaleFactor, \"bg_opa\": 0, \"border_side\": 0 },\r\n];\r\n\r\nreturn msg;"},{"id":"02f48e6d8567da9f","type":"inject","z":"fb8e012451d1e803","g":"141215bdcc5dac6a","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":840,"y":105,"wires":[["3de70dc003a542b4"]]}]
1 Like

Thanks, will try out.
I see at first a handy function "Send jsonl Chunks"
I'm going to play with this!!

1 Like

I had a few issues trying to send large amounts of data at once, as there is a 2K limit to the size that the jsonl parser can handle.

That function allows you to send any size payload, and breaks it into 2K chunks to forward on to openHasp.

I'm working on some tools to make the page creation easier.

To help where there is essentially a lot of duplication, eg X number of buttons on X number of pages, where each button only differs by a label or icon etc, I'm trying to create a kind of templating system so the actual amount of typing to produce pages is a minimal as possible.

I hope this will eventually lead to something where only a list of labels / icons is needed to create a page of buttons etc.

It's still very much work in progress but if anyone is interested in giving it a go here is the simple flow (much less simple function node :wink: )

You can inject required parameters -

{"screenWidth":480,"screenHeight":307,"numRows":6,"numCols":7,"gapBetweenItems":5,"topMargin":20,"bottomMargin":0,"leftMargin":0,"rightMargin":0}

Seems to work OK from 1x1 to 7x7, if you can read the text at 7x7 you have better eyesight than me :eyeglasses:

Screen size is the size of the container obj. Margins are optional, but use 4 values in the correct order if you do provide them. My example is to fit x number of items into a tab using the tabview element, and as I wanted some space at the top to press the tab buttons, I added the margin option.

The test layout is created automatically, based on the desired grid. The size of the buttons, fonts and icons are calculated along with the x and y coordinates of each button, these are dynamically added to a standard button template which contains all the formatting so there is minimal editing required.

Just for good measure it pulls in a background colour from an array based on the id of the button :wink:

Topics, broker and page number etc will need to be setup for your system in the MQTT node and at the bottom of the function node - msg.topic = "hasp/1/command" msg.payload = ['clearpage 3']

WhatsApp Image 2024-01-27 at 17.27.22_8894fe82 WhatsApp Image 2024-01-27 at 17.16.29_5285c2df

WhatsApp Image 2024-01-27 at 17.17.33_3c6122d6 WhatsApp Image 2024-01-27 at 17.18.05_48ba9432

[{"id":"b8db925542b1e4d8","type":"group","z":"fb8e012451d1e803","name":"Layout testing","style":{"fill":"#ffbfbf","label":true},"nodes":["1a89d406f2e9c9ca","c9f0b4285251c548","8906e4034994d2a9","e79e533707f96908","ba66805bb61a8530"],"x":704,"y":49,"w":902,"h":142},{"id":"1a89d406f2e9c9ca","type":"inject","z":"fb8e012451d1e803","g":"b8db925542b1e4d8","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"{\"screenWidth\":480,\"screenHeight\":308,\"numRows\":6,\"numCols\":7,\"gapBetweenItems\":5,\"topMargin\":20,\"bottomMargin\":0,\"leftMargin\":0,\"rightMargin\":0}","payloadType":"json","x":800,"y":135,"wires":[["c9f0b4285251c548"]]},{"id":"c9f0b4285251c548","type":"function","z":"fb8e012451d1e803","g":"b8db925542b1e4d8","name":"Scene buttons layout tool","func":"const grid = global.get(\"calculateGridValues\")\nnode.warn(msg.payload);\nconst layout = grid(\n    msg.payload.screenWidth,\n    msg.payload.screenHeight,\n    msg.payload.numRows,\n    msg.payload.numCols,\n    msg.payload.gapBetweenItems,\n    msg.payload.topMargin,\n    msg.payload.bottomMargin,\n    msg.payload.leftMargin,\n    msg.payload.rightMargin\n);\nnode.warn(layout);\n\nlet numIcons = msg.payload.numRows * msg.payload.numCols\nconst scale = Math.min(layout.itemWidth / 150, layout.itemHeight / 130); // original button was 150 x 130\n\nconst textColor = \"#bfbfbf\"\nconst backgroundColor = \"#000000\"\nconst btnColors = [\"#FFFFFF\", \"#b59f2f\", \"#F26462\", \"#0B7C98\", \"#254F3B\", \"#76AA4B\", \"#742a75\", \"#94742e\", \"#823532\", \"#F6963B\", \"#D74548\", \"#a93fb5\",\n    \"#AFAE6E\", \"#0e7d87\", \"#0C4976\", \"#1d1f99\", \"#519190\", \"#aba28e\", \"#d47a35\", \"#FFFFFF\", \"#b59f2f\", \"#F26462\", \"#0B7C98\", \"#254F3B\", \"#76AA4B\", \"#742a75\", \"#94742e\", \"#823532\", \"#F6963B\", \"#D74548\", \"#a93fb5\",\n    \"#AFAE6E\", \"#0e7d87\", \"#0C4976\", \"#1d1f99\", \"#519190\", \"#aba28e\", \"#d47a35\"]\n\n\nlet btnValues = {\n    \"obj\": \"btn\",\n    \"parentid\": 51, // first tab\n    \"w\": layout.itemWidth,\n    \"h\": layout.itemHeight,\n    \"outline_width\": 1,\n    \"outline_color\": \"#aaaaaa\",\n    \"outline_opa\": 50,\n    \"bg_color\": \"#356E62\",\n    //  \"bg_opa\": 255,\n    //  \"border_opa\": 255,\n    //  \"border_side\": 1,\n    // \"border_width\": 11,\n    // \"border_color\": \"#393939\",\n    \"radius\": 8,\n    \"text_font\": 58 * scale,\n    \"text_color\": textColor,\n    \"text_line_space\": -20 * scale,\n    \"value_font\": 30 * scale,\n    \"value_ofs_y\": 26 * scale,\n    \"value_color\": textColor\n};\n\nconst tabviewValues = {\n    \"id\": 50, \"obj\": \"tabview\", \"btn_pos\": 1, \"y\": 75, \"w\": 480, \"h\": 353, \"bg_color\": backgroundColor, \"text_font\": 26, \"border_side\": 0,\n    \"bg_opa10\": 0,                      // 10 hide underscore showing selected tab\n    \"bg_color30\": backgroundColor,      // 30 tab name background\n    \"text_color40\": \"#8a8a8a\",          // 40 tab name font colour\n    \"pad_top40\": 1, \"pad_bottom40\": 1   // 40 container for tab names\n\n}\nconst tabValues = { \"obj\": \"tab\", \"parentid\": 50 }\n\n// set x and y for each button based on id sequence ie 1 = top left\n// x & y are calculated by calculateGridValues function\nfunction calcXY(id) {\n    const x = layout.valuesX[(id - 1) % layout.valuesX.length];\n    const y = layout.valuesY[Math.floor((id - 1) / layout.valuesX.length) % layout.valuesY.length];\n    return { x, y };\n}\n\nlet message = [\n\n    { \"id\": 1, \"text\": \"\\uF1E1\\n\", \"value_str\": \"Main\", ...btnValues }, //change \"text\" value to icon and \"value_str\" to label\n    { \"id\": 2, \"text\": \"\\uE8DD\\n\", \"value_str\": \"Lamp\", ...btnValues },\n    { \"id\": 3, \"text\": \"\\uE91C\\n\", \"value_str\": \"Wall\", ...btnValues },\n    { \"id\": 4, \"text\": \"\\uE6B5\\n\", \"value_str\": \"Dining\", ...btnValues },\n    { \"id\": 5, \"text\": \"\\uE238\\n\", \"value_str\": \"Fire\", ...btnValues },\n    { \"id\": 6, \"text\": \"\\uE1D9\\n\", \"value_str\": \"Fish Pump\", ...btnValues },\n    { \"id\": 7, \"text\": \"\\uF1E1\\n\", \"value_str\": \"Blinds\", ...btnValues },\n    { \"id\": 8, \"text\": \"\\uE8DD\\n\", \"value_str\": \"Open\", ...btnValues },\n    { \"id\": 9, \"text\": \"\\uE91C\\n\", \"value_str\": \"Close\", ...btnValues },\n    { \"id\": 10, \"text\": \"\\uE6B5\\n\", \"value_str\": \"Dining\", ...btnValues },\n    { \"id\": 11, \"text\": \"\\uE238\\n\", \"value_str\": \"Fire\", ...btnValues },\n    { \"id\": 12, \"text\": \"\\uE1D9\\n\", \"value_str\": \"Fish Pump\", ...btnValues },\n    { \"id\": 13, \"text\": \"\\uF1E1\\n\", \"value_str\": \"Blinds\", ...btnValues },\n    { \"id\": 14, \"text\": \"\\uE8DD\\n\", \"value_str\": \"Open\", ...btnValues },\n    { \"id\": 15, \"text\": \"\\uE91C\\n\", \"value_str\": \"Close\", ...btnValues },\n    { \"id\": 16, \"text\": \"\\uE6B5\\n\", \"value_str\": \"Dining\", ...btnValues },\n    { \"id\": 17, \"text\": \"\\uE238\\n\", \"value_str\": \"Fire\", ...btnValues },\n    { \"id\": 18, \"text\": \"\\uE1D9\\n\", \"value_str\": \"Fish Pump\", ...btnValues },\n    { \"id\": 19, \"text\": \"\\uF1E1\\n\", \"value_str\": \"Blinds\", ...btnValues },\n    { \"id\": 20, \"text\": \"\\uE8DD\\n\", \"value_str\": \"Open\", ...btnValues },\n    { \"id\": 21, \"text\": \"\\uE91C\\n\", \"value_str\": \"Close\", ...btnValues },\n    { \"id\": 22, \"text\": \"\\uE6B5\\n\", \"value_str\": \"Dining\", ...btnValues },\n    { \"id\": 23, \"text\": \"\\uE238\\n\", \"value_str\": \"Fire\", ...btnValues },\n    { \"id\": 24, \"text\": \"\\uE1D9\\n\", \"value_str\": \"Fish Pump\", ...btnValues },\n    { \"id\": 25, \"text\": \"\\uE91C\\n\", \"value_str\": \"Close\", ...btnValues },\n    { \"id\": 26, \"text\": \"\\uE6B5\\n\", \"value_str\": \"Dining\", ...btnValues },\n    { \"id\": 27, \"text\": \"\\uE238\\n\", \"value_str\": \"Fire\", ...btnValues },\n    { \"id\": 28, \"text\": \"\\uE1D9\\n\", \"value_str\": \"Fish Pump\", ...btnValues },\n    { \"id\": 29, \"text\": \"\\uF1E1\\n\", \"value_str\": \"Blinds\", ...btnValues },\n    { \"id\": 30, \"text\": \"\\uE8DD\\n\", \"value_str\": \"Open\", ...btnValues },\n    { \"id\": 31, \"text\": \"\\uE91C\\n\", \"value_str\": \"Close\", ...btnValues },\n    { \"id\": 32, \"text\": \"\\uE6B5\\n\", \"value_str\": \"Dining\", ...btnValues },\n    { \"id\": 33, \"text\": \"\\uE238\\n\", \"value_str\": \"Fire\", ...btnValues },\n    { \"id\": 34, \"text\": \"\\uE1D9\\n\", \"value_str\": \"Fish Pump\", ...btnValues },\n    { \"id\": 35, \"text\": \"\\uF1E1\\n\", \"value_str\": \"Blinds\", ...btnValues },\n    { \"id\": 36, \"text\": \"\\uE8DD\\n\", \"value_str\": \"Open\", ...btnValues },\n    { \"id\": 37, \"text\": \"\\uE91C\\n\", \"value_str\": \"Close\", ...btnValues },\n    { \"id\": 38, \"text\": \"\\uE6B5\\n\", \"value_str\": \"Dining\", ...btnValues },\n    { \"id\": 39, \"text\": \"\\uE238\\n\", \"value_str\": \"Fire\", ...btnValues },\n    { \"id\": 40, \"text\": \"\\uE1D9\\n\", \"value_str\": \"Fish Pump\", ...btnValues },\n    { \"id\": 41, \"text\": \"\\uE91C\\n\", \"value_str\": \"Close\", ...btnValues },\n    { \"id\": 42, \"text\": \"\\uE1D9\\n\", \"value_str\": \"Fish Pump\", ...btnValues },\n]\n\nlet buttons = []\nmessage.forEach(obj => {\n    if (obj.id >= 1 && obj.id <= numIcons) {\n        const { x, y } = calcXY(obj.id);\n        obj.x = x;\n        obj.y = y;\n        obj.bg_color = btnColors[obj.id]\n        buttons.push(obj)\n    }\n});\n\nmsg.topic = \"hasp/1/command\"\nmsg.payload = ['clearpage 3']\nnode.send([null, msg]);\n\nmsg.payload = [{ \"page\": 3 }, { ...tabviewValues }, { \"id\": 51, \"text\": \"Lounge\", ...tabValues }, { \"id\": 52, \"text\": \"Scene\", ...tabValues },\n{ \"id\": 53, \"text\": \"Blinds\", ...tabValues }, ...buttons]\n\nreturn msg;","outputs":2,"timeout":0,"noerr":0,"initialize":"function calculateGridValues(width, height, numRows, numCols, minGap, topMargin = 0, bottomMargin = 0, leftMargin = 0, rightMargin = 0) {\n    let minGapX = minGap;\n    let minGapY = minGap;\n\n    let remainingWidth, remainingHeight, itemWidth, itemHeight, availableWidth, availableHeight, totalHorizontalGap, totalVerticalGap;\n\n    for (let index = minGap - 3; index < minGap + 3; index++) {\n        totalHorizontalGap = (numCols - 1) * minGapX;\n        totalVerticalGap = (numRows - 1) * minGapY;\n\n        availableWidth = width - leftMargin - rightMargin - totalHorizontalGap;\n        availableHeight = height - topMargin - bottomMargin - totalVerticalGap;\n\n        itemWidth = Math.floor(availableWidth / numCols);\n        itemHeight = Math.floor(availableHeight / numRows);\n\n        remainingWidth = width - leftMargin - rightMargin - (itemWidth * numCols) - totalHorizontalGap;\n        remainingHeight = height - topMargin - bottomMargin - (itemHeight * numRows) - totalVerticalGap;\n\n        // If remaining space is not zero, try another minGap\n        minGapX = remainingWidth !== 0 ? index : minGapX;\n        minGapY = remainingHeight !== 0 ? index : minGapY;\n        if (remainingWidth === 0 && remainingHeight === 0) break;\n    }\n\n    const valuesX = [];\n    for (let col = 0; col < numCols; col++) {\n        const x = leftMargin + col * (itemWidth + minGapX);\n        valuesX.push(x);\n    }\n    const valuesY = [];\n    for (let row = 0; row < numRows; row++) {\n        const y = topMargin + row * (itemHeight + minGapY);\n        valuesY.push(y);\n    }\n\n    return {\n        valuesX,\n        valuesY,\n        itemWidth,\n        itemHeight,\n        availableWidth,\n        availableHeight,\n        remainingWidth,\n        remainingHeight,\n        totalHorizontalGap,\n        totalVerticalGap,\n        minGapX,\n        minGapY\n    };\n}\n\nglobal.set(\"calculateGridValues\", calculateGridValues);\n\n","finalize":"","libs":[],"x":995,"y":135,"wires":[["8906e4034994d2a9"],["ba66805bb61a8530"]]},{"id":"8906e4034994d2a9","type":"delay","z":"fb8e012451d1e803","g":"b8db925542b1e4d8","name":"","pauseType":"delay","timeout":"1","timeoutUnits":"seconds","rate":"1","nbRateUnits":"1","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":false,"allowrate":false,"outputs":1,"x":1195,"y":90,"wires":[["e79e533707f96908"]]},{"id":"e79e533707f96908","type":"function","z":"fb8e012451d1e803","g":"b8db925542b1e4d8","name":"Send jsonl Chunks","func":"// Limit msg.payloads to 2048 bytes to fit with OpenHasp buffer size\n\nlet message = msg.payload;\nlet topicParts = msg.topic.split(\"/\");\nlet topic = `hasp/${topicParts[1]}/command/jsonl`;\n\n// send in chunks\nconst maxLength = 2000;\nlet payload = \"\";\n\nfor (let index = 0; index < message.length; index++) {\n    const jsonString = JSON.stringify(message[index]) \n\n    if (payload.length + jsonString.length <= maxLength) {\n        payload += jsonString;\n    } else {\n        sendPayload(topic, payload);\n        payload = jsonString;\n    }\n}\n\n// send any remaining payload\nsendPayload(topic, payload);\n\nfunction sendPayload(topic, payload) {\n    if (payload.length > 0) {\n        node.send({\n            \"topic\": topic,\n            \"payload\": payload\n        });\n    }\n}\n\n","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":1360,"y":90,"wires":[["ba66805bb61a8530"]],"info":"let message=msg.payload\r\nlet topicParts = msg.topic.split(\"/\")\r\nlet topic = `hasp/${topicParts[1]}/command/jsonl`\r\n\r\n// send in chunks\r\nlet payload = \"\";\r\nconst maxLength = 2048;\r\nfor (let index = 0; index < message.length; index++) {\r\n    const jsonString = JSON.stringify(message[index]) + \"\\n\";\r\n    \r\n    if (payload.length + jsonString.length <= maxLength) {\r\n        payload += jsonString;\r\n    } else {\r\n        node.send({\r\n            \"topic\": topic,\r\n            \"payload\": payload\r\n        });\r\n        payload = jsonString;\r\n    }\r\n}\r\n// anything remaining to send?\r\nif (payload.length > 0) {\r\n    node.send({\r\n        \"topic\": topic,\r\n        \"payload\": payload\r\n    });\r\n}"},{"id":"ba66805bb61a8530","type":"mqtt out","z":"fb8e012451d1e803","g":"b8db925542b1e4d8","name":"","topic":"","qos":"","retain":"","respTopic":"","contentType":"","userProps":"","correl":"","expiry":"","broker":"","x":1530,"y":150,"wires":[]}]
3 Likes

@BartButenaers My latest creation is a dynamically created page navigation popup, using the data I already had in the pages layout.

Another step towards having to do very little to build a working screen :rofl:

image

2 Likes

Nice I'll load this up on dev device and see how it works.

@thatkide Still work in progress, as there is a lot more to come.

What screen resolution do you have ?

My testing is on 480x480, so it would be good to find out what needs tweaking for other sizes.

Let me know how you get on, this was just an early POC, but if you want to try the latest version let me know and I will see if I can put something together for you.