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 )
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
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
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']
[{"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":[]}]