Part of HashMap not properly copied

Hi,

I want to create a scene controller for my lights that work both ways. With this I mean that I want to be able to set the scene from OpenHAB, but I also want OpenHAB to know the current scene if the lights are manually set to match a scene. I don’t know if this is the most efficient way to do it, as it is a combination of (trying to) get what I want and learn at the same time. I found some examples on the forum of scene controls, but they were not exactly what I wanted with 2-way functionality and the possibility to set individual items to one value and the other values in a group to other values. Anyway:

I have managed to get the part where I control the scenes from OpenHAB to work properly. However, I cannot get my code for OpenHAB to detect the scene to work reliably. Sometimes it can guess the correct scene, but other times not. It seems to be a bit random.

I have a HashMap that contains my data for all scenes. My house has two floors, and I have one group for the lights per floor. When a member of the groups has changed, I trigger the code to compare the lights that are on with the scene-HashMap to either give a certain reply or a guess (i.e. for the scenes to only control lights on one floor, it can never be certain as the values on the other floors are not checked).

The part that doesn’t work is where I copy the current scene into a new working variable. Some times the data is copied correctly, other times it is not. I guess I need to define the variable in a different way? Does OpenHAB run the loops in parallel on multiple threads and, thus, use the data from a different iteration?

As an example:
I manually set the lights to match the scene “EVENING”. I get the following in the log once my code check’s the scene:

2020-11-09 14:17:39.844 [INFO ] [smarthome.model.script.scene_EVENING] - MScene_Start: {glightsGroundfloor=OFF, glightsFstfloor=OFF}
2020-11-09 14:17:39.849 [INFO ] [smarthome.model.script.scene_EVENING] - Item: vardagsrum_sarfatti
2020-11-09 14:17:39.853 [INFO ] [smarthome.model.script.scene_EVENING] - mscene: {glightsGroundfloor=OFF, glightsFstfloor=OFF}
2020-11-09 14:17:39.857 [INFO ] [smarthome.model.script.scene_EVENING] - Processing item vardagsrum_sarfatti
2020-11-09 14:17:39.865 [INFO ] [eclipse.smarthome.model.script.scene] - scene EVENING fail 1
2020-11-09 14:17:39.874 [INFO ] [eclipse.smarthome.model.script.Scene] - Current scene is not EVENING

If I refresh the rule and trig the code again, it works, and then it stops working again. Looking at the mscene variable printout I can see that the times it works, it for some reason aquires the correct part.

As can be seen, the Mscene does not contain the proper value from start, resulting in a failure to detect the scene. How should I set mscene to make sure I keep the proper value there?

I attach my code below. I have only copied the part to identify a rule (not set it), as that is not relevant here. Please note the code is not cleaned up and finished yet.

EDIT: As the tabs disappear in the codefences, making the code almost unreadable, I will try to summarize the code below:
scenes = newHashMap with two levels that in the first level contain the scene names as keys and a nested Hashmap as values. The nested one contains item names as keys and item values as values.

The problematic code is as follows:

scenes.keySet.forEach[ scene |
    var mscene = newHashMap()
    mscene = scenes.get(scene)
]

The mscene value does not retrieve the correct data from the scenes-hashmap.
I have tried to add a while loop (as in the code below) to loop until the item count is correct, but it did not help. So I suppose the problem is how I declare the mscene-variable inside the forEach loop.

I have a printout, so that I can see that the scene is correct. So the correct data should be retrieved.

End of edit.

val useFirstPlaussible = false
val scenes = newHashMap(
	"EVENING_GOTOSLEEP" -> (newLinkedHashMap(
		"vardagsrum_pentiklampaD_control" -> 10,
		"glightsGroundfloor" -> "OFF",
		"glightsFstfloor" -> "OFF"
	)),
	"EVENING_TV" -> (newLinkedHashMap(
		"vardagsrum_pentiklampaD_control" -> 80,
		"vardagsrum_secto" -> 100,
		"glightsGroundfloor" -> "OFF",
		"glightsFstfloor" -> "OFF"
	)),
	"EVENING" -> (newLinkedHashMap(
		"vardagsrum_pentiklampaD_control" -> 80,
		"vardagsrum_secto" -> 100,
		"vardagsrum_sarfatti" -> 30,
		"glightsGroundfloor" -> "OFF",
		"glightsFstfloor" -> "OFF"
	)),
	"EVENING_OUT" -> (newLinkedHashMap(
		"vardagsrum_pentiklampaD_control" -> 80,
		"vardagsrum_secto" -> 100,
		"vardagsrum_sarfatti" -> 30,
		"tambur_taklampa" -> "ON",
		"glightsGroundfloor" -> "OFF",
		"glightsFstfloor" -> "OFF"
	)),	
	"MORNING" -> (newLinkedHashMap(
		"glightsGroundfloor" -> "OFF",
		"glightsFstfloor" -> "OFF"
	)),	
	"MORNING_OUT" -> (newLinkedHashMap(
		"tambur_taklampa" -> "ON",
		"glightsGroundfloor" -> "OFF",
		"glightsFstfloor" -> "OFF"
	)),	
	"DINNER" -> (newLinkedHashMap(
		"vardagsrum_pentiklampaD_control" -> 80,
		"vardagsrum_sarfatti" -> 20,
		"glightsGroundfloor" -> "OFF",
		"glightsFstfloor" -> "OFF"
	)),	
	"DAY" -> (newLinkedHashMap(
		"vardagsrum_secto" -> 100,
		"glightsGroundfloor" -> "OFF",
		"glightsFstfloor" -> "OFF"
	)),
	"DAY_OUT" -> (newLinkedHashMap(
		"vardagsrum_secto" -> 100,
		"tambur_taklampa" -> "ON",
		"glightsGroundfloor" -> "OFF",
		"glightsFstfloor" -> "OFF"
	)),
	"UPSTAIRS_CLEANING" -> (newLinkedHashMap(
		"glightsFstfloor" -> "ON"
	)),		
	"DOWNSTAIRS_CLEANING" -> (newLinkedHashMap(
		"glightsGroundfloor" -> "ON"
	)),
	"ALL_OFF" -> (newLinkedHashMap(
		"glightsGroundfloor" -> "OFF",
		"glightsFstfloor" -> "OFF"
	))
)
rule "SceneController" when Item scenecontrol changed or Member of glightsGroundfloor changed or Member of glightsFstfloor changed then
	logInfo("SceneGuess", "Start")
	// Get state of all items and note those not OFF
	var status = newHashMap()
	// Iterate through all items and add to status map if not off
	glightsGroundfloor.members.forEach[ i |
		logInfo("Add", i.toString)
		if (i.state == ON || i.state > 0) {
			status.put(i.name, i.state)
			logInfo("Added", i.toString)
		}
	]
	glightsFstfloor.members.forEach[ i |
		if (i.state == ON || i.state > 0) {
			status.put(i.name, i.state)
		}
	]
	logInfo("statusgroup", status.toString)
	// Figure out scene
	var String plaussible = ""
	var String certain = ""
	// Loop through all scenes
	scenes.keySet.forEach [ scene |
		// Create copy of scene in memory
		var mscene = newHashMap()
		while (mscene.size() != scenes.get(scene).size()) {
			mscene = scenes.get(scene)
		}
		logInfo("scene_" + scene.toString, "MScene_Start: " + mscene.toString)
		// Loop through current state, and delete the identical states from mscene
		var tStatusFailed = false
		var tStatusPlaussible = false
		status.keySet.forEach [ citem |
			if (!tStatusFailed && certain == "") {
				logInfo("scene_" + scene.toString, "Item: " + citem.toString)
				logInfo("scene_" + scene.toString, "mscene: " + mscene.toString)
				// If scene has the item
				if (mscene.containsKey(citem)) {
//					logInfo("scene_" + scene.toString, "Item found, checking value")
//					logInfo("scene_" + scene.toString, "Item " + citem.toString + ": " + mscene.get(citem).toString + " | " + status.get(citem).toString)
					// If item has same value
					if (mscene.get(citem).toString == status.get(citem).toString) {
						// Delete it from the memory map
//						logInfo("scene_" + scene.toString, "--Start--")
//						logInfo("scene_" + scene.toString, mscene.toString)
						mscene.remove(citem)
//						logInfo("scene_" + scene.toString, "Removing item " + citem.toString + " from mscene. Result:")
//						logInfo("scene_" + scene.toString, mscene.toString)
//						logInfo("scene_" + scene.toString, "--End--")
					}
				}
				// Else check if scene has a group command setting different state to the item
				else {
					logInfo("scene_" + scene.toString, "Processing item " + citem.toString)
					// If current item is member of glightsGroundfloor
					if (glightsGroundfloor.members.filter[ i | i.name == citem ].size() == 1) {
						// If glightsGroundfloor is in the scene
						if (mscene.containsKey("glightsGroundfloor")) {
							// If value is different than current items state
							if (mscene.get("glightsGroundfloor").toString !== status.get(citem).toString) {
								// Mark as failed
								tStatusFailed = true
								logInfo("scene", "scene " + scene.toString + " fail 1")
							}
							// Else OK, as value is same as group value
							else {
								tStatusPlaussible = true
								logInfo("scene", "scene " + scene.toString + " plaussible 1")
							}
						}
						// Else mark as plaussible, as the value is OK for the scene but not defined by it.
						else {
							tStatusPlaussible = true
							logInfo("scene", "scene " + scene.toString + " plaussible 2")
						}
					}
					// Else if current item is member of glightsFstfloor
					if ((glightsFstfloor.members.filter[ i | i.name == citem].size() == 1) && !tStatusFailed) {
						// If glightsFstfloor is in the scene
						if (mscene.containsKey("glightsFstfloor")) {
							// If value is different than current items state
							if (mscene.get("glightsFstfloor").toString !== status.get(citem).toString) {
								// Mark as failed
								tStatusFailed = true
								logInfo("scene", "scene " + scene.toString + " fail 2")
							}
							// Else OK, as value is same as group value
							else {
								tStatusPlaussible = true
								logInfo("scene", "scene " + scene.toString + " plaussible 3")
							}
						}
						// Else mark as plaussible, as the value is OK for the scene but not defined by it.
						else {
							tStatusPlaussible = true
							logInfo("scene", "scene " + scene.toString + " plaussible 4")
						}
					}
				}					
			 }
		]
//			if (!tStatusFailed) {
//				logInfo("scene", "scene " + scene.toString + ", not failed")
//			}
//			if (!tStatusPlaussible) {
//				logInfo("scene", "scene " + scene.toString + ", not plaussible")
//			}
			// Check that the scene contains maximum two values (glightsGroundfloor & glightsFstfloor)
		if ((mscene.size() <= 2) && !tStatusFailed) {
			// If size is 2 it must contain both groups
			if (mscene.size == 2) {
				if (mscene.containsKey("glightsGroundfloor") && mscene.containsKey("glightsFstfloor")) {
					// If both are OFF or 0
					if ((mscene.get("glightsGroundfloor").toString == "OFF" || mscene.get("glightsGroundfloor").toString == "0") && (mscene.get("glightsFstfloor").toString == "OFF" || mscene.get("glightsFstfloor").toString == "0")) {
						// OK							
					} else {
						// Mark as failed
						tStatusFailed = true
						logInfo("scene", "scene " + scene.toString + " fail 3")
					}
				} // Else mark as failed
				else {
					tStatusFailed = true
					logInfo("scene", "scene " + scene.toString + " fail 4")
				}
			}
			// Else if size is 1, it must be either one
			else if (mscene.size == 1) {
				// If it contains glightsGroundfloor
				if (mscene.containsKey("glightsGroundfloor")) {
					// If both are OFF or 0
					if (mscene.get("glightsGroundfloor").toString == "OFF" || mscene.get("glightsGroundfloor").toString == "0") {
						// Plaussible, but not certain
						tStatusPlaussible = true
						logInfo("scene", "scene " + scene.toString + " plaussible 5")
					}
					// Else failed, as all items are not on
					else {
						tStatusFailed = true
						logInfo("scene", "scene " + scene.toString + " fail 5")
					}
				}
				// Else if it contains glightsFstfloor
				else if (mscene.containsKey("glightsFstfloor")) {
					// If both are OFF or 0
					if (mscene.get("glightsFstfloor").toString == "OFF" || mscene.get("glightsFstfloor").toString == "0") {
						// Plaussible, but not certain
						tStatusPlaussible = true
						logInfo("scene", "scene " + scene.toString + " plaussible 6")
					}    
					// Else failed, as all items are not on
					else {
						tStatusFailed = true
						logInfo("scene", "scene " + scene.toString + " fail 6")
					}
				}
				// Else mark as failed, as all items are not on
				else {
					tStatusFailed = true
					logInfo("scene", "scene " + scene.toString + " fail 7")
				}
			}
			// Else mark as failed, as all items are not on
			else {
				tStatusFailed = true
				logInfo("scene", "scene " + scene.toString + " fail 8")
			}
		}
		// Else mark as failed, as all items are not on
		else if (!tStatusFailed) {
			tStatusFailed = true
			logInfo("scene", "scene " + scene.toString + " fail 9")
			logInfo("scene_" + scene.toString, "scene failed as there are " + mscene.size().toString + " items in mscene:")
			logInfo("scene", mscene.toString)
		}
		// If status failed
		if (tStatusFailed) {
			logInfo("Scene", "Current scene is not " + scene)
		}
		// If status plaussible and not failed
		if (tStatusPlaussible && !tStatusFailed) {
			logInfo("Plaussible " + scene)
			if (plaussible == "") {
				plaussible = scene
			}
		}
		// If neither failed or plaussible it is certain
		if (!tStatusFailed && !tStatusPlaussible) {
			certain = scene
		}
	]
	// Decide what scene is set
	// If a certain one is set, use that
//	if (certain !== "") {
//		scenecontrol.postUpdate(certain)
//	}
	// Else if a plaussible is set, use that
//	else if (plaussible !== "") {
//		scenecontrol.postUpdate(plaussible)
//	}
	// Else set unknown
//	else {
//		scenecontrol.postUpdate("UNKNOWN")
//	}
	logInfo("Lights_Status", status.toString)
	logInfo("Scenes_Plaussible", plaussible.toString)
	logInfo("Scenes_Certain", certain.toString)

EDIT: The codefences and tabs does not seem to work properly…?

I haven’t ploughed through that lot, but offer three thoughts.

If you send a command to an Item, and then immediately read the Item state, you will get the old value. It takes time for commands to act.

If you update a member Item of a Group, and immediately read the Group state, you will get the old value. It takes time for Group functions to calculate new state.

If a Group state does get recalculated, beware that it may fire several update events relating to the Group. This can get you into trouble if you use Group updates to trigger rules.

codefences work fine. Just enclose your text like this. [spaces are present but not showing below]

```
text here
spaced in 4 spaces
```

It becomes

text here
    spaced in 4 spaces

Thank you for the heads-up! I will adjust my code with a timer in order to avoid the old value to be used. I guess few seconds should be enough for the new state to be properly registered?

However, the main problem here is not the group item update delay. I copy part of a “val” (“static variable”) into a new variable, and it is not copied properly. To isolate the specific issue from the larger code:

I define the value:

val scenes = newHashMap(
	"EVENING" -> (newLinkedHashMap(
		"vardagsrum_pentiklampaD_control" -> 80,
		"vardagsrum_secto" -> 100,
		"vardagsrum_sarfatti" -> 30,
		"glightsGroundfloor" -> "OFF",
		"glightsFstfloor" -> "OFF"
	))
)

I try to copy the value inside a forEach loop into a new variable mscene:

scenes.keySet.forEach[ scene |
var mscene = newHashMap()
mscene = scenes.get(scene)
]

I expect the outcome to be that mscene is:

{vardagsrum_pentiklampaD_control=80,vardagsrum_secto=100,vardagsrum_sarfatti=30,glightsGroundfloor=OFF, glightsFstfloor=OFF}

Instead, mscene is:

{glightsGroundfloor=OFF, glightsFstfloor=OFF}

Hi,

Thank you for the tip. I used the HTML-style code-tags (How to use code fences) expecting the same results. I updated the post to have three ` instead, and now it seem to work fine.

Wrap the logs too, please.

Why copy? Just use the map you pulled?

    var mscene = scenes.get(scene)

You are mixing types in the HashMap which could be a problem. Make sure all the values in the Maps are the same type. In this case make them all Strings.

If you really do need a copy of the map (i.e. I see you delete an entry from the map, presumably you don’t want that deleted from scenes too?), use

var mscene = scenes.get(scene).clone()

Thank you for the reply and kind support!

I suspected it could be something like this - that the copy is done incorrectly. However, the type of the new value seem to be different than the original. How can I “navigate” the new one?

020-11-09 19:55:37.574 [ERROR] [ntime.internal.engine.RuleEngineImpl] - Rule 'SceneController': 'containsKey' is not a member of 'java.lang.Object'; line 113, column 16, length 25

Caused by:

if (mscene.containsKey(citem)) {

You probably need to cast the result of the clone to a Map

var mscene = scenes.get(scene).clone() as Map<String, String>

You may need to import java.util.Map

Thank you for your kind support, that did the trick!