Using openHASP with Node-Red

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