Ideas for a "database-like" structure in global context

Hi all

I'm looking for some critique of my implementation of lighting control in NR.

For a while I've just set up dashboards with static controls for each light, but I'd like to move towards a dynamic configuration. So the UI would basically "create itself", based on the configuration. Also, the config could be changed within Dashboard. (e.g. adding a light to a zone, or storing a scene preset)

The main items to store are:

  • light fixtures
  • zones, (each one containing an array of fixtures)
  • scenes (e.g. a 2D array, each zone will have settings for each fixture in the given zone)

The obvious way to implement this is using a database, but I'd prefer to store everything in NR.

e.g. an object in context like below. What do you think of this idea? Should I just keep it simple / static? It would be pretty cool being able to make changes through the dashboard (more user friendly for others), also I should add we are building a new home, so will be setting up lighting over the course of a few months, hence it might be useful to have a framework to work in, for testing and ease of changing one's mind!

{
	"lights" : [
		{
			"id"			:	"3f_bedroom_ceiling",
			"friendly"		: 	"3rd floor bedroom ceiling",
			"type"			:	"dimmable single colour",
			"topic"			:	"home/light/1",
			"defaultstate"	:	20,
			"currentstate"	:	50
		},
		{
			"id"			:	"3f_bedroom_bedside",
			"friendly"		: 	"3rd floor bedroom bedside",
			"type"			:	"dimmable single colour",
			"topic"			:	"home/light/2",
			"defaultstate"	:	20,
			"currentstate"	:	50
		}
	],
	"zones"	: [
		{
			"id"			: "3f_bedroom",
			"friendly"		: "Master bedroom"
			"members"		: ["3f_bedroom_ceiling", "3f_bedroom_bedside"],
			"scenes"		: [
				{
					"Name":"Off",
					"Values" : [
						
					]
				}
			]
		},
		{
			"id"			: "3f_living",
			"friendly"		: "Living Room",
			"members"		: ["3f_living_ceiling", "3f_living_wall","huego1"]
		}
	]
}

Yes, this is very similar to what I've done for a long time and more recently started reworking to make it even more generic and reusable.

You might want to consider several variables though. So one to list devices and a separate one to list locations. You should probably write some standard functions then that let you do the kinds of lookups and validations you are likely to need along the way.

image

I'll be simplifying those in a future itteration.

Hi,
I have the same approach to my home automation system. All variables / parmeters should be able to be edited on the dashboard and represented in one (or more) "settings" objects.

Currently a part of my heating settings looks like that on the dashboard (sorry for the german labels but you get the idea):


I can define differed time periods for my heating settings

After the third attempt the flow is finally streamlined like this:

and the result in the global store:
image

All data is created "on the go", except the days of the week, but these will not change soon.
I do not want to go into my flows if I have to change any parameter. The flow should be only the logic level. In the future I try to implement some logic into the parameter = ui level like if a light bulb moves from one room to another because the lamp was moved but that's a long way to go.

I can publish the flow if someone is interested.

3 Likes

Hi Chris,
I would be interested in seeing your 'creative' flow if you are willing to publish it.

For anyone interested, here's my progress so far. I have made a UI in Dashboard, with three panels:

  • Create / delete / view list of light fixtures
  • Create / delete /view list of light zones
  • Assign a fixture to a zone (based on @TotallyInformation's "device to room map")

It will save the items to global context, and uses flow context for temp storage of the new values. Also it features validation to ensure all fields were filled in / selected. And of course it checks the ID before saving, to ensure no duplicates.

The IDs for each fixture or zone actually form the object key, so it's nice to look through in context explorer sidebar.

[{"id":"8a93364b.187ff8","type":"comment","z":"705d403b.f2b8","name":"Version 2","info":"\n6/10/2019\n---------\nStart with the things we want to do / make easy for installation\n- Ongoing\n - tweak lighting scenes\n - re-assign roving lights to different zones\n\n- Setup\n - Create fixings from the UI? If we are using DMX we could standardise the\n     output message, and set this in the UI as well. Otherwise, what's the\n     point? We'd still have to create the thing in Node-RED that sends the\n     message to the light. Well, maybe there is a point. Maybe it's just\n     useful to see the contents of memory in realtime from the front end,\n     as you add devices. We could set the MQTT path from front end as well.\n     Then all we'd have to do is set up the device and \"attach\" to the MQTT\n     path.\n - Create zones and map fixings to zones\n - Create zone presets\n   - List out the zones and mapped fixings\n   - List out the zone presets\n\nThen move on to how we want to store stuff in memory.\n- home.light.fixture\n- home.light.zone\n- home.light.preset\n- home.light.mapping.fixture_zone\n- home.light.mapping.preset_zone\n\n\n\nLights are standalone items\nWithin their definition, their location is not set\n\nWe also have room definitions, each room is a simple list of light\nindexes.\n\n\n{\n\t\"lights\" : [\n\t\t{\n\t\t\t\"id\"\t\t\t:\t\"3f_bedroom_ceiling\",\n\t\t\t\"friendly\"\t\t: \t\"3rd floor bedroom ceiling\",\n\t\t\t\"type\"\t\t\t:\t\"dimmable single colour\",\n\t\t\t\"topic\"\t\t\t:\t\"home/light/1\",\n\t\t\t\"defaultstate\"\t:\t20,\n\t\t\t\"currentstate\"\t:\t50\n\t\t},\n\t\t{\n\t\t\t\"id\"\t\t\t:\t\"3f_bedroom_bedside\",\n\t\t\t\"friendly\"\t\t: \t\"3rd floor bedroom bedside\",\n\t\t\t\"type\"\t\t\t:\t\"dimmable single colour\",\n\t\t\t\"topic\"\t\t\t:\t\"home/light/2\",\n\t\t\t\"defaultstate\"\t:\t20,\n\t\t\t\"currentstate\"\t:\t50\n\t\t}\n\t],\n\t\"zones\"\t: [\n\t\t{\n\t\t\t\"id\"\t\t\t: \"3f_bedroom\",\n\t\t\t\"friendly\"\t\t: \"Master bedroom\"\n\t\t\t\"members\"\t\t: [\"3f_bedroom_ceiling\", \"3f_bedroom_bedside\"],\n\t\t\t\"scenes\"\t\t: [\n\t\t\t\t{\n\t\t\t\t\t\"Name\":\"Off\",\n\t\t\t\t\t\"Values\" : [\n\t\t\t\t\t\t\n\t\t\t\t\t]\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"id\"\t\t\t: \"3f_living\",\n\t\t\t\"friendly\"\t\t: \"Living Room\",\n\t\t\t\"members\"\t\t: [\"3f_living_ceiling\", \"3f_living_wall\",\"huego1\"]\n\t\t}\n\t]\n}\n\n\n","x":2800,"y":1860,"wires":[]},{"id":"7dc1ca73.ae1494","type":"ui_text_input","z":"705d403b.f2b8","name":"","label":"ID","tooltip":"","group":"1a37e545.435d2b","order":1,"width":3,"height":1,"passthru":false,"mode":"text","delay":"350","topic":"","x":3150,"y":1900,"wires":[["e5afc774.989118"]]},{"id":"e5afc774.989118","type":"change","z":"705d403b.f2b8","name":"","rules":[{"t":"set","p":"newfixture.ID","pt":"flow","to":"payload","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":3420,"y":1900,"wires":[[]]},{"id":"40d1dfbc.93ecb","type":"ui_button","z":"705d403b.f2b8","name":"","group":"1a37e545.435d2b","order":5,"width":1,"height":1,"passthru":false,"label":"Save","tooltip":"","color":"","bgcolor":"","icon":"","payload":"nothing","payloadType":"str","topic":"","x":2810,"y":2020,"wires":[["678588e9.2d24c8"]]},{"id":"7dbff401.90176c","type":"ui_text_input","z":"705d403b.f2b8","name":"","label":"Friendly location","tooltip":"","group":"1a37e545.435d2b","order":2,"width":3,"height":1,"passthru":true,"mode":"text","delay":"1000","topic":"","x":3180,"y":1920,"wires":[["65ce0f79.994dc"]]},{"id":"65ce0f79.994dc","type":"change","z":"705d403b.f2b8","name":"","rules":[{"t":"set","p":"newfixture.friendly_location","pt":"flow","to":"payload","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":3460,"y":1920,"wires":[[]]},{"id":"678588e9.2d24c8","type":"function","z":"705d403b.f2b8","name":"New fixture","func":"// Save new fixture entered in Dashboard to global context\n// Includes form validation and duplicate check\n// and supports clearing the form when done correctly\n\n// check form was filled in properly\nif (typeof flow.get(\"newfixture\") == 'undefined') {\n    msg.payload = \"You didn't enter anything - fixture not saved\"\n    msg.error = true\n    return msg\n}\n\n// assume newfixture object exists (some part of form was filled)\nvar newfixture = flow.get(\"newfixture\")\n\n// if form not complete\nif (typeof newfixture.ID == 'undefined' ||\n           newfixture.ID === \"\" ||\n    typeof newfixture.friendly_location == 'undefined' ||\n           newfixture.friendly_location === \"\" ||\n    typeof newfixture.friendly_name == 'undefined' ||\n           newfixture.friendly_name === \"\" ||\n    typeof newfixture.type == 'undefined' ||\n           newfixture.type === \"\") {\n    msg.payload = \"Missing info - fixture not saved\"\n    msg.error = true\n    return msg\n}\n\n// if we've got this far, we have a good new fixture to add\n// create global fixtures object if it's not there\nif (typeof global.get(\"home.light.config.fixtures\") == 'undefined') {\n    global.set(\"home.light.config.fixtures\", {})\n}\n\n// get light fixtures object\nvar fixtures = global.get(\"home.light.config.fixtures\")\n\n// does fixture ID exist?\n\nif (fixtures.hasOwnProperty(newfixture.ID)) {\n    msg.payload = \"oops, ID already exists\"\n    msg.error = true\n    return msg\n} else {\n    d = new Date().toLocaleDateString(); t = new Date().toLocaleTimeString();\n    fixtures[newfixture.ID] = {\n        \"friendly_location\": newfixture.friendly_location,\n        \"friendly_name\": newfixture.friendly_name,\n        \"type\": newfixture.type,\n        \"created_date\": d,\n        \"created_time\": t\n    }\n    \n    global.set(\"home.light.config.fixtures\",fixtures)\n    msg.payload = \"Success - added fixture\"\n    msg.error = false\n    // now remove item from flow context, otherwise our\n    // validation tests above will always pass\n    flow.set(\"newfixture\", undefined);\n    \n    return msg\n}","outputs":1,"noerr":0,"x":2950,"y":2020,"wires":[["13efd823.365108","e05ac37e.8fa61"]]},{"id":"178c7bb6.0832d4","type":"ui_dropdown","z":"705d403b.f2b8","name":"Type","label":"","tooltip":"","place":"Control Type","group":"1a37e545.435d2b","order":4,"width":3,"height":1,"passthru":true,"options":[{"label":"Non Dimmable (Switch)","value":"1_non_dim","type":"str"},{"label":"Dimmable Single Colour","value":"2_single_colour","type":"str"},{"label":"RGB","value":"3_rgb","type":"str"},{"label":"RGBW","value":"4_RGBW","type":"str"},{"label":"RGBWW","value":"5_RGBWW","type":"str"},{"label":"Individually Addressable","value":"6_individually_addressable","type":"str"}],"payload":"","topic":"","x":3150,"y":1960,"wires":[["8715f8dc.f16588"]]},{"id":"8715f8dc.f16588","type":"change","z":"705d403b.f2b8","name":"","rules":[{"t":"set","p":"newfixture.type","pt":"flow","to":"payload","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":3430,"y":1960,"wires":[[]]},{"id":"13efd823.365108","type":"ui_template","z":"705d403b.f2b8","group":"1a37e545.435d2b","name":"New fixture validation","order":6,"width":"8","height":1,"format":"{{msg.payload}}","storeOutMessages":true,"fwdInMessages":false,"templateScope":"local","x":3400,"y":2020,"wires":[[]]},{"id":"efc2b541.5fa958","type":"delay","z":"705d403b.f2b8","name":"","pauseType":"delay","timeout":"5","timeoutUnits":"seconds","rate":"1","nbRateUnits":"1","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":false,"x":3220,"y":2060,"wires":[["13efd823.365108"]]},{"id":"e05ac37e.8fa61","type":"function","z":"705d403b.f2b8","name":"Success","func":"// Save new fixture: clear form & result if successful\n\nif (!msg.error) {\n    msg.payload = \"\"\n    return msg;\n}\n","outputs":1,"noerr":0,"x":3060,"y":2060,"wires":[["efc2b541.5fa958","7dc1ca73.ae1494","7dbff401.90176c","178c7bb6.0832d4","9126cc5a.af8f3","c501b125.62688"]]},{"id":"77a3d35d.3ceecc","type":"change","z":"705d403b.f2b8","name":"","rules":[{"t":"set","p":"newfixture.friendly_name","pt":"flow","to":"payload","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":3460,"y":1940,"wires":[[]]},{"id":"9126cc5a.af8f3","type":"ui_text_input","z":"705d403b.f2b8","name":"","label":"Friendly Fitting Type","tooltip":"","group":"1a37e545.435d2b","order":3,"width":3,"height":1,"passthru":true,"mode":"text","delay":"1000","topic":"","x":3200,"y":1940,"wires":[["77a3d35d.3ceecc"]]},{"id":"c501b125.62688","type":"change","z":"705d403b.f2b8","name":"Get","rules":[{"t":"set","p":"fixtures","pt":"msg","to":"home.light.config.fixtures","tot":"global"}],"action":"","property":"","from":"","to":"","reg":false,"x":3210,"y":2120,"wires":[["8c47badb.7f7df8"]]},{"id":"8c47badb.7f7df8","type":"ui_template","z":"705d403b.f2b8","group":"1a37e545.435d2b","name":"Fixtures List","order":7,"width":"12","height":"8","format":"<h3>Register of Light Fixtures</h3>\n\n<table>\n    <tr style=\"font-weight:800; background-color:rgba(20,20,20,.6);\">\n        <td>ID</td>\n        <td>Location</td>\n        <td>Name</td>\n        <td>Type</td>\n        <td>Added</td>\n        <td>Delete*</td>\n    </tr>\n    <tr ng-repeat=\"(key, value) in msg.fixtures\">\n        <td>{{key}}</td>\n        <td>{{value.friendly_location}}</td>\n        <td>{{value.friendly_name}}</td>\n        <td>{{value.type}}</td>\n        <td style=\"color:#888;\">{{value.created_time}} {{value.created_date}}</td>\n        <td><md-button ng-click=\"send({action: 'delete', topic: key})\">Del</md-button></td>\n    </tr>\n</table>","storeOutMessages":false,"fwdInMessages":false,"templateScope":"local","x":3350,"y":2120,"wires":[["7dadd06a.5a505"]]},{"id":"7dadd06a.5a505","type":"function","z":"705d403b.f2b8","name":"Set","func":"// get object from memory\n// NB we are using \"bracket notation\" just in case the ID\n// (i.e. the object name, represented here as msg.topic)\n// had a space\n\nif (msg.action == \"delete\") {\n    obj = global.get(\"home.light.config.fixtures\")\n    delete obj[msg.topic]\n    global.set(\"home.light.config.fixtures\", obj)\n}\n\nreturn msg;\n","outputs":1,"noerr":0,"x":3490,"y":2120,"wires":[["c501b125.62688"]]},{"id":"5ea5059b.ac0cac","type":"inject","z":"705d403b.f2b8","name":"Update view","topic":"","payload":"","payloadType":"date","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":3030,"y":2120,"wires":[["c501b125.62688"]]},{"id":"4714f8ee.5b3398","type":"ui_text_input","z":"705d403b.f2b8","name":"","label":"ID","tooltip":"","group":"1386e1df.83b3ae","order":1,"width":3,"height":1,"passthru":false,"mode":"text","delay":"350","topic":"","x":3150,"y":2240,"wires":[["7797971f.f78778"]]},{"id":"7797971f.f78778","type":"change","z":"705d403b.f2b8","name":"","rules":[{"t":"set","p":"newzone.ID","pt":"flow","to":"payload","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":3420,"y":2240,"wires":[[]]},{"id":"5b4809a9.073948","type":"ui_button","z":"705d403b.f2b8","name":"","group":"1386e1df.83b3ae","order":3,"width":1,"height":1,"passthru":false,"label":"Save","tooltip":"","color":"","bgcolor":"","icon":"","payload":"nothing","payloadType":"str","topic":"","x":2810,"y":2320,"wires":[["77413dc7.e8c9b4"]]},{"id":"77413dc7.e8c9b4","type":"function","z":"705d403b.f2b8","name":"New zone","func":"// Save new zone entered in Dashboard to global context\n// Includes form validation and duplicate check\n// and supports clearing the form when done correctly\n\n// check form was filled in properly\nif (typeof flow.get(\"newzone\") == 'undefined') {\n    msg.payload = \"You didn't enter anything - zone not saved\"\n    msg.error = true\n    return msg\n}\n\n// assume newzone object exists (some part of form was filled)\nvar newzone = flow.get(\"newzone\")\n\n// if form not complete\nif (typeof newzone.ID == 'undefined' ||\n           newzone.ID === \"\" ||\n    typeof newzone.friendly_name == 'undefined' ||\n           newzone.friendly_name === \"\") {\n    msg.payload = \"Missing info - zone not saved\"\n    msg.error = true\n    return msg\n}\n\n// if we've got this far, we have a good new zone to add\n// create global zones object if it's not there\nif (typeof global.get(\"home.light.config.zones\") == 'undefined') {\n    global.set(\"home.light.config.zones\", {})\n}\n\n// get light zones object\nvar zones = global.get(\"home.light.config.zones\")\n\n// does fixture ID exist?\n\nif (zones.hasOwnProperty(newzone.ID)) {\n    msg.payload = \"oops, ID already exists\"\n    msg.error = true\n    return msg\n} else {\n    d = new Date().toLocaleDateString(); t = new Date().toLocaleTimeString();\n    zones[newzone.ID] = {\n        \"friendly_name\": newzone.friendly_name,\n        \"created_date\": d,\n        \"created_time\": t\n    }\n    \n    global.set(\"home.light.config.zones\",zones)\n    msg.payload = \"Success - added zone\"\n    msg.error = false\n    // now remove item from flow context, otherwise our\n    // validation tests above will always pass\n    flow.set(\"newzone\", undefined);\n    \n    return msg\n}","outputs":1,"noerr":0,"x":2940,"y":2320,"wires":[["58c8c589.5d48dc","549ddba1.fb5e44"]]},{"id":"58c8c589.5d48dc","type":"ui_template","z":"705d403b.f2b8","group":"1386e1df.83b3ae","name":"New zone validation","order":4,"width":"8","height":1,"format":"{{msg.payload}}","storeOutMessages":true,"fwdInMessages":false,"templateScope":"local","x":3400,"y":2320,"wires":[[]]},{"id":"dcf2a212.18aa1","type":"delay","z":"705d403b.f2b8","name":"","pauseType":"delay","timeout":"5","timeoutUnits":"seconds","rate":"1","nbRateUnits":"1","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":false,"x":3220,"y":2360,"wires":[["58c8c589.5d48dc"]]},{"id":"549ddba1.fb5e44","type":"function","z":"705d403b.f2b8","name":"Success","func":"// Save new zone: clear form & result if successful\n\nif (!msg.error) {\n    msg.payload = \"\"\n    return msg;\n}\n","outputs":1,"noerr":0,"x":3060,"y":2360,"wires":[["dcf2a212.18aa1","4714f8ee.5b3398","8f98479c.d386a8","ddc87a2a.8ae778"]]},{"id":"8f98479c.d386a8","type":"change","z":"705d403b.f2b8","name":"Get","rules":[{"t":"set","p":"zones","pt":"msg","to":"home.light.config.zones","tot":"global"}],"action":"","property":"","from":"","to":"","reg":false,"x":3210,"y":2420,"wires":[["8a402bb0.963de8"]]},{"id":"8a402bb0.963de8","type":"ui_template","z":"705d403b.f2b8","group":"1386e1df.83b3ae","name":"Zones List","order":5,"width":"12","height":"8","format":"<h3>Register of Light Zones</h3>\n\n<table>\n    <tr style=\"font-weight:800; background-color:rgba(20,20,20,.6);\">\n        <td>ID</td>\n        <td>Name</td>\n        <td>Added</td>\n        <td>Delete*</td>\n    </tr>\n    <tr ng-repeat=\"(key, value) in msg.zones\">\n        <td>{{key}}</td>\n        <td>{{value.friendly_name}}</td>\n        <td style=\"color:#888;\">{{value.created_time}} {{value.created_date}}</td>\n        <td><md-button ng-click=\"send({action: 'delete', topic: key})\">Del</md-button></td>\n    </tr>\n</table>","storeOutMessages":false,"fwdInMessages":false,"templateScope":"local","x":3350,"y":2420,"wires":[["ab08a35d.f692d"]]},{"id":"ab08a35d.f692d","type":"function","z":"705d403b.f2b8","name":"Set","func":"// get object from memory\n// NB we are using \"bracket notation\" just in case the ID\n// (i.e. the object name, represented here as msg.topic)\n// had a space\n\nif (msg.action == \"delete\") {\n    obj = global.get(\"home.light.config.zones\")\n    delete obj[msg.topic]\n    global.set(\"home.light.config.zones\", obj)\n}\n\nreturn msg;\n","outputs":1,"noerr":0,"x":3490,"y":2420,"wires":[["8f98479c.d386a8"]]},{"id":"1dabe67a.a9504a","type":"inject","z":"705d403b.f2b8","name":"Update view","topic":"","payload":"","payloadType":"date","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":3030,"y":2420,"wires":[["8f98479c.d386a8"]]},{"id":"ddc87a2a.8ae778","type":"ui_text_input","z":"705d403b.f2b8","name":"","label":"Friendly Name","tooltip":"","group":"1386e1df.83b3ae","order":2,"width":3,"height":1,"passthru":true,"mode":"text","delay":"1000","topic":"","x":3180,"y":2260,"wires":[["9c619228.80fde"]]},{"id":"9c619228.80fde","type":"change","z":"705d403b.f2b8","name":"","rules":[{"t":"set","p":"newzone.friendly_name","pt":"flow","to":"payload","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":3450,"y":2260,"wires":[[]]},{"id":"aed3ce62.0a993","type":"change","z":"705d403b.f2b8","name":"","rules":[{"t":"set","p":"new_fixture_zone_mapping.fixtureID","pt":"flow","to":"payload","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":3490,"y":2580,"wires":[[]]},{"id":"c45f942b.25fb78","type":"ui_button","z":"705d403b.f2b8","name":"","group":"1598a8ff.884b77","order":3,"width":1,"height":1,"passthru":false,"label":"Assign","tooltip":"","color":"","bgcolor":"","icon":"","payload":"nothing","payloadType":"str","topic":"","x":2710,"y":2660,"wires":[["bcc049ac.db8cd8"]]},{"id":"bcc049ac.db8cd8","type":"function","z":"705d403b.f2b8","name":"New fixture_zone_mapping","func":"// NEEDS RE-WRITING FOR FIXTURE TO ZONE ASSIGNMENT\n// ID will be automatically generated as per:\n// zoneID_fixtureID\n// if such an ID exists, then message: already exists in zone\n// Don't forget to update code for deleting a zone and\n// a fixture. Each should check for corresponding entries\n// in the mappings object and remove these entries\n\n\n// Save new zone entered in Dashboard to global context\n// Includes form validation and duplicate check\n// and supports clearing the form when done correctly\n\n// check form was filled in properly\nif (typeof flow.get(\"new_fixture_zone_mapping\") == 'undefined') {\n    msg.payload = \"You didn't enter anything - fixture not assigned\"\n    msg.error = true\n    return msg\n}\n\n// assume newzone object exists (some part of form was filled)\nvar new_fixture_zone_mapping = flow.get(\"new_fixture_zone_mapping\")\n\n// if form not complete\nif (typeof new_fixture_zone_mapping.fixtureID == 'undefined' ||\n           new_fixture_zone_mapping.fixtureID === \"\" ||\n    typeof new_fixture_zone_mapping.zoneID == 'undefined' ||\n           new_fixture_zone_mapping.zoneID === \"\") {\n    msg.payload = \"Missing info - fixture not assigned\"\n    msg.error = true\n    return msg\n}\n\n// if we've got this far, we have a good assignment to create\n// create global assignment object if it's not there\nif (typeof global.get(\"home.light.config.fixture_zone_mappings\") == 'undefined') {\n    global.set(\"home.light.config.fixture_zone_mappings\", {})\n}\n\n// get light fixture zone mappings object\nvar fixture_zone_mappings = global.get(\"home.light.config.fixture_zone_mappings\")\n\n// does mapping ID exist? (it will look like this: zoneID_fixtureID)\nvar newID = new_fixture_zone_mapping.zoneID + \"_\" + new_fixture_zone_mapping.fixtureID\n\nif (fixture_zone_mappings.hasOwnProperty(newID)) {\n    msg.payload = \"oops, fixture already added to zone\"\n    msg.error = true\n    return msg\n} else {\n    d = new Date().toLocaleDateString(); t = new Date().toLocaleTimeString();\n    fixture_zone_mappings[newID] = {\n        \"zoneID\": new_fixture_zone_mapping.zoneID,        // technically not necessary\n        \"fixtureID\": new_fixture_zone_mapping.fixtureID,  // technically not necessary\n        \"created_date\": d,\n        \"created_time\": t\n    }\n    \n    global.set(\"home.light.config.fixture_zone_mappings\",fixture_zone_mappings)\n    msg.payload = \"Success - assigned fixture to zone\"\n    msg.error = false\n    // now remove item from flow context, otherwise our\n    // validation tests above will always pass\n    flow.set(\"new_fixture_zone_mapping\", undefined);\n    \n    return msg\n}","outputs":1,"noerr":0,"x":2900,"y":2660,"wires":[["4bafda67.14c724","c903cb21.a24cd8"]]},{"id":"4bafda67.14c724","type":"ui_template","z":"705d403b.f2b8","group":"1598a8ff.884b77","name":"New fixture-zone validation","order":4,"width":"8","height":1,"format":"{{msg.payload}}","storeOutMessages":true,"fwdInMessages":false,"templateScope":"local","x":3420,"y":2660,"wires":[[]]},{"id":"121995cb.dae1aa","type":"delay","z":"705d403b.f2b8","name":"","pauseType":"delay","timeout":"5","timeoutUnits":"seconds","rate":"1","nbRateUnits":"1","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":false,"x":3220,"y":2700,"wires":[["4bafda67.14c724"]]},{"id":"c903cb21.a24cd8","type":"function","z":"705d403b.f2b8","name":"Success","func":"// Save new zone: clear form & result if successful\n\nif (!msg.error) {\n    msg.payload = \"\"\n    return msg;\n}\n","outputs":1,"noerr":0,"x":3060,"y":2700,"wires":[["121995cb.dae1aa","a3100af0.716338","b7e6e6f8.691188","a944297e.5ee6f8"]]},{"id":"a3100af0.716338","type":"change","z":"705d403b.f2b8","name":"Get","rules":[{"t":"set","p":"fixture_zone_mappings","pt":"msg","to":"home.light.config.fixture_zone_mappings","tot":"global"}],"action":"","property":"","from":"","to":"","reg":false,"x":3210,"y":2760,"wires":[["4e545849.410518"]]},{"id":"4e545849.410518","type":"ui_template","z":"705d403b.f2b8","group":"1598a8ff.884b77","name":"Fixture to Zone Mappings List","order":5,"width":"12","height":"8","format":"<h3>Register of Fixture to Zone Mappings</h3>\n\n<table>\n    <tr style=\"font-weight:800; background-color:rgba(20,20,20,.6);\">\n        <td>ID</td>\n        <td>Zone</td>\n        <td>Fixture</td>\n        <td>Added</td>\n        <td>Delete*</td>\n    </tr>\n    <tr ng-repeat=\"(key, value) in msg.fixture_zone_mappings\">\n        <td>{{key}}</td>\n        <td>{{value.zoneID}}</td>\n        <td>{{value.fixtureID}}</td>\n        <td style=\"color:#888;\">{{value.created_time}} {{value.created_date}}</td>\n        <td><md-button ng-click=\"send({action: 'delete', topic: key})\">Del</md-button></td>\n    </tr>\n</table>","storeOutMessages":false,"fwdInMessages":false,"templateScope":"local","x":3410,"y":2760,"wires":[["c0c7b85b.4670e8"]]},{"id":"c0c7b85b.4670e8","type":"function","z":"705d403b.f2b8","name":"Set","func":"// get object from memory\n// NB we are using \"bracket notation\" just in case the ID\n// (i.e. the object name, represented here as msg.topic)\n// had a space\n\nif (msg.action == \"delete\") {\n    obj = global.get(\"home.light.config.fixture_zone_mappings\")\n    delete obj[msg.topic]\n    global.set(\"home.light.config.fixture_zone_mappings\", obj)\n}\n\nreturn msg;\n","outputs":1,"noerr":0,"x":3610,"y":2760,"wires":[["a3100af0.716338"]]},{"id":"863b3a11.caffb8","type":"inject","z":"705d403b.f2b8","name":"Update view","topic":"","payload":"","payloadType":"date","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":3030,"y":2760,"wires":[["a3100af0.716338"]]},{"id":"66275f51.ec58b","type":"change","z":"705d403b.f2b8","name":"","rules":[{"t":"set","p":"new_fixture_zone_mapping.zoneID","pt":"flow","to":"payload","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":3490,"y":2600,"wires":[[]]},{"id":"551ae558.ece26c","type":"ui_dropdown","z":"705d403b.f2b8","name":"Fixture Select","label":"","tooltip":"","place":"Select Fixture","group":"1598a8ff.884b77","order":1,"width":"3","height":"1","passthru":true,"options":[],"payload":"","topic":"","x":3220,"y":2580,"wires":[["aed3ce62.0a993"]]},{"id":"b7e6e6f8.691188","type":"function","z":"705d403b.f2b8","name":"","func":"msg.options = Object.keys((global.get(\"home.light.config.fixtures\")))\nreturn msg;","outputs":1,"noerr":0,"x":3050,"y":2580,"wires":[["551ae558.ece26c","cb5e87f1.d11458"]]},{"id":"8a237566.217618","type":"inject","z":"705d403b.f2b8","name":"manually refresh select lists","topic":"","payload":"","payloadType":"date","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":2760,"y":2580,"wires":[["b7e6e6f8.691188","a944297e.5ee6f8"]]},{"id":"cb5e87f1.d11458","type":"debug","z":"705d403b.f2b8","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","x":3180,"y":2520,"wires":[]},{"id":"3ebfbf6e.973d6","type":"ui_dropdown","z":"705d403b.f2b8","name":"Zone Select","label":"","tooltip":"","place":"Select Zone","group":"1598a8ff.884b77","order":2,"width":"3","height":"1","passthru":true,"options":[],"payload":"","topic":"","x":3210,"y":2600,"wires":[["66275f51.ec58b"]]},{"id":"a944297e.5ee6f8","type":"function","z":"705d403b.f2b8","name":"","func":"msg.options = Object.keys((global.get(\"home.light.config.zones\")))\nreturn msg;","outputs":1,"noerr":0,"x":3050,"y":2600,"wires":[["3ebfbf6e.973d6"]]},{"id":"1a37e545.435d2b","type":"ui_group","z":"","name":"New Fixture","tab":"869c06ba.dfea88","order":1,"disp":true,"width":"12","collapse":false},{"id":"1386e1df.83b3ae","type":"ui_group","z":"","name":"New Zone","tab":"869c06ba.dfea88","order":2,"disp":true,"width":"12","collapse":false},{"id":"1598a8ff.884b77","type":"ui_group","z":"","name":"Fixture to Zone Assignment","tab":"869c06ba.dfea88","order":3,"disp":true,"width":"12","collapse":false},{"id":"869c06ba.dfea88","type":"ui_tab","z":"","name":"Manage Presets","icon":"dashboard","disabled":false,"hidden":false}]

Here's a zoomed-out screenshot of the Dashboard (flow in previous post)

ALSO: I have no idea why I'm calling them "fixtures" as opposed to "fittings", just looked it up and apparently it's a US English thing. (I'm UK!)

If you want to complicate it further, in NEC/NFPA documentation (official fire/electrical building code adopted by most of the US) any light is referred to as a "luminaire" to distinguish it as a receptacle explicitly wired for lighting (different load characteristics from the typical outlet in a worst case scenario.)

Hi, I now done the 5th iteration (Yes, "(Software-) design is an iterative process :wink:
I redone everything to make the code easy to be reusable. All is configured now with topics. But in the end (so around 02:30) I found out that the ui_List does not support topics :frowning: I patched the node and send a pull request...
I have to do some tests tomorrow before I will post the flow here.

That's very responsible / perfectionistic of you :white_check_mark:

Unfortunately, I have no shame, so I'll post the updated / unfinished version of my lighting / zone / scene "database" project here :stuck_out_tongue:

  • Updated version shows "luminaire" to "zone" mappings within the "zone" table, each with its own "remove" button, which removes the mapping.
  • There are notes in the comment node within the flow posted here, more work to do. e.g. delete mappings when you delete a light or a zone.
[{"id":"8a93364b.187ff8","type":"comment","z":"705d403b.f2b8","name":"Version 2","info":"Stuff to tidy\n- delete zone -> remove mappings\n- delete fixture -> remove mappings\n\nRight now if we delete a zone, when there's\nalready a mapping, it crashes the zone view!\n\nWe should therefore do the above AND\nput some error checking into the zone list\nget function.\n\n\n6/10/2019\n---------\nStart with the things we want to do / make easy for installation\n- Ongoing\n - tweak lighting scenes\n - re-assign roving lights to different zones\n\n- Setup\n - Create fixings from the UI? If we are using DMX we could standardise the\n     output message, and set this in the UI as well. Otherwise, what's the\n     point? We'd still have to create the thing in Node-RED that sends the\n     message to the light. Well, maybe there is a point. Maybe it's just\n     useful to see the contents of memory in realtime from the front end,\n     as you add devices. We could set the MQTT path from front end as well.\n     Then all we'd have to do is set up the device and \"attach\" to the MQTT\n     path.\n - Create zones and map fixings to zones\n - Create zone presets\n   - List out the zones and mapped fixings\n   - List out the zone presets\n\nThen move on to how we want to store stuff in memory.\n- home.light.fixture\n- home.light.zone\n- home.light.preset\n- home.light.mapping.fixture_zone\n- home.light.mapping.preset_zone\n\n\n\nLights are standalone items\nWithin their definition, their location is not set\n\nWe also have room definitions, each room is a simple list of light\nindexes.\n\n\n{\n\t\"lights\" : [\n\t\t{\n\t\t\t\"id\"\t\t\t:\t\"3f_bedroom_ceiling\",\n\t\t\t\"friendly\"\t\t: \t\"3rd floor bedroom ceiling\",\n\t\t\t\"type\"\t\t\t:\t\"dimmable single colour\",\n\t\t\t\"topic\"\t\t\t:\t\"home/light/1\",\n\t\t\t\"defaultstate\"\t:\t20,\n\t\t\t\"currentstate\"\t:\t50\n\t\t},\n\t\t{\n\t\t\t\"id\"\t\t\t:\t\"3f_bedroom_bedside\",\n\t\t\t\"friendly\"\t\t: \t\"3rd floor bedroom bedside\",\n\t\t\t\"type\"\t\t\t:\t\"dimmable single colour\",\n\t\t\t\"topic\"\t\t\t:\t\"home/light/2\",\n\t\t\t\"defaultstate\"\t:\t20,\n\t\t\t\"currentstate\"\t:\t50\n\t\t}\n\t],\n\t\"zones\"\t: [\n\t\t{\n\t\t\t\"id\"\t\t\t: \"3f_bedroom\",\n\t\t\t\"friendly\"\t\t: \"Master bedroom\"\n\t\t\t\"members\"\t\t: [\"3f_bedroom_ceiling\", \"3f_bedroom_bedside\"],\n\t\t\t\"scenes\"\t\t: [\n\t\t\t\t{\n\t\t\t\t\t\"Name\":\"Off\",\n\t\t\t\t\t\"Values\" : [\n\t\t\t\t\t\t\n\t\t\t\t\t]\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"id\"\t\t\t: \"3f_living\",\n\t\t\t\"friendly\"\t\t: \"Living Room\",\n\t\t\t\"members\"\t\t: [\"3f_living_ceiling\", \"3f_living_wall\",\"huego1\"]\n\t\t}\n\t]\n}\n\n\n","x":2800,"y":1860,"wires":[]},{"id":"7dc1ca73.ae1494","type":"ui_text_input","z":"705d403b.f2b8","name":"","label":"ID","tooltip":"","group":"1a37e545.435d2b","order":1,"width":3,"height":1,"passthru":false,"mode":"text","delay":"350","topic":"","x":3150,"y":1900,"wires":[["e5afc774.989118"]]},{"id":"e5afc774.989118","type":"change","z":"705d403b.f2b8","name":"","rules":[{"t":"set","p":"newfixture.ID","pt":"flow","to":"payload","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":3420,"y":1900,"wires":[[]]},{"id":"40d1dfbc.93ecb","type":"ui_button","z":"705d403b.f2b8","name":"","group":"1a37e545.435d2b","order":5,"width":1,"height":1,"passthru":false,"label":"Save","tooltip":"","color":"","bgcolor":"","icon":"","payload":"nothing","payloadType":"str","topic":"","x":2810,"y":2020,"wires":[["678588e9.2d24c8"]]},{"id":"7dbff401.90176c","type":"ui_text_input","z":"705d403b.f2b8","name":"","label":"Friendly location","tooltip":"","group":"1a37e545.435d2b","order":2,"width":3,"height":1,"passthru":true,"mode":"text","delay":"1000","topic":"","x":3180,"y":1920,"wires":[["65ce0f79.994dc"]]},{"id":"65ce0f79.994dc","type":"change","z":"705d403b.f2b8","name":"","rules":[{"t":"set","p":"newfixture.friendly_location","pt":"flow","to":"payload","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":3460,"y":1920,"wires":[[]]},{"id":"678588e9.2d24c8","type":"function","z":"705d403b.f2b8","name":"New fixture","func":"// Save new fixture entered in Dashboard to global context\n// Includes form validation and duplicate check\n// and supports clearing the form when done correctly\n\n// check form was filled in properly\nif (typeof flow.get(\"newfixture\") == 'undefined') {\n    msg.payload = \"You didn't enter anything - fixture not saved\"\n    msg.error = true\n    return msg\n}\n\n// assume newfixture object exists (some part of form was filled)\nvar newfixture = flow.get(\"newfixture\")\n\n// if form not complete\nif (typeof newfixture.ID == 'undefined' ||\n           newfixture.ID === \"\" ||\n    typeof newfixture.friendly_location == 'undefined' ||\n           newfixture.friendly_location === \"\" ||\n    typeof newfixture.friendly_name == 'undefined' ||\n           newfixture.friendly_name === \"\" ||\n    typeof newfixture.type == 'undefined' ||\n           newfixture.type === \"\") {\n    msg.payload = \"Missing info - fixture not saved\"\n    msg.error = true\n    return msg\n}\n\n// if we've got this far, we have a good new fixture to add\n// create global fixtures object if it's not there\nif (typeof global.get(\"home.light.config.fixtures\") == 'undefined') {\n    global.set(\"home.light.config.fixtures\", {})\n}\n\n// get light fixtures object\nvar fixtures = global.get(\"home.light.config.fixtures\")\n\n// does fixture ID exist?\n\nif (fixtures.hasOwnProperty(newfixture.ID)) {\n    msg.payload = \"oops, ID already exists\"\n    msg.error = true\n    return msg\n} else {\n    d = new Date().toLocaleDateString(); t = new Date().toLocaleTimeString();\n    fixtures[newfixture.ID] = {\n        \"friendly_location\": newfixture.friendly_location,\n        \"friendly_name\": newfixture.friendly_name,\n        \"type\": newfixture.type,\n        \"created_date\": d,\n        \"created_time\": t\n    }\n    \n    global.set(\"home.light.config.fixtures\",fixtures)\n    msg.payload = \"Success - added fixture\"\n    msg.error = false\n    // now remove item from flow context, otherwise our\n    // validation tests above will always pass\n    flow.set(\"newfixture\", undefined);\n    \n    return msg\n}","outputs":1,"noerr":0,"x":2950,"y":2020,"wires":[["13efd823.365108","e05ac37e.8fa61"]]},{"id":"178c7bb6.0832d4","type":"ui_dropdown","z":"705d403b.f2b8","name":"Type","label":"","tooltip":"","place":"Control Type","group":"1a37e545.435d2b","order":4,"width":3,"height":1,"passthru":true,"options":[{"label":"Non Dimmable (Switch)","value":"1_non_dim","type":"str"},{"label":"Dimmable Single Colour","value":"2_single_colour","type":"str"},{"label":"RGB","value":"3_rgb","type":"str"},{"label":"RGBW","value":"4_RGBW","type":"str"},{"label":"RGBWW","value":"5_RGBWW","type":"str"},{"label":"Individually Addressable","value":"6_individually_addressable","type":"str"}],"payload":"","topic":"","x":3150,"y":1960,"wires":[["8715f8dc.f16588"]]},{"id":"8715f8dc.f16588","type":"change","z":"705d403b.f2b8","name":"","rules":[{"t":"set","p":"newfixture.type","pt":"flow","to":"payload","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":3430,"y":1960,"wires":[[]]},{"id":"13efd823.365108","type":"ui_template","z":"705d403b.f2b8","group":"1a37e545.435d2b","name":"New fixture validation","order":6,"width":"8","height":1,"format":"{{msg.payload}}","storeOutMessages":true,"fwdInMessages":false,"templateScope":"local","x":3400,"y":2020,"wires":[[]]},{"id":"efc2b541.5fa958","type":"delay","z":"705d403b.f2b8","name":"","pauseType":"delay","timeout":"5","timeoutUnits":"seconds","rate":"1","nbRateUnits":"1","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":false,"x":3220,"y":2060,"wires":[["13efd823.365108"]]},{"id":"e05ac37e.8fa61","type":"function","z":"705d403b.f2b8","name":"Success","func":"// Save new fixture: clear form & result if successful\n\nif (!msg.error) {\n    msg.payload = \"\"\n    return msg;\n}\n","outputs":1,"noerr":0,"x":3060,"y":2060,"wires":[["efc2b541.5fa958","7dc1ca73.ae1494","7dbff401.90176c","178c7bb6.0832d4","9126cc5a.af8f3","c501b125.62688"]]},{"id":"77a3d35d.3ceecc","type":"change","z":"705d403b.f2b8","name":"","rules":[{"t":"set","p":"newfixture.friendly_name","pt":"flow","to":"payload","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":3460,"y":1940,"wires":[[]]},{"id":"9126cc5a.af8f3","type":"ui_text_input","z":"705d403b.f2b8","name":"","label":"Friendly Fitting Type","tooltip":"","group":"1a37e545.435d2b","order":3,"width":3,"height":1,"passthru":true,"mode":"text","delay":"1000","topic":"","x":3200,"y":1940,"wires":[["77a3d35d.3ceecc"]]},{"id":"c501b125.62688","type":"change","z":"705d403b.f2b8","name":"Get","rules":[{"t":"set","p":"fixtures","pt":"msg","to":"home.light.config.fixtures","tot":"global"}],"action":"","property":"","from":"","to":"","reg":false,"x":3210,"y":2120,"wires":[["8c47badb.7f7df8"]]},{"id":"8c47badb.7f7df8","type":"ui_template","z":"705d403b.f2b8","group":"1a37e545.435d2b","name":"Fixtures List","order":7,"width":"12","height":"8","format":"<h3>Register of Light Fixtures</h3>\n\n<table>\n    <tr style=\"font-weight:800; background-color:rgba(20,20,20,.6);\">\n        <td>ID</td>\n        <td>Location</td>\n        <td>Name</td>\n        <td>Type</td>\n        <td>Added</td>\n        <td>Delete*</td>\n    </tr>\n    <tr ng-repeat=\"(key, value) in msg.fixtures\">\n        <td>{{key}}</td>\n        <td>{{value.friendly_location}}</td>\n        <td>{{value.friendly_name}}</td>\n        <td>{{value.type}}</td>\n        <td style=\"color:#888;\">{{value.created_time}} {{value.created_date}}</td>\n        <td><md-button ng-click=\"send({action: 'delete', topic: key})\">Del</md-button></td>\n    </tr>\n</table>","storeOutMessages":false,"fwdInMessages":false,"templateScope":"local","x":3350,"y":2120,"wires":[["7dadd06a.5a505"]]},{"id":"7dadd06a.5a505","type":"function","z":"705d403b.f2b8","name":"Set","func":"// get object from memory\n// NB we are using \"bracket notation\" just in case the ID\n// (i.e. the object name, represented here as msg.topic)\n// had a space\n\nif (msg.action == \"delete\") {\n    obj = global.get(\"home.light.config.fixtures\")\n    delete obj[msg.topic]\n    global.set(\"home.light.config.fixtures\", obj)\n}\n\nreturn msg;\n","outputs":1,"noerr":0,"x":3490,"y":2120,"wires":[["c501b125.62688"]]},{"id":"5ea5059b.ac0cac","type":"inject","z":"705d403b.f2b8","name":"Update view","topic":"","payload":"","payloadType":"date","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":3030,"y":2120,"wires":[["c501b125.62688"]]},{"id":"4714f8ee.5b3398","type":"ui_text_input","z":"705d403b.f2b8","name":"","label":"ID","tooltip":"","group":"1386e1df.83b3ae","order":1,"width":3,"height":1,"passthru":false,"mode":"text","delay":"350","topic":"","x":3150,"y":2240,"wires":[["7797971f.f78778"]]},{"id":"7797971f.f78778","type":"change","z":"705d403b.f2b8","name":"","rules":[{"t":"set","p":"newzone.ID","pt":"flow","to":"payload","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":3420,"y":2240,"wires":[[]]},{"id":"5b4809a9.073948","type":"ui_button","z":"705d403b.f2b8","name":"","group":"1386e1df.83b3ae","order":3,"width":1,"height":1,"passthru":false,"label":"Save","tooltip":"","color":"","bgcolor":"","icon":"","payload":"nothing","payloadType":"str","topic":"","x":2810,"y":2320,"wires":[["77413dc7.e8c9b4"]]},{"id":"77413dc7.e8c9b4","type":"function","z":"705d403b.f2b8","name":"New zone","func":"// Save new zone entered in Dashboard to global context\n// Includes form validation and duplicate check\n// and supports clearing the form when done correctly\n\n// check form was filled in properly\nif (typeof flow.get(\"newzone\") == 'undefined') {\n    msg.payload = \"You didn't enter anything - zone not saved\"\n    msg.error = true\n    return msg\n}\n\n// assume newzone object exists (some part of form was filled)\nvar newzone = flow.get(\"newzone\")\n\n// if form not complete\nif (typeof newzone.ID == 'undefined' ||\n           newzone.ID === \"\" ||\n    typeof newzone.friendly_name == 'undefined' ||\n           newzone.friendly_name === \"\") {\n    msg.payload = \"Missing info - zone not saved\"\n    msg.error = true\n    return msg\n}\n\n// if we've got this far, we have a good new zone to add\n// create global zones object if it's not there\nif (typeof global.get(\"home.light.config.zones\") == 'undefined') {\n    global.set(\"home.light.config.zones\", {})\n}\n\n// get light zones object\nvar zones = global.get(\"home.light.config.zones\")\n\n// does fixture ID exist?\n\nif (zones.hasOwnProperty(newzone.ID)) {\n    msg.payload = \"oops, ID already exists\"\n    msg.error = true\n    return msg\n} else {\n    d = new Date().toLocaleDateString(); t = new Date().toLocaleTimeString();\n    zones[newzone.ID] = {\n        \"friendly_name\": newzone.friendly_name,\n        \"created_date\": d,\n        \"created_time\": t\n    }\n    \n    global.set(\"home.light.config.zones\",zones)\n    msg.payload = \"Success - added zone\"\n    msg.error = false\n    // now remove item from flow context, otherwise our\n    // validation tests above will always pass\n    flow.set(\"newzone\", undefined);\n    \n    return msg\n}","outputs":1,"noerr":0,"x":2940,"y":2320,"wires":[["58c8c589.5d48dc","549ddba1.fb5e44"]]},{"id":"58c8c589.5d48dc","type":"ui_template","z":"705d403b.f2b8","group":"1386e1df.83b3ae","name":"New zone validation","order":4,"width":"8","height":1,"format":"{{msg.payload}}","storeOutMessages":true,"fwdInMessages":false,"templateScope":"local","x":3400,"y":2320,"wires":[[]]},{"id":"dcf2a212.18aa1","type":"delay","z":"705d403b.f2b8","name":"","pauseType":"delay","timeout":"5","timeoutUnits":"seconds","rate":"1","nbRateUnits":"1","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":false,"x":3220,"y":2360,"wires":[["58c8c589.5d48dc"]]},{"id":"549ddba1.fb5e44","type":"function","z":"705d403b.f2b8","name":"Success","func":"// Save new zone: clear form & result if successful\n\nif (!msg.error) {\n    msg.payload = \"\"\n    return msg;\n}\n","outputs":1,"noerr":0,"x":3060,"y":2360,"wires":[["dcf2a212.18aa1","4714f8ee.5b3398","ddc87a2a.8ae778","6a236e08.0a12d"]]},{"id":"8a402bb0.963de8","type":"ui_template","z":"705d403b.f2b8","group":"1386e1df.83b3ae","name":"Zones List","order":5,"width":"12","height":"8","format":"<style>\n    table {border: none;}\n    .nr-dashboard-theme .nr-dashboard-template .md-button:not(:first-of-type) {\n    margin-top: 0px;\n    }\n    .nr-dashboard-theme .nr-dashboard-template .md-button {\n    margin-right: 0 10px 0 0;\n    min-height: 0;\n    min-width: unset;\n    line-height: unset;\n    height: 21px;\n    }\n    .tags {\n    font-size: 11px;\n    color: #ddd;\n    border: 1px solid #555;\n    background-color: rgba(9, 116, 121, 0.31);\n    padding: 2px;\n    margin: 0 0px 0 5px;\n    }\n    .datarow td {\n    padding: 5px 0 0px 0px;\n    background-color: #404040;\n    }\n    .tagcol td {\n    padding: 0 0 10px 0;\n    background: #404040;\n    border-bottom: 10px solid #333;   \n    }\n    .tagcol {\n\n    }\n</style>\n\n<h3>Register of Light Zones</h3>\n\n<table cellspacing=\"0\" cellpadding=\"0\">\n    <tr style=\"font-weight:800; background-color:rgba(20,20,20,.6);\">\n        <td>ID</td>\n        <td>Name</td>\n        <td>Added</td>\n        <td>Delete*</td>\n    </tr>\n    <tr class=\"datarow\" ng-repeat-start=\"(key, value) in msg.zones\">\n        <td>{{key}}</td>\n        <td>{{value.friendly_name}}</td>\n        <td style=\"color:#888;\">{{value.created_time}} {{value.created_date}}</td>\n        <td><md-button ng-click=\"send({action: 'delete', topic: key})\">Del</md-button></td>\n    </tr>\n    <tr class=\"tagcol\" ng-repeat-end>\n        <td colspan=\"5\">\n            <span class=\"tags\" ng-repeat-start=\"val1 in value.fixtures\">{{val1}}</span>\n            <md-button class=\"minibutton\" ng-repeat-end ng-click=\"send({action: 'deletefixturemapping', topic: [key,val1]})\">x</md-button>\n        </td>\n    </tr>\n</table>\n","storeOutMessages":false,"fwdInMessages":false,"templateScope":"local","x":3350,"y":2420,"wires":[["ab08a35d.f692d","cb5e87f1.d11458"]]},{"id":"ab08a35d.f692d","type":"function","z":"705d403b.f2b8","name":"Set","func":"// get object from memory\n// NB we are using \"bracket notation\" just in case the ID\n// (i.e. the object name, represented here as msg.topic)\n// had a space\n\nif (msg.action == \"delete\") {\n    obj = global.get(\"home.light.config.zones\")\n    delete obj[msg.topic]\n    global.set(\"home.light.config.zones\", obj)\n}\n\nif (msg.action == \"deletefixturemapping\") {\n    obj = global.get(\"home.light.config.fixture_zone_mappings\")\n    delete obj[msg.topic[0] + \"_\" + msg.topic[1]]\n    global.set(\"home.light.config.fixture_zone_mappings\", obj)\n}\n\nreturn msg;\n","outputs":1,"noerr":0,"x":3490,"y":2420,"wires":[["6a236e08.0a12d","e2fb5079.17931"]]},{"id":"1dabe67a.a9504a","type":"inject","z":"705d403b.f2b8","name":"Update view","topic":"","payload":"","payloadType":"date","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":3030,"y":2420,"wires":[["6a236e08.0a12d"]]},{"id":"ddc87a2a.8ae778","type":"ui_text_input","z":"705d403b.f2b8","name":"","label":"Friendly Name","tooltip":"","group":"1386e1df.83b3ae","order":2,"width":3,"height":1,"passthru":true,"mode":"text","delay":"1000","topic":"","x":3180,"y":2260,"wires":[["9c619228.80fde"]]},{"id":"9c619228.80fde","type":"change","z":"705d403b.f2b8","name":"","rules":[{"t":"set","p":"newzone.friendly_name","pt":"flow","to":"payload","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":3450,"y":2260,"wires":[[]]},{"id":"aed3ce62.0a993","type":"change","z":"705d403b.f2b8","name":"","rules":[{"t":"set","p":"new_fixture_zone_mapping.fixtureID","pt":"flow","to":"payload","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":3490,"y":2580,"wires":[[]]},{"id":"c45f942b.25fb78","type":"ui_button","z":"705d403b.f2b8","name":"","group":"1598a8ff.884b77","order":3,"width":1,"height":1,"passthru":false,"label":"Assign","tooltip":"","color":"","bgcolor":"","icon":"","payload":"nothing","payloadType":"str","topic":"","x":2710,"y":2660,"wires":[["bcc049ac.db8cd8"]]},{"id":"bcc049ac.db8cd8","type":"function","z":"705d403b.f2b8","name":"New fixture_zone_mapping","func":"// NEEDS RE-WRITING FOR FIXTURE TO ZONE ASSIGNMENT\n// ID will be automatically generated as per:\n// zoneID_fixtureID\n// if such an ID exists, then message: already exists in zone\n// Don't forget to update code for deleting a zone and\n// a fixture. Each should check for corresponding entries\n// in the mappings object and remove these entries\n\n\n// Save new zone entered in Dashboard to global context\n// Includes form validation and duplicate check\n// and supports clearing the form when done correctly\n\n// check form was filled in properly\nif (typeof flow.get(\"new_fixture_zone_mapping\") == 'undefined') {\n    msg.payload = \"You didn't enter anything - fixture not assigned\"\n    msg.error = true\n    return msg\n}\n\n// assume newzone object exists (some part of form was filled)\nvar new_fixture_zone_mapping = flow.get(\"new_fixture_zone_mapping\")\n\n// if form not complete\nif (typeof new_fixture_zone_mapping.fixtureID == 'undefined' ||\n           new_fixture_zone_mapping.fixtureID === \"\" ||\n    typeof new_fixture_zone_mapping.zoneID == 'undefined' ||\n           new_fixture_zone_mapping.zoneID === \"\") {\n    msg.payload = \"Missing info - fixture not assigned\"\n    msg.error = true\n    return msg\n}\n\n// if we've got this far, we have a good assignment to create\n// create global assignment object if it's not there\nif (typeof global.get(\"home.light.config.fixture_zone_mappings\") == 'undefined') {\n    global.set(\"home.light.config.fixture_zone_mappings\", {})\n}\n\n// get light fixture zone mappings object\nvar fixture_zone_mappings = global.get(\"home.light.config.fixture_zone_mappings\")\n\n// does mapping ID exist? (it will look like this: zoneID_fixtureID)\nvar newID = new_fixture_zone_mapping.zoneID + \"_\" + new_fixture_zone_mapping.fixtureID\n\nif (fixture_zone_mappings.hasOwnProperty(newID)) {\n    msg.payload = \"oops, fixture already added to zone\"\n    msg.error = true\n    return msg\n} else {\n    d = new Date().toLocaleDateString(); t = new Date().toLocaleTimeString();\n    fixture_zone_mappings[newID] = {\n        \"zoneID\": new_fixture_zone_mapping.zoneID,        // technically not necessary\n        \"fixtureID\": new_fixture_zone_mapping.fixtureID,  // technically not necessary\n        \"created_date\": d,\n        \"created_time\": t\n    }\n    \n    global.set(\"home.light.config.fixture_zone_mappings\",fixture_zone_mappings)\n    msg.payload = \"Success - assigned fixture to zone\"\n    msg.error = false\n    // now remove item from flow context, otherwise our\n    // validation tests above will always pass\n    flow.set(\"new_fixture_zone_mapping\", undefined);\n    \n    return msg\n}","outputs":1,"noerr":0,"x":2900,"y":2660,"wires":[["4bafda67.14c724","c903cb21.a24cd8"]]},{"id":"4bafda67.14c724","type":"ui_template","z":"705d403b.f2b8","group":"1598a8ff.884b77","name":"New fixture-zone validation","order":4,"width":"8","height":1,"format":"{{msg.payload}}","storeOutMessages":true,"fwdInMessages":false,"templateScope":"local","x":3420,"y":2660,"wires":[[]]},{"id":"121995cb.dae1aa","type":"delay","z":"705d403b.f2b8","name":"","pauseType":"delay","timeout":"5","timeoutUnits":"seconds","rate":"1","nbRateUnits":"1","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":false,"x":3220,"y":2700,"wires":[["4bafda67.14c724"]]},{"id":"c903cb21.a24cd8","type":"function","z":"705d403b.f2b8","name":"Success","func":"// Save new zone: clear form & result if successful\n\nif (!msg.error) {\n    msg.payload = \"\"\n    return msg;\n}\n","outputs":1,"noerr":0,"x":3060,"y":2700,"wires":[["121995cb.dae1aa","a3100af0.716338","b7e6e6f8.691188","a944297e.5ee6f8","1e90979b.b14858"]]},{"id":"a3100af0.716338","type":"change","z":"705d403b.f2b8","name":"Get","rules":[{"t":"set","p":"fixture_zone_mappings","pt":"msg","to":"home.light.config.fixture_zone_mappings","tot":"global"}],"action":"","property":"","from":"","to":"","reg":false,"x":3210,"y":2760,"wires":[["4e545849.410518"]]},{"id":"4e545849.410518","type":"ui_template","z":"705d403b.f2b8","group":"1598a8ff.884b77","name":"Fixture to Zone Mappings List","order":5,"width":"12","height":"8","format":"<h3>Register of Fixture to Zone Mappings</h3>\n\n<table>\n    <tr style=\"font-weight:800; background-color:rgba(20,20,20,.6);\">\n        <td>ID</td>\n        <td>Zone</td>\n        <td>Fixture</td>\n        <td>Added</td>\n        <td>Delete*</td>\n    </tr>\n    <tr ng-repeat=\"(key, value) in msg.fixture_zone_mappings\">\n        <td>{{key}}</td>\n        <td>{{value.zoneID}}</td>\n        <td>{{value.fixtureID}}</td>\n        <td style=\"color:#888;\">{{value.created_time}} {{value.created_date}}</td>\n        <td><md-button ng-click=\"send({action: 'delete', topic: key})\">Del</md-button></td>\n    </tr>\n</table>","storeOutMessages":false,"fwdInMessages":false,"templateScope":"local","x":3410,"y":2760,"wires":[["c0c7b85b.4670e8"]]},{"id":"c0c7b85b.4670e8","type":"function","z":"705d403b.f2b8","name":"Set","func":"// get object from memory\n// NB we are using \"bracket notation\" just in case the ID\n// (i.e. the object name, represented here as msg.topic)\n// had a space\n\nif (msg.action == \"delete\") {\n    obj = global.get(\"home.light.config.fixture_zone_mappings\")\n    delete obj[msg.topic]\n    global.set(\"home.light.config.fixture_zone_mappings\", obj)\n}\n\n\nreturn msg;\n","outputs":1,"noerr":0,"x":3610,"y":2760,"wires":[["a3100af0.716338","1e90979b.b14858"]]},{"id":"863b3a11.caffb8","type":"inject","z":"705d403b.f2b8","name":"Update view","topic":"","payload":"","payloadType":"date","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":3030,"y":2760,"wires":[["a3100af0.716338"]]},{"id":"66275f51.ec58b","type":"change","z":"705d403b.f2b8","name":"","rules":[{"t":"set","p":"new_fixture_zone_mapping.zoneID","pt":"flow","to":"payload","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":3490,"y":2600,"wires":[[]]},{"id":"551ae558.ece26c","type":"ui_dropdown","z":"705d403b.f2b8","name":"Fixture Select","label":"","tooltip":"","place":"Select Fixture","group":"1598a8ff.884b77","order":1,"width":"3","height":"1","passthru":true,"options":[],"payload":"","topic":"","x":3220,"y":2580,"wires":[["aed3ce62.0a993"]]},{"id":"b7e6e6f8.691188","type":"function","z":"705d403b.f2b8","name":"","func":"msg.options = Object.keys((global.get(\"home.light.config.fixtures\")))\nreturn msg;","outputs":1,"noerr":0,"x":3050,"y":2580,"wires":[["551ae558.ece26c"]]},{"id":"8a237566.217618","type":"inject","z":"705d403b.f2b8","name":"manually refresh select lists","topic":"","payload":"","payloadType":"date","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":2760,"y":2580,"wires":[["b7e6e6f8.691188","a944297e.5ee6f8"]]},{"id":"cb5e87f1.d11458","type":"debug","z":"705d403b.f2b8","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","x":3530,"y":2500,"wires":[]},{"id":"3ebfbf6e.973d6","type":"ui_dropdown","z":"705d403b.f2b8","name":"Zone Select","label":"","tooltip":"","place":"Select Zone","group":"1598a8ff.884b77","order":2,"width":"3","height":"1","passthru":true,"options":[],"payload":"","topic":"","x":3210,"y":2600,"wires":[["66275f51.ec58b"]]},{"id":"a944297e.5ee6f8","type":"function","z":"705d403b.f2b8","name":"","func":"msg.options = Object.keys((global.get(\"home.light.config.zones\")))\nreturn msg;","outputs":1,"noerr":0,"x":3050,"y":2600,"wires":[["3ebfbf6e.973d6"]]},{"id":"6a236e08.0a12d","type":"function","z":"705d403b.f2b8","name":"Get","func":"// list out the zones\n// also show light fixtures assigned to each zone\n// as zones and assignments are two separate objects,\n// we shall insert the mappings for a given zone as an array into the\n// zone object returned\nmsg.zones = {}\nzones = global.get(\"home.light.config.zones\")\n\n// copy our zones into the msg to send out, also set up fixtures array\n// so we can populate this for the list view\nObject.keys(zones).forEach(key => {\n    msg.zones[key] = {\n        \"friendly_name\": zones[key].friendly_name,\n        \"created_date\": zones[key].created_date,\n        \"created_time\": zones[key].created_time,\n        \"fixtures\": []\n    }\n})\n\n\n// now populate fixtures arrays in each zone\n// loop through every mapping, get the zoneID, then insert the fixtureID\n// into the corresponding zone now in msg.zones\n\nfixture_zone_mappings = global.get(\"home.light.config.fixture_zone_mappings\")\n\nObject.keys(fixture_zone_mappings).forEach(key => {\n    // (the IF ensures we don't insert fixture into zone that\n    // does not exist). It will just fail silently.\n    // Right now, the zone won't be shown, but the mappings will still\n    // exist. We should remove mappings when zone is removed...\n    if (typeof msg.zones[fixture_zone_mappings[key].zoneID] !== 'undefined') {\n        msg.zones[fixture_zone_mappings[key].zoneID].fixtures.push(fixture_zone_mappings[key].fixtureID)\n    }\n\n//   console.log(key);        // the name of the current key.\n//   console.log(myObj[key]); // the value of the current key.\n});\n\nreturn msg","outputs":1,"noerr":0,"x":3210,"y":2420,"wires":[["8a402bb0.963de8"]]},{"id":"f37b3ccd.a5de4","type":"link in","z":"705d403b.f2b8","name":"To zone","links":["1e90979b.b14858"],"x":3075,"y":2460,"wires":[["6a236e08.0a12d"]]},{"id":"1e90979b.b14858","type":"link out","z":"705d403b.f2b8","name":"From fixture_zone_mapping","links":["f37b3ccd.a5de4"],"x":3175,"y":2820,"wires":[]},{"id":"e2fb5079.17931","type":"link out","z":"705d403b.f2b8","name":"From zone","links":["e4329ecb.85cc8"],"x":3615,"y":2420,"wires":[]},{"id":"e4329ecb.85cc8","type":"link in","z":"705d403b.f2b8","name":"To fixture_zone_mapping","links":["e2fb5079.17931"],"x":3075,"y":2820,"wires":[["a3100af0.716338"]]},{"id":"1a37e545.435d2b","type":"ui_group","z":"","name":"New Fixture","tab":"869c06ba.dfea88","order":1,"disp":true,"width":"12","collapse":false},{"id":"1386e1df.83b3ae","type":"ui_group","z":"","name":"New Zone","tab":"869c06ba.dfea88","order":2,"disp":true,"width":"12","collapse":false},{"id":"1598a8ff.884b77","type":"ui_group","z":"","name":"Fixture to Zone Assignment","tab":"869c06ba.dfea88","order":3,"disp":true,"width":"12","collapse":false},{"id":"869c06ba.dfea88","type":"ui_tab","z":"","name":"Manage Presets","icon":"dashboard","disabled":false,"hidden":false}]

I had same concerns, now storing all vars in global context. But wanted to ensure it is ‘future proof’, ie, wanted to follow some ‘standard’, where orhers helps to think theough the next requirement. Then I learnt about homie. See

https://homieiot.github.io/

So i structured an object that I store, and update at the right ‘leaf node’ in global context and read for dash and to transform to update HomeKit, which follows the deviceID / nodeID / properties / attributes.

Finally went live this weekend, it really works well.

The Homie convention, nice. It's on my To-do list for my ESP nodes as well. :slightly_smiling_face:

Hi,
thank you for the Interest on the node-red-contrib-homie-convention node. This node already can build and maintain the "homie" tree in global context.

Is it possible to direct all homie related posts to this place.

Looking forward to get your feedback.

So ... finally here you are.

  • the data structure is defined by the topics: nameInStore.object[s].property seperated by dots.
  • nameInStore will be used as global.get("nameInStore","file") You have to edit this in ALL function nodes if you have a different store provider.
  • object[s] the object in witch the list[] should be stored
  • property name of the property or keyword to trigger a function (uiDropdown, uiAdd, uiDelete). The property name must be provided to store the name of a configuration.
    image
  • make sure that image is unchecked for ALL ui_Nodes EXCEPT the dropdown list
  • all ui_Nodes must provide the topic described above. You can use the most resend ui_List , this should have the possibility to enter a topic.
  • edit the addNew function node and add your default values
  • edit the switch node to match your topics in the ui_Nodes
    image
  • all single msg.payload input / output can be wired directly between the switch node and the save Data function node
  • for other nodes you have to add special nodes like the save List function node in the second example
  • last but not least enter the initial topic in the inject node
    image
    And this is the result in your store:

Version with switches:

[{"id":"e73adecd.24ffa","type":"ui_text_input","z":"897fdb2a.109f68","name":"name","label":"","tooltip":"","group":"d11141a.b59afc","order":5,"width":6,"height":1,"passthru":false,"mode":"text","delay":"0","topic":"mySettings.heating.test.name","x":590,"y":280,"wires":[["1ff3f5ad.ab0eca"]]},{"id":"74ea0435.e9838c","type":"ui_button","z":"897fdb2a.109f68","name":"uiDelete","group":"d11141a.b59afc","order":3,"width":1,"height":1,"passthru":false,"label":"","tooltip":"","color":"","bgcolor":"","icon":"delete","payload":"Do you really like to delete this entry?","payloadType":"str","topic":"mySettings.heating.test.uiDelete","x":600,"y":220,"wires":[["843a51.ab4095b"]]},{"id":"372cbb4a.c123c4","type":"function","z":"897fdb2a.109f68","name":"delete period","func":"node.status({});\nvar currentObject= global.get(msg.topic.slice(0,msg.topic.indexOf('.')),\"file\");\nif (!currentObject) return;\nvar currentProperty=currentObject.getEntry(msg.topic);\nif (!currentProperty) {\n    node.status({fill:\"red\",shape:\"dot\",text: msg.topic+\" not found\"});\n    return;\n}\nvar name=currentProperty.name;\n\ntry {\n    if (currentObject.deleteEntry(msg.topic)) {\n        node.status({fill:\"green\",shape:\"dot\",text:name+\" deleted\"});\n    } else {\n        node.status({fill:\"green\",shape:\"dot\",text:name+\" NOT deleted\"});\n    }\n    return msg;\n} catch (err) {\n    node.status({fill:\"red\",shape:\"dot\",text:\"ups, something gone wrong.\"});\n    console.log(err);\n} ","outputs":1,"noerr":0,"x":890,"y":220,"wires":[["d3df79d6.ec2598"]]},{"id":"bd2033fe.103aa","type":"ui_date_picker","z":"897fdb2a.109f68","name":"start","label":"","group":"d11141a.b59afc","order":7,"width":6,"height":1,"passthru":false,"topic":"mySettings.heating.test.start","x":590,"y":320,"wires":[["1ff3f5ad.ab0eca"]]},{"id":"4dbfe990.188838","type":"ui_date_picker","z":"897fdb2a.109f68","name":"end","label":"","group":"d11141a.b59afc","order":9,"width":6,"height":1,"passthru":false,"topic":"mySettings.heating.test.end","x":590,"y":360,"wires":[["1ff3f5ad.ab0eca"]]},{"id":"3159de8.1169f22","type":"ui_text","z":"897fdb2a.109f68","group":"d11141a.b59afc","order":4,"width":2,"height":1,"name":"Name:","label":"Name:","format":"{{msg.payload}}","layout":"row-spread","x":110,"y":280,"wires":[]},{"id":"5073b43e.635a4c","type":"ui_text","z":"897fdb2a.109f68","group":"d11141a.b59afc","order":6,"width":2,"height":1,"name":"Start:","label":"Begin:","format":"{{msg.payload}}","layout":"row-spread","x":110,"y":340,"wires":[]},{"id":"fd9ab15d.fbc79","type":"ui_text","z":"897fdb2a.109f68","group":"d11141a.b59afc","order":8,"width":2,"height":1,"name":"End:","label":"End:","format":"{{msg.payload}}","layout":"row-spread","x":110,"y":400,"wires":[]},{"id":"d793942.135c968","type":"ui_dropdown","z":"897fdb2a.109f68","name":"uiDropdown","label":"","tooltip":"","place":"select period","group":"d11141a.b59afc","order":1,"width":6,"height":1,"passthru":true,"options":[],"payload":"","topic":"mySettings.heating.test.uiDropdown","x":610,"y":100,"wires":[["7dbcf8cd.631618"]]},{"id":"79445713.d60a98","type":"function","z":"897fdb2a.109f68","name":"update","func":"var msgDropdown={};\nvar msgName={};\nvar msgList={};\nvar msgParameter=[];\n\nvar topicParts=msg.topic.split('.');\nvar currentObject= global.get(topicParts[0],\"file\");\nif (!currentObject) return;\nvar keyword = topicParts.pop();\nvar propertyTopic = msg.topic.slice(0,msg.topic.lastIndexOf('.'));\nvar currentProperty=currentObject.getObject(msg.topic);\nif (!currentProperty) {\n    node.status({fill:\"red\",shape:\"dot\",text:\"msg.topic='object.property' expected\"});\n    return;\n}\n\nmsgDropdown.topic=msg.topic;\n\ntry {\n    // fill an array for all parameters in list if exiting\n    if (currentProperty.list) { \n        for (var item in currentProperty.list[currentProperty.uiPointer]) {\n            if (!item.startsWith('ui')) \n                msgParameter.push({\"payload\":currentProperty.list[currentProperty.uiPointer][item], \"topic\": propertyTopic+'.'+item});\n        }\n    }    \n    switch (keyword) {\n        case \"uiDropdown\":\n            node.status({fill:\"green\",shape:\"dot\",text:\"Dropdown updated!\"});\n            return [null,msgParameter]; // update all parameters\n        case \"uiAdd\":\n            msgDropdown.payload=currentProperty.uiPointer;\n            node.status({fill:\"green\",shape:\"dot\",text:\"new entry nr=\"+currentProperty.uiPointer});\n            return [msgDropdown,null]; // update only dropdown, parametes will follw next call\n        case \"uiDelete\":\n            msgDropdown.payload=\"init\";\n            node.status({fill:\"green\",shape:\"dot\",text:\"entry deleted\"});\n            return [msgDropdown,null]; // update only dropdown, parametes will follw next call\n        case \"name\":\n            msgDropdown.payload=currentProperty.uiPointer;\n            node.status({fill:\"green\",shape:\"dot\",text:\"entry renamed!\"});\n            return [msgDropdown,null]; // update only dropdown\n    }\n    \n    node.status({fill:\"gray\",shape:\"dot\",text:msg.topic+\"=\"+msg.payload});\n    return null;\n} catch (err) {\n    node.status({fill:\"red\",shape:\"dot\",text:\"ups, somthing gone wrong. (\"+keyword+\")\"});\n    console.log(err);\n} ","outputs":2,"noerr":0,"x":190,"y":200,"wires":[["f022e935.c6ab48"],["7c078fcf.9a8e9"]]},{"id":"b51dc782.8c09d8","type":"link out","z":"897fdb2a.109f68","name":"updatePeriod","links":["bc9c5ee1.ceb48"],"x":995,"y":280,"wires":[]},{"id":"bc9c5ee1.ceb48","type":"link in","z":"897fdb2a.109f68","name":"updatePeriod","links":["b51dc782.8c09d8","750a4ef9.a29c9","480be8a7.ca3858","d3df79d6.ec2598","54d64382.c8d62c"],"x":95,"y":200,"wires":[["79445713.d60a98"]]},{"id":"750a4ef9.a29c9","type":"link out","z":"897fdb2a.109f68","name":"updatePeriod","links":["bc9c5ee1.ceb48"],"x":995,"y":100,"wires":[]},{"id":"1ff3f5ad.ab0eca","type":"function","z":"897fdb2a.109f68","name":"save Data","func":"node.status({});\nvar currentObject= global.get(msg.topic.slice(0,msg.topic.indexOf('.')),\"file\");\nif (!currentObject) return;\nvar currentProperty=currentObject.getEntry(msg.topic);\nif (!currentProperty) {\n    node.status({fill:\"red\",shape:\"dot\",text: msg.topic+\" not found\"});\n    return;\n}\nvar propertyName=msg.topic.slice(msg.topic.lastIndexOf('.')+1);\n\ntry {\n    // make shure your topic is set correctly in your ui nodes(s): objectName.propertyName\n    currentProperty[propertyName]=msg.payload;\n\n    node.status({fill:\"green\",shape:\"dot\",text:propertyName+\"=\"+msg.payload+\"!\"});\n    return msg;\n} catch (err) {\n    node.status({fill:\"red\",shape:\"dot\",text:\"ups, somthing gone wrong.\"});\n    console.log(err);\n} ","outputs":1,"noerr":0,"x":880,"y":280,"wires":[["b51dc782.8c09d8"]],"icon":"font-awesome/fa-database"},{"id":"f022e935.c6ab48","type":"function","z":"897fdb2a.109f68","name":"fill dropdown","func":"var topicParts=msg.topic.split('.');\nvar currentObject= global.get(topicParts[0],\"file\");\nif (!currentObject) return;\nvar keyword = topicParts.pop();\nvar currentProperty=currentObject.getObject(msg.topic);\nif (!currentProperty) {\n    node.status({fill:\"red\",shape:\"dot\",text:\"msg.topic='object.property' expected\"});\n    return;\n}\n\n//try {\n    console.log(currentProperty.list!==undefined)\n    if (currentProperty.list!==undefined) {\n        msg.options=[];\n        var listEntry={};\n        currentProperty.list.forEach(function(item,index){\n            listEntry={};\n            listEntry[item.name]=String(index);\n            msg.options.push(listEntry);\n        })\n        msg.options.sort();\n     \n        if (!currentProperty.list[currentProperty.uiPointer]) {\n            if (currentProperty.list.length>0){\n                currentProperty.uiPointer=0; // reset to first\n                msg.payload=currentProperty.uiPointer;\n            } else { // no list existing\n                delete msg.payload;\n            }\n        } else msg.payload=currentProperty.uiPointer;\n            \n        node.status({fill:\"green\",shape:\"dot\",text:\"done!\"});\n    } else {\n        node.status({fill:\"yellow\",shape:\"dot\",text:\"object.list empty\"});\n    }\n    return msg;\n\n/*} catch (err) {\n    node.status({fill:\"yellow\",shape:\"dot\",text:\"No list entry\"});\n    return msg;\n} */","outputs":1,"noerr":0,"x":410,"y":100,"wires":[["d793942.135c968"]]},{"id":"5ecf0362.de014c","type":"inject","z":"897fdb2a.109f68","name":"initalize","topic":"mySettings.heating.test.null","payload":"uiInit","payloadType":"str","repeat":"","crontab":"","once":true,"onceDelay":0.1,"x":100,"y":100,"wires":[["b984a3e8.bf33"]]},{"id":"7dbcf8cd.631618","type":"function","z":"897fdb2a.109f68","name":"set editPeriod","func":"var topicParts=msg.topic.split('.');\nvar currentObject= global.get(topicParts[0],\"file\");\nif (!currentObject) return;\nvar currentProperty=currentObject.getObject(msg.topic);\nif (!currentProperty) {\n    node.status({fill:\"red\",shape:\"dot\",text:\"msg.topic='object.property' expected\"});\n    return;\n}\n\ntry {\n    currentProperty.uiPointer=Number(msg.payload);\n    node.status({fill:\"green\",shape:\"dot\",text:msg.payload+\"!\"});\n    return msg;\n} catch (err) {\n    node.status({fill:\"red\",shape:\"dot\",text:\"ups, somthing gone wrong.\"});\n    console.log(err);\n} ","outputs":1,"noerr":0,"x":840,"y":100,"wires":[["750a4ef9.a29c9"]]},{"id":"d3df79d6.ec2598","type":"link out","z":"897fdb2a.109f68","name":"updatePeriod","links":["bc9c5ee1.ceb48"],"x":995,"y":220,"wires":[]},{"id":"5a8215ea.6905cc","type":"ui_button","z":"897fdb2a.109f68","name":"uiAdd","group":"d11141a.b59afc","order":2,"width":1,"height":1,"passthru":false,"label":"","tooltip":"","color":"","bgcolor":"","icon":"playlist_add","payload":"unnamed","payloadType":"str","topic":"mySettings.heating.test.uiAdd","x":590,"y":160,"wires":[["cf5aca8c.406948"]]},{"id":"cf5aca8c.406948","type":"function","z":"897fdb2a.109f68","name":"add new","func":"node.status({});\nvar currentObject= global.get(msg.topic.slice(0,msg.topic.indexOf('.')),\"file\");\nif (!currentObject) return;\nvar currentProperty=currentObject.newObject(msg.topic);\nif (!currentProperty) {\n    node.status({fill:\"red\",shape:\"dot\",text: msg.topic+\" not created\"});\n    return;\n}\n\n// move Pointer to new entry\ncurrentProperty.uiPointer=currentProperty.list.length-1\nvar newItem = currentProperty.list[currentProperty.uiPointer];\n\n// initialize property object\nnewItem.name=msg.payload;\n\n//---------------------------------------------------------------\n// fill in defaults here! \n//---------------------------------------------------------------\nnewItem.start=new Date();\nnewItem.end=new Date();\nnewItem.monday=false;\nnewItem.tuesday=false;\nnewItem.wednesday=false;\nnewItem.thursday=false;\nnewItem.friday=false;\nnewItem.saturday=false;\nnewItem.sunday=false;\n\n\n// send output\nnode.status({fill:\"green\",shape:\"dot\",text:msg.payload+\" saved\"});\nreturn msg;","outputs":1,"noerr":0,"x":880,"y":160,"wires":[["54d64382.c8d62c"]],"icon":"node-red/file.png"},{"id":"54d64382.c8d62c","type":"link out","z":"897fdb2a.109f68","name":"updatePeriod","links":["bc9c5ee1.ceb48"],"x":995,"y":160,"wires":[]},{"id":"7c078fcf.9a8e9","type":"switch","z":"897fdb2a.109f68","name":"topic","property":"topic","propertyType":"msg","rules":[{"t":"eq","v":"mySettings.heating.test.name","vt":"str"},{"t":"eq","v":"mySettings.heating.test.start","vt":"str"},{"t":"eq","v":"mySettings.heating.test.end","vt":"str"},{"t":"eq","v":"mySettings.heating.test.monday","vt":"str"},{"t":"eq","v":"mySettings.heating.test.tuesday","vt":"str"},{"t":"eq","v":"mySettings.heating.test.wednesday","vt":"str"},{"t":"eq","v":"mySettings.heating.test.thursday","vt":"str"},{"t":"eq","v":"mySettings.heating.test.friday","vt":"str"},{"t":"eq","v":"mySettings.heating.test.saturday","vt":"str"},{"t":"eq","v":"mySettings.heating.test.sunday","vt":"str"}],"checkall":"true","repair":false,"outputs":10,"x":370,"y":340,"wires":[["e73adecd.24ffa"],["bd2033fe.103aa"],["4dbfe990.188838"],["61878250.8fc43c"],["2148eae6.2c8076"],["9572f07e.81162"],["9b06565e.dd2798"],["515536be.f1d9c8"],["4d24d50f.a27ccc"],["4ba796b0.614ec8"]]},{"id":"843a51.ab4095b","type":"ui_toast","z":"897fdb2a.109f68","position":"dialog","displayTime":"3","highlight":"","sendall":false,"outputs":1,"ok":"OK","cancel":"","topic":"","name":"Delete?","x":740,"y":220,"wires":[["372cbb4a.c123c4"]]},{"id":"b984a3e8.bf33","type":"function","z":"897fdb2a.109f68","name":"initialize","func":"if (!msg.payload) return;\n\nvar topicParts=\"\";\n\ntry {\n    topicParts=msg.topic.split('.');\n} catch (err) {\n    node.status({fill:\"red\",shape:\"dot\",text:\"msg.topic='object.property' expected\"});\n}\nvar keyword = topicParts.pop();\n\nif (topicParts.length<3) {\n    node.status({fill:\"red\",shape:\"dot\",text:\"msg.topic='object.property' expected\"});\n    return;\n}\n\n\n// get or initalize root for global store\nvar currentObject= global.get(topicParts[0],\"file\");\nif (!currentObject) {\n    currentObject={};\n    global.set(topicParts[0],currentObject,\"file\")\n}\n\n// function to create object for specific topic\ncurrentObject.newObject = function(topic) {\n    var topicParts = topic.split('.');\n    var keyword=topicParts.pop();\n    if (keyword!='uiAdd') return Null\n    if (topicParts.length<2) return null;\n    var objectPtr=global.get(topicParts[0],\"file\");\n    for (var i=1; i<topicParts.length; i++) {\n        if (!objectPtr[topicParts[i]]) return null; // object not found\n        objectPtr=objectPtr[topicParts[i]];\n    }\n    if (!objectPtr.list) objectPtr.list=[]; // initialize list if not existing\n    objectPtr.list.push({}); // add new object into list\n    return objectPtr;\n};\n\n// function to get object for specific topic\ncurrentObject.getObject = function(topic) {\n    var topicParts = topic.split('.');\n    if (topicParts.length<2) return null;\n    var objectPtr=global.get(topicParts[0],\"file\");\n    for (var i=1; i<topicParts.length-1; i++) { // ignore the last identifier\n        if (!objectPtr[topicParts[i]]) return null; // object not found\n        objectPtr=objectPtr[topicParts[i]];\n    }    \n    return objectPtr;\n};\n\n// function to get object for current entry out of list\ncurrentObject.getEntry = function(topic) {\n    objectPtr=this.getObject(topic);\n    if (objectPtr.uiPointer<objectPtr.list.length){\n        return objectPtr.list[objectPtr.uiPointer];\n    }\n    return null;\n};\n\n// function to delete current entry out of list\ncurrentObject.deleteEntry = function(topic) {\n    objectPtr=this.getObject(topic);\n    if (objectPtr.uiPointer<objectPtr.list.length){\n        var returnObj= objectPtr.list.splice(objectPtr.uiPointer,1);\n        if (returnObj) objectPtr.uiPointer=0;\n        return returnObj;\n    }\n    return null;\n};\n\n// (re)build Object Tree\nvar objectRoot= currentObject;\nfor (i=1; i<topicParts.length; i++) {\n  if (!currentObject[topicParts[i]]) currentObject[topicParts[i]]={};\n  currentObject=currentObject[topicParts[i]];\n}\nif (currentObject.uiPointer===undefined) currentObject.uiPointer=0;\nnode.status({fill:\"green\",shape:\"dot\",text:\"done!\"});\nreturn msg;\n","outputs":1,"noerr":0,"x":240,"y":100,"wires":[["f022e935.c6ab48"]]},{"id":"61878250.8fc43c","type":"ui_switch","z":"897fdb2a.109f68","name":"","label":"Monday","tooltip":"","group":"d11141a.b59afc","order":10,"width":0,"height":0,"passthru":true,"decouple":"false","topic":"mySettings.heating.test.monday","style":"","onvalue":"true","onvalueType":"bool","onicon":"","oncolor":"","offvalue":"false","offvalueType":"bool","officon":"","offcolor":"","x":600,"y":400,"wires":[["1ff3f5ad.ab0eca"]]},{"id":"2148eae6.2c8076","type":"ui_switch","z":"897fdb2a.109f68","name":"","label":"Tuesday","tooltip":"","group":"d11141a.b59afc","order":11,"width":0,"height":0,"passthru":true,"decouple":"false","topic":"mySettings.heating.test.tuesday","style":"","onvalue":"true","onvalueType":"bool","onicon":"","oncolor":"","offvalue":"false","offvalueType":"bool","officon":"","offcolor":"","x":600,"y":460,"wires":[["1ff3f5ad.ab0eca"]]},{"id":"9572f07e.81162","type":"ui_switch","z":"897fdb2a.109f68","name":"","label":"Wednesday","tooltip":"","group":"d11141a.b59afc","order":12,"width":0,"height":0,"passthru":true,"decouple":"false","topic":"mySettings.heating.test.wednesday","style":"","onvalue":"true","onvalueType":"bool","onicon":"","oncolor":"","offvalue":"false","offvalueType":"bool","officon":"","offcolor":"","x":610,"y":520,"wires":[["1ff3f5ad.ab0eca"]]},{"id":"9b06565e.dd2798","type":"ui_switch","z":"897fdb2a.109f68","name":"","label":"Thursday","tooltip":"","group":"d11141a.b59afc","order":13,"width":0,"height":0,"passthru":true,"decouple":"false","topic":"mySettings.heating.test.thursday","style":"","onvalue":"true","onvalueType":"bool","onicon":"","oncolor":"","offvalue":"false","offvalueType":"bool","officon":"","offcolor":"","x":600,"y":580,"wires":[["1ff3f5ad.ab0eca"]]},{"id":"515536be.f1d9c8","type":"ui_switch","z":"897fdb2a.109f68","name":"","label":"Friday","tooltip":"","group":"d11141a.b59afc","order":14,"width":0,"height":0,"passthru":true,"decouple":"false","topic":"mySettings.heating.test.friday","style":"","onvalue":"true","onvalueType":"bool","onicon":"","oncolor":"","offvalue":"false","offvalueType":"bool","officon":"","offcolor":"","x":590,"y":640,"wires":[["1ff3f5ad.ab0eca"]]},{"id":"4d24d50f.a27ccc","type":"ui_switch","z":"897fdb2a.109f68","name":"","label":"Saturday","tooltip":"","group":"d11141a.b59afc","order":15,"width":0,"height":0,"passthru":true,"decouple":"false","topic":"mySettings.heating.test.saturday","style":"","onvalue":"true","onvalueType":"bool","onicon":"","oncolor":"","offvalue":"false","offvalueType":"bool","officon":"","offcolor":"","x":600,"y":700,"wires":[["1ff3f5ad.ab0eca"]]},{"id":"4ba796b0.614ec8","type":"ui_switch","z":"897fdb2a.109f68","name":"","label":"Sunday","tooltip":"","group":"d11141a.b59afc","order":16,"width":0,"height":0,"passthru":true,"decouple":"false","topic":"mySettings.heating.test.sunday","style":"","onvalue":"true","onvalueType":"bool","onicon":"","oncolor":"","offvalue":"false","offvalueType":"bool","officon":"","offcolor":"","x":600,"y":760,"wires":[["1ff3f5ad.ab0eca"]]},{"id":"d11141a.b59afc","type":"ui_group","z":"","name":"Test Period","tab":"450f9708.9afe08","disp":true,"width":"8","collapse":false},{"id":"450f9708.9afe08","type":"ui_tab","z":"","name":"ui_Test","icon":"dashboard","disabled":false,"hidden":false}]

image

ui_list version in next post

2 Likes

Version with ui_list. The only difference is the ui_List node and the save list function node.
So for every new property in best case you only need to wire in ui_Node and configure the switch and add new node, worst case add a special node to handle the output.
The feednack ist only needed for the name value to update the dropdoen list. all others will be ignored.

[{"id":"5e92c20f.ec0a6c","type":"ui_text_input","z":"afaa0885.ad2718","name":"name","label":"","tooltip":"","group":"1727a207.b3522e","order":5,"width":6,"height":1,"passthru":false,"mode":"text","delay":"0","topic":"mySettings.heating.periods.name","x":610,"y":1280,"wires":[["40302cf4.7e27e4"]]},{"id":"4f49163c.5cf618","type":"ui_button","z":"afaa0885.ad2718","name":"uiDelete","group":"1727a207.b3522e","order":3,"width":1,"height":1,"passthru":false,"label":"","tooltip":"","color":"","bgcolor":"","icon":"delete","payload":"Wirklich löschen?","payloadType":"str","topic":"mySettings.heating.periods.uiDelete","x":620,"y":1220,"wires":[["d6348ffa.c8c09"]]},{"id":"64fa2ace.070df4","type":"function","z":"afaa0885.ad2718","name":"delete period","func":"node.status({});\nvar currentObject= global.get(msg.topic.slice(0,msg.topic.indexOf('.')),\"file\");\nif (!currentObject) return;\nvar currentProperty=currentObject.getEntry(msg.topic);\nif (!currentProperty) {\n    node.status({fill:\"red\",shape:\"dot\",text: msg.topic+\" not found\"});\n    return;\n}\nvar name=currentProperty.name;\n\ntry {\n    if (currentObject.deleteEntry(msg.topic)) {\n        node.status({fill:\"green\",shape:\"dot\",text:name+\" deleted\"});\n    } else {\n        node.status({fill:\"green\",shape:\"dot\",text:name+\" NOT deleted\"});\n    }\n    return msg;\n} catch (err) {\n    node.status({fill:\"red\",shape:\"dot\",text:\"ups, something gone wrong.\"});\n    console.log(err);\n} ","outputs":1,"noerr":0,"x":910,"y":1220,"wires":[["8e2785df.4b1f38"]]},{"id":"6940f151.ee7af","type":"ui_date_picker","z":"afaa0885.ad2718","name":"start","label":"","group":"1727a207.b3522e","order":7,"width":6,"height":1,"passthru":false,"topic":"mySettings.heating.periods.start","x":610,"y":1340,"wires":[["40302cf4.7e27e4"]]},{"id":"fa154658.7c86d8","type":"ui_date_picker","z":"afaa0885.ad2718","name":"end","label":"","group":"1727a207.b3522e","order":9,"width":6,"height":1,"passthru":false,"topic":"mySettings.heating.periods.end","x":610,"y":1400,"wires":[["40302cf4.7e27e4"]]},{"id":"74aa6cbd.107304","type":"ui_text","z":"afaa0885.ad2718","group":"1727a207.b3522e","order":4,"width":2,"height":1,"name":"Name:","label":"Name:","format":"{{msg.payload}}","layout":"row-spread","x":130,"y":1280,"wires":[]},{"id":"d21d39f.cf447c8","type":"ui_text","z":"afaa0885.ad2718","group":"1727a207.b3522e","order":6,"width":2,"height":1,"name":"Start:","label":"Anfang:","format":"{{msg.payload}}","layout":"row-spread","x":130,"y":1340,"wires":[]},{"id":"bf3c7ebf.f89d4","type":"ui_text","z":"afaa0885.ad2718","group":"1727a207.b3522e","order":8,"width":2,"height":1,"name":"End:","label":"Ende:","format":"{{msg.payload}}","layout":"row-spread","x":130,"y":1400,"wires":[]},{"id":"4e308951.08be78","type":"ui_dropdown","z":"afaa0885.ad2718","name":"uiDropdown","label":"","tooltip":"","place":"Tage auswählen","group":"1727a207.b3522e","order":1,"width":6,"height":1,"passthru":true,"options":[],"payload":"","topic":"mySettings.heating.periods.uiDropdown","x":630,"y":1100,"wires":[["3404a4d4.23919c"]]},{"id":"4bc14249.f80a5c","type":"function","z":"afaa0885.ad2718","name":"update","func":"var msgDropdown={};\nvar msgName={};\nvar msgList={};\nvar msgParameter=[];\n\nvar topicParts=msg.topic.split('.');\nvar currentObject= global.get(topicParts[0],\"file\");\nif (!currentObject) return;\nvar keyword = topicParts.pop();\nvar propertyTopic = msg.topic.slice(0,msg.topic.lastIndexOf('.'));\nvar currentProperty=currentObject.getObject(msg.topic);\nif (!currentProperty) {\n    node.status({fill:\"red\",shape:\"dot\",text:\"msg.topic='object.property' expected\"});\n    return;\n}\n\nmsgDropdown.topic=msg.topic;\n\ntry {\n    // fill an array for all parameters in list if exiting\n    if (currentProperty.list) { \n        for (var item in currentProperty.list[currentProperty.uiPointer]) {\n            if (!item.startsWith('ui')) \n                msgParameter.push({\"payload\":currentProperty.list[currentProperty.uiPointer][item], \"topic\": propertyTopic+'.'+item});\n        }\n    }    \n    switch (keyword) {\n        case \"uiDropdown\":\n            node.status({fill:\"green\",shape:\"dot\",text:\"Dropdown updated!\"});\n            return [null,msgParameter]; // update all parameters\n        case \"uiAdd\":\n            msgDropdown.payload=currentProperty.uiPointer;\n            node.status({fill:\"green\",shape:\"dot\",text:\"new entry nr=\"+currentProperty.uiPointer});\n            return [msgDropdown,null]; // update only dropdown, parametes will follw next call\n        case \"uiDelete\":\n            msgDropdown.payload=\"init\";\n            node.status({fill:\"green\",shape:\"dot\",text:\"entry deleted\"});\n            return [msgDropdown,null]; // update only dropdown, parametes will follw next call\n        case \"name\":\n            msgDropdown.payload=currentProperty.uiPointer;\n            node.status({fill:\"green\",shape:\"dot\",text:\"entry renamed!\"});\n            return [msgDropdown,null]; // update only dropdown\n    }\n    \n    node.status({fill:\"gray\",shape:\"dot\",text:msg.topic+\"=\"+msg.payload});\n    return null;\n} catch (err) {\n    node.status({fill:\"red\",shape:\"dot\",text:\"ups, somthing gone wrong. (\"+keyword+\")\"});\n    console.log(err);\n} ","outputs":2,"noerr":0,"x":210,"y":1200,"wires":[["1273f17e.cacfff"],["126c086f.d5ee48"]]},{"id":"8708046a.a91488","type":"link out","z":"afaa0885.ad2718","name":"updatePeriod","links":["d3e12acd.ca7e78"],"x":1035,"y":1340,"wires":[]},{"id":"d3e12acd.ca7e78","type":"link in","z":"afaa0885.ad2718","name":"updatePeriod","links":["8708046a.a91488","8ff23ed0.e7991","480be8a7.ca3858","8e2785df.4b1f38","facf168d.a84228"],"x":115,"y":1200,"wires":[["4bc14249.f80a5c"]]},{"id":"8ff23ed0.e7991","type":"link out","z":"afaa0885.ad2718","name":"updatePeriod","links":["d3e12acd.ca7e78"],"x":1035,"y":1100,"wires":[]},{"id":"40302cf4.7e27e4","type":"function","z":"afaa0885.ad2718","name":"save Data","func":"node.status({});\nvar currentObject= global.get(msg.topic.slice(0,msg.topic.indexOf('.')),\"file\");\nif (!currentObject) return;\nvar currentProperty=currentObject.getEntry(msg.topic);\nif (!currentProperty) {\n    node.status({fill:\"red\",shape:\"dot\",text: msg.topic+\" not found\"});\n    return;\n}\nvar propertyName=msg.topic.slice(msg.topic.lastIndexOf('.')+1);\n\ntry {\n    // make shure your topic is set correctly in your ui nodes(s): objectName.propertyName\n    currentProperty[propertyName]=msg.payload;\n\n    node.status({fill:\"green\",shape:\"dot\",text:propertyName+\"=\"+msg.payload+\"!\"});\n    return msg;\n} catch (err) {\n    node.status({fill:\"red\",shape:\"dot\",text:\"ups, somthing gone wrong.\"});\n    console.log(err);\n} ","outputs":1,"noerr":0,"x":900,"y":1340,"wires":[["8708046a.a91488"]]},{"id":"1273f17e.cacfff","type":"function","z":"afaa0885.ad2718","name":"fill dropdown","func":"var topicParts=msg.topic.split('.');\nvar currentObject= global.get(topicParts[0],\"file\");\nif (!currentObject) return;\nvar keyword = topicParts.pop();\nvar currentProperty=currentObject.getObject(msg.topic);\nif (!currentProperty) {\n    node.status({fill:\"red\",shape:\"dot\",text:\"msg.topic='object.property' expected\"});\n    return;\n}\n\n//try {\n    console.log(currentProperty.list!==undefined)\n    if (currentProperty.list!==undefined) {\n        msg.options=[];\n        var listEntry={};\n        currentProperty.list.forEach(function(item,index){\n            listEntry={};\n            listEntry[item.name]=String(index);\n            msg.options.push(listEntry);\n        })\n        msg.options.sort();\n     \n        if (!currentProperty.list[currentProperty.uiPointer]) {\n            if (currentProperty.list.length>0){\n                currentProperty.uiPointer=0; // reset to first\n                msg.payload=currentProperty.uiPointer;\n            } else { // no period existing\n                delete msg.payload;\n            }\n        } else msg.payload=currentProperty.uiPointer;\n            \n        node.status({fill:\"green\",shape:\"dot\",text:\"done!\"});\n    } else {\n        node.status({fill:\"yellow\",shape:\"dot\",text:\"object.list empty\"});\n    }\n    return msg;\n\n/*} catch (err) {\n    node.status({fill:\"yellow\",shape:\"dot\",text:\"No list entry\"});\n    return msg;\n} */","outputs":1,"noerr":0,"x":430,"y":1100,"wires":[["4e308951.08be78"]]},{"id":"9b8345ab.734018","type":"inject","z":"afaa0885.ad2718","name":"initalize","topic":"mySettings.heating.periods.null","payload":"uiInit","payloadType":"str","repeat":"","crontab":"","once":true,"onceDelay":0.1,"x":120,"y":1100,"wires":[["cc912830.ac5188"]]},{"id":"20d22b21.441424","type":"function","z":"afaa0885.ad2718","name":"save List","func":"node.status({});\nvar currentObject= global.get(msg.topic.slice(0,msg.topic.indexOf('.')),\"file\");\nif (!currentObject) return;\nvar currentProperty=currentObject.getEntry(msg.topic);\nif (!currentProperty) {\n    node.status({fill:\"red\",shape:\"dot\",text: msg.topic+\" not found\"});\n    return;\n}\nvar propertyName=msg.topic.slice(msg.topic.lastIndexOf('.')+1);\n\ntry {\n    currentProperty.days.forEach(function (item,index) {\n        if (item.title.includes(msg.payload.title)) {\n            item.isChecked=msg.payload.isChecked;\n            node.status({fill:\"green\",shape:\"dot\",text:msg.payload.title+\"=\"+msg.payload.isChecked});\n            return msg;\n        }\n    });\n //   node.status({fill:\"yellow\",shape:\"dot\",text:msg.payload.title+\" not found\"});\n} catch (err) {\n    node.status({fill:\"red\",shape:\"dot\",text:\"ups, somthing gone wrong.\"});\n    console.log(err);\n} ","outputs":1,"noerr":0,"x":900,"y":1460,"wires":[[]]},{"id":"3404a4d4.23919c","type":"function","z":"afaa0885.ad2718","name":"set editPeriod","func":"var topicParts=msg.topic.split('.');\nvar currentObject= global.get(topicParts[0],\"file\");\nif (!currentObject) return;\nvar currentProperty=currentObject.getObject(msg.topic);\nif (!currentProperty) {\n    node.status({fill:\"red\",shape:\"dot\",text:\"msg.topic='object.property' expected\"});\n    return;\n}\n\ntry {\n    currentProperty.uiPointer=Number(msg.payload);\n    node.status({fill:\"green\",shape:\"dot\",text:msg.payload+\"!\"});\n    return msg;\n} catch (err) {\n    node.status({fill:\"red\",shape:\"dot\",text:\"ups, somthing gone wrong.\"});\n    console.log(err);\n} ","outputs":1,"noerr":0,"x":920,"y":1100,"wires":[["8ff23ed0.e7991"]]},{"id":"8e2785df.4b1f38","type":"link out","z":"afaa0885.ad2718","name":"updatePeriod","links":["d3e12acd.ca7e78"],"x":1035,"y":1220,"wires":[]},{"id":"8cd0c53e.487bf8","type":"ui_button","z":"afaa0885.ad2718","name":"uiAdd","group":"1727a207.b3522e","order":2,"width":1,"height":1,"passthru":false,"label":"","tooltip":"","color":"","bgcolor":"","icon":"playlist_add","payload":"unbenannt","payloadType":"str","topic":"mySettings.heating.periods.uiAdd","x":610,"y":1160,"wires":[["e1f0305c.a78a"]]},{"id":"e1f0305c.a78a","type":"function","z":"afaa0885.ad2718","name":"add new","func":"node.status({});\nvar currentObject= global.get(msg.topic.slice(0,msg.topic.indexOf('.')),\"file\");\nif (!currentObject) return;\nvar currentProperty=currentObject.newObject(msg.topic);\nif (!currentProperty) {\n    node.status({fill:\"red\",shape:\"dot\",text: msg.topic+\" not created\"});\n    return;\n}\n\n// move Pointer to new entry\ncurrentProperty.uiPointer=currentProperty.list.length-1\nvar newItem = currentProperty.list[currentProperty.uiPointer];\n\n// initialize property object\nnewItem.name=msg.payload;\n\n// fill defaults \nnewItem.start=new Date();\nnewItem.end=new Date();\nnewItem.days=[]\nnewItem.days.push({title:\"Montag\", isChecked: false});\nnewItem.days.push({title:\"Dienstag\", isChecked: false});\nnewItem.days.push({title:\"Mittwoch\", isChecked: false});\nnewItem.days.push({title:\"Donnerstag\", isChecked: false});\nnewItem.days.push({title:\"Freitag\", isChecked: false});\nnewItem.days.push({title:\"Samstag\", isChecked: false});\nnewItem.days.push({title:\"Sonntag\", isChecked: false});\n\n// send output\nnode.status({fill:\"green\",shape:\"dot\",text:msg.payload+\" saved\"});\nreturn msg;","outputs":1,"noerr":0,"x":900,"y":1160,"wires":[["facf168d.a84228"]]},{"id":"facf168d.a84228","type":"link out","z":"afaa0885.ad2718","name":"updatePeriod","links":["d3e12acd.ca7e78"],"x":1035,"y":1160,"wires":[]},{"id":"126c086f.d5ee48","type":"switch","z":"afaa0885.ad2718","name":"topic","property":"topic","propertyType":"msg","rules":[{"t":"eq","v":"mySettings.heating.periods.name","vt":"str"},{"t":"eq","v":"mySettings.heating.periods.start","vt":"str"},{"t":"eq","v":"mySettings.heating.periods.end","vt":"str"},{"t":"eq","v":"mySettings.heating.periods.days","vt":"str"}],"checkall":"true","repair":false,"outputs":4,"x":410,"y":1380,"wires":[["5e92c20f.ec0a6c"],["6940f151.ee7af"],["fa154658.7c86d8"],["ea72c8b7.6b8be8"]]},{"id":"d6348ffa.c8c09","type":"ui_toast","z":"afaa0885.ad2718","position":"dialog","displayTime":"3","highlight":"","sendall":false,"outputs":1,"ok":"OK","cancel":"","topic":"","name":"Delete?","x":760,"y":1220,"wires":[["64fa2ace.070df4"]]},{"id":"cc912830.ac5188","type":"function","z":"afaa0885.ad2718","name":"initialize","func":"if (!msg.payload) return;\n\nvar topicParts=\"\";\n\ntry {\n    topicParts=msg.topic.split('.');\n} catch (err) {\n    node.status({fill:\"red\",shape:\"dot\",text:\"msg.topic='object.property' expected\"});\n}\nvar keyword = topicParts.pop();\n\nif (topicParts.length<3) {\n    node.status({fill:\"red\",shape:\"dot\",text:\"msg.topic='object.property' expected\"});\n    return;\n}\n\n\n// get or initalize root for global store\nvar currentObject= global.get(topicParts[0],\"file\");\nif (!currentObject) {\n    currentObject={};\n    global.set(topicParts[0],currentObject,\"file\")\n}\n\n// function to create object for specific topic\ncurrentObject.newObject = function(topic) {\n    var topicParts = topic.split('.');\n    var keyword=topicParts.pop();\n    if (keyword!='uiAdd') return Null\n    if (topicParts.length<2) return null;\n    var objectPtr=global.get(topicParts[0],\"file\");\n    for (var i=1; i<topicParts.length; i++) {\n        if (!objectPtr[topicParts[i]]) return null; // object not found\n        objectPtr=objectPtr[topicParts[i]];\n    }\n    if (!objectPtr.list) objectPtr.list=[]; // initialize list if not existing\n    objectPtr.list.push({}); // add new object into list\n    return objectPtr;\n};\n\n// function to get object for specific topic\ncurrentObject.getObject = function(topic) {\n    var topicParts = topic.split('.');\n    if (topicParts.length<2) return null;\n    var objectPtr=global.get(topicParts[0],\"file\");\n    for (var i=1; i<topicParts.length-1; i++) { // ignore the last identifier\n        if (!objectPtr[topicParts[i]]) return null; // object not found\n        objectPtr=objectPtr[topicParts[i]];\n    }    \n    return objectPtr;\n};\n\n// function to get object for current entry out of list\ncurrentObject.getEntry = function(topic) {\n    objectPtr=this.getObject(topic);\n    if (objectPtr.uiPointer<objectPtr.list.length){\n        return objectPtr.list[objectPtr.uiPointer];\n    }\n    return null;\n};\n\n// function to delete current entry out of list\ncurrentObject.deleteEntry = function(topic) {\n    objectPtr=this.getObject(topic);\n    if (objectPtr.uiPointer<objectPtr.list.length){\n        var returnObj= objectPtr.list.splice(objectPtr.uiPointer,1);\n        if (returnObj) objectPtr.uiPointer=0;\n        return returnObj;\n    }\n    return null;\n};\n\n// (re)build Object Tree\nvar objectRoot= currentObject;\nfor (i=1; i<topicParts.length; i++) {\n  if (!currentObject[topicParts[i]]) currentObject[topicParts[i]]={};\n  currentObject=currentObject[topicParts[i]];\n}\nif (currentObject.uiPointer===undefined) currentObject.uiPointer=0;\nnode.status({fill:\"green\",shape:\"dot\",text:\"done!\"});\nreturn msg;\n","outputs":1,"noerr":0,"x":260,"y":1100,"wires":[["1273f17e.cacfff"]]},{"id":"ea72c8b7.6b8be8","type":"ui_list","z":"afaa0885.ad2718","group":"4ac97505.03ae4c","name":"days","order":20,"width":"8","height":"7","lineType":"one","actionType":"check","allowHTML":false,"outputs":1,"topic":"mySettings.heating.periods.days","x":610,"y":1460,"wires":[["20d22b21.441424"]]},{"id":"1727a207.b3522e","type":"ui_group","z":"","name":"Zeitabschnitt","tab":"831d491a.c7a7e8","order":3,"disp":true,"width":"8","collapse":false},{"id":"4ac97505.03ae4c","type":"ui_group","z":"","name":"Wochentage","tab":"831d491a.c7a7e8","order":4,"disp":true,"width":"8","collapse":true},{"id":"831d491a.c7a7e8","type":"ui_tab","z":"","name":"Heizung","icon":"dashboard","order":1,"disabled":false,"hidden":false}]


Hope it is usefull for somebody.

Chris

2 Likes

Hi @Christian-Me, thanks a million for your posts. I'm looking forward to going through your flows to see how you have implemented these things.

It goes to show how flexible Node-RED is as a platform, to come up with our own solutions and applications.