Persist Scenes using storestates/restorestates in a string

  • Platform information:
    • Hardware: Pine64 aarch64/2gb/16gb SD
    • OS: Ubuntu Bionic
    • Java Runtime Environment: openjdk-11-jre 11.0.6+10-1ubuntu1~18.04.1 (arm64)
    • openHAB version: openhab2 2.5.2-1
  • Issue of the topic:
    I have a rule that I use to save and reload scenes using storestates and restorestates.
var Map SceneState2 = null
rule "scene 2"
when
    Item Scene_Buttons2 received command
then
  	if (Scene_Buttons2.state == OFF) {
            SceneState2 = storeStates(RestoreBedroom)
        }
        else if (Scene_Buttons2.state == ON) {
            restoreStates (SceneState2)
        }
end

This rule works great, only pitfall is that if you edit the rule file or openhab restarts, your scenes are gone.
So i thought it would be ]simple enough to put the scenestate2 variable in a string item and persist that item in a mapdb. … BOY WAS I WRONG.
Here’s an attempt:

rule "test scene"
when
    Item Scene_Buttons received command
then
  	if (Scene_Buttons.state == OFF) {
            SceneString1.postUpdate(storeStates(RestoreBedroom).toString)
        }
        else if (Scene_Buttons.state == ON) {
            restoreStates (SceneString1.state)
        }
end

Saving the state tot the items works fine, but loading the string back into a map doesn’t work. I think i needs to cast .toMap … but cant find any info on how to get my string containing a map back into a map variable. Do i need to use another serializer ?

What it looks like :blush:
image
image
Sitemap lines involved

        Group item=Scene_Switch label="Scenes" {
            Switch  item=Scene_Buttons                  label="Scene 1"                         mappings=[OFF="Save", ON="load"]
            Switch  item=Scene_Buttons2                 label="Scene 2"                         mappings=[OFF="Save", ON="load"]
            Switch  item=Scene_Buttons3                 label="Scene 3"                         mappings=[OFF="Save", ON="load"]
        }
            Switch item=Scene_Switch                    label="Scene"                           mappings=[1="1",2="2",3="3"]

Some items involved

Group    RestoreBedroom            "Group containing item channels to be restored as scene"                              <firstfloor>    (Home)                     ["FirstFloor"]
Switch Scene_Buttons          "save scene"                          <flowpipe>           (scenetest)
Switch Scene_Buttons2         "save scene 2"                        <flowpipe>           (scenetest)
Switch Scene_Buttons3         "save scene 3"                        <flowpipe>           (scenetest)
Number Scene_Switch            <flowpipe>
String SceneString1     "string containg scene data for persistence"
String SceneString2     "string containg scene data for persistence"
String SceneString3     "string containg scene data for persistence"

Logs whens saving … which seems to work …

2020-02-26 11:02:27.292 [vent.ItemStateChangedEvent] - Scene_Buttons changed from NULL to OFF
2020-02-26 11:02:28.709 [vent.ItemStateChangedEvent] - SceneString1 changed from NULL to {Wemos_Power3 (Type=SwitchItem, State=OFF, Label=Power3, Category=light, Groups=[Bedroom, Wemos, RestoreBedroom, Bedroom_Switch])=OFF, Cubes_Ledstrip_Power (Type=SwitchItem, State=OFF, Label=Power, Category=light, Groups=[Bedroom, Cubes_Ledstrip, RestoreBedroom, Bedroom_Switch])=OFF, LSC_White_Led_RestoreState (Type=StringItem, State=FF00, Label=null, Category=network, Groups=[RestoreBedroom])=FF00, Wemos_Power5 (Type=SwitchItem, State=OFF, Label=Power5, Category=light, Groups=[Bedroom, Wemos, RestoreBedroom])=OFF, Wemos_Power4 (Type=SwitchItem, State=OFF, Label=Power4, Category=light, Groups=[Bedroom, Wemos, RestoreBedroom, Bedroom_Switch])=OFF, Wemos_Power7 (Type=SwitchItem, State=OFF, Label=Power7, Category=light, Groups=[Bedroom, Wemos, RestoreBedroom])=OFF, Wemos_Power6 (Type=SwitchItem, State=OFF, Label=Power6, Category=light, Groups=[Bedroom, Wemos, RestoreBedroom])=OFF, Cubes_Ledstrip_RestoreState (Type=StringItem, State=2F00FF, Label=null, Category=network, Groups=[RestoreBedroom])=2F00FF, Sidelight_Power (Type=SwitchItem, State=OFF, Label=Power, Category=light, Groups=[Bedroom, Sidelight, RestoreBedroom, Bedroom_Switch])=OFF, Wemos_RestoreState (Type=StringItem, State=000000, Label=null, Category=network, Groups=[RestoreBedroom])=000000, LSC_White_Led_Power (Type=SwitchItem, State=ON, Label=Power, Category=light, Groups=[Bedroom, LSC_White_Led, RestoreBedroom, Bedroom_Switch])=ON, Sidelight_RestoreState (Type=StringItem, State=FF0A1E0000, Label=null, Category=network, Groups=[RestoreBedroom])=FF0A1E0000}

Log when trying to load the scene

2020-02-26 11:03:52.509 [ERROR] [ntime.internal.engine.RuleEngineImpl] - Rule 'test scene': An error occurred during the script execution: Could not invoke method: org.eclipse.smarthome.model.script.actions.BusEvent.restoreStates(java.util.Map) on instance: null

Not sure if SceneString1 contains all info to go back to a map and if the structure is still valid after .toString, neither how to ask openhab to make it a map.

So basically I’m looking for a way to make restoreStates (SceneString1.state) work. I understand that string and map are not the same, but I cant find a way to convert between the twom or how else to persist a map.

Isn’t more like you need to construct a new Map?
I don’t know exactly …
someVariable = new Map (some stringy stuff)

Hey thanks for the reply, I think I tried it before,but just to be safe did it again :

import java.util.Map
rule "test scene"
when
    Item Scene_Buttons received command
then
  	if (Scene_Buttons.state == OFF) {
            SceneString1.postUpdate(storeStates(RestoreBedroom).toString)
        }
        else if (Scene_Buttons.state == ON) {
            var Map SceneState = SceneString1.state
            restoreStates (SceneState)
        }
end

Gives me an error:

2020-02-26 13:52:13.692 [ERROR] [ntime.internal.engine.RuleEngineImpl] - Rule 'test scene': An error occurred during the script execution: Could not invoke method: org.eclipse.smarthome.model.script.actions.BusEvent.restoreStates(java.util.Map) on instance: null

Tried your exact syntax :

        }
        else if (Scene_Buttons.state == ON) {
            SceneState = new Map (SceneString1.state)
            restoreStates (SceneState)
        }
end

That threw me this error:

2020-02-26 14:16:16.745 [ERROR] [ntime.internal.engine.RuleEngineImpl] - Rule 'test scene': An error occurred during the script execution: null

Note, I know nothing about this storeStates thingy, relying on searches

There’s one obvious pitfall -

Even for a String type Item, a state object is state object.
You stored a string there and so should properly recover a string -

var Map SceneState = SceneString1.state.toString

Don’t think that will fix anything though !

I know you declare a Map type there, but DSL is loosely typed.
I think SceneState will become a simple string variable.
Have to try harder to construct a Map.
It should at least be var Map<Item,state> SceneState
but from what I read there is a trap about keyword Item

This looks really relevant here, both how to construct the Map and hint about populating it -

I think in the end you may need to build code that parses the string and puts the data in the Map one by one?

storeStates and restoreStates works with Map<Item, State> as rossko57 points out. Item Objects in particular have all sorts of stuff injected into them at runtime. You can’t just serialize out and reconstitute an Item that will actually work. It’ll be missing all the extra runtime stuff.

Also, you can’t just pass a big old String of stuff to the Map and expect it to be able to figure out how to deserialize it into the proper Objects. You have to use the right kind of reader which means you need to dig deep into Java IO coding.

Also, I’m not certain whether Item or State are even serializable.

In short, you can’t get there from here. If this is a path you want to try to go down what you need to do is:

  • convert the Map<Item, State> to a Map<String, String> with the key being the Item name and the value a String representation of the State
  • Using https://beginnersbook.com/2013/12/how-to-serialize-hashmap-in-java/ as a guide, serialize the Map but instead of writing it to a file you need to write it to a String (NOTE: I’m not positive this is possible, it may need to be binary). Alternatively, loop through the keys and values and come up with your own String representation (e.g. "Key1->Value1, Key2->Value2").
  • Store that into your String Item.
  • Restore needs to use the link above to deserialize the Map or you need to write code to parse and rebuild the Map.
  • You cannot use restoreStates. You will need to write code to loop through the Items and call sendCommand(key, value) to restore the Items.

The storeStates and restoreStates Actions are clunky to use and it is usually easier to just directly use the persistence methods. You can get the time of Scene_Buttons2’s last OFF state, and you can also restore the state of RestoreBedroom to what it was at that time. The data is already stored, so there is no reason to store it again!

rule "scene 2"
when
    Item Scene_Buttons2 received update ON
then
    RestoreBedroom.sendCommand(
        RestoreBedroom.historicState(// returns the state of RestoreBedroom at a previous point in time
            new DateTime(Scene_Buttons2.previousState(true).timestamp.time)// provides the time the state of Scene_Buttons2 was last different than it is right now, which is ON, so OFF
        ).state.toString// easiest way to convert from State to Command
    )
end

What is the RestoreBedroom Item? A light? If there is more than one Item to restore, you can add them into a group and then iterate through it to restore all of their states. WDYT?

Sorry for the delay, and thanks for the replies
Still not willing to ginve up I see 3 possible ways this could go:

1 Write a java serialiser-stringify function and use it to serialise the map into a string .

  1. Write 2 OpenHAB javascript transformations;
  1. use jsr223

So my bigest question at the moment is whether option 2 is realy a possibility, as that would seem to be a clean way to handle the problem.

  1. You could create a lambda which could be reused by multiple Rules. But lambdas have significant limitations (not thread safe, can only be seen by rules in the same .rules file, limited to only seven arguments, everything must be passed to it as an argument, etc).

  2. I don’t think this will buy you anything. For one, the JS transformation will receive a String, not the Map I believe. But even it it were able to receive the Map, it can only output a String so there would be no way to go back to a Map using a transformation.

  3. This is probably your best bet, but like Scott says, storeStates and restoreStates really are not buying you much at all. You’ll be doing more work trying to use them than you will just storing and restoring the states yourself.

I was looking for a reliable way to save off a group’s prior states for later restore that can survive openhab rule file changes and restarts. I stumbled on this topic and worked the issue. I solved this and thought I’d share my solution, in case someone else might find this useful. I suspect my code could be simplified but it does work.

Note: I use Mapdb for persistence and Influxdb for graphing data, which uses persistence to save off the data. I don’t leverage either in this solution. I am running Openhab on Linux Ubuntu 18 and use MQTT extensively. My solution uses a bash script running all the time in Linux that processes MQTT data sent to it from Openhab.

Basic process: When my alarm goes off, I save the gAlarm_lights (group) current settings then turn all the lights ON for 10 minutes (using an expire for the timer). At the end of 10 minutes, the prior saved settings are applied. I avoid using global variables which are problematic on an openhab restart. I don’t use storeStates/restoreStates.

Items
	Group:Switch:AND(ON,OFF) gAlarm_lights "Alarm Lights" <switch>
		// This has about 8 switches in the group
	
    String gAlarm_lights_to_MQTT "used by rule to send via MQTT to a script"  
		{ mqtt=">[server:myhome/gAlarm_lights:command:*:default]", expire="3s, state='' "  }
		
    Switch alarm_lights_decay    "Alarm trip turns this on"  
        { expire="10m,state=OFF" }

	
Rules
	rule "Turn on lights"  
			// This sends multiple messages via MQTT to the script. 
			//  	<file>
			//		item state
			//		item state
			//		etc
		when
			Item alarm_lights_decay changed from OFF to ON
		then
			send_telegram.sendCommand("Lights turned on for 10 minutes.")  // send me a message that the alarm tripped.
			gAlarm_lights_to_MQTT.sendCommand("<file>")
			gAlarm_lights.members.filter[ i | i.state != NULL ].forEach[ ii |
				gAlarm_lights_to_MQTT.sendCommand(ii.name.toString + " " + ii.state.toString)
			]
			gAlarm_lights.sendCommand(ON)
		end

	rule "Restore Lights"
		when
			Item alarm_lights_decay changed from ON to OFF
		then
			send_telegram.sendCommand("Lights returned to former states.")
			// restore gAlarm_lights by applying saved states
			gAlarm_lights_to_MQTT.sendCommand("<restore>")
		end

Bash Script - started by linux cron @reboot and constantly runs in the background

actionfile="/tmp/openhab_previous.sh"
mosquitto_sub -t "myhome/gAlarm_lights" | while read line
do
    if [[ "$line" =~ "<restore>" ]] ; then
        bash $actionfile
        continue
    fi
    if [[ "$line" =~ "<file>" ]] ; then
        [ -e $actionfile ] && rm $actionfile
        touch $actionfile
        continue
    fi
    item=$(echo $line | awk '{print $1}')
    state=$(echo $line | awk '{print $2}')
    echo "curl -X PUT --header \"Content-Type: text/plain\" --header \"Accept: application/json\" -d \"$state\" \"http://192.168.1.101:8080/rest/items/$item/state\" " >> $actionfile
done
Sample contents of /tmp/openhab_previous.sh
curl -X PUT --header "Content-Type: text/plain" --header "Accept: application/json" -d "OFF" "http://192.168.1.101:8080/rest/items/garage_light/state"
curl -X PUT --header "Content-Type: text/plain" --header "Accept: application/json" -d "OFF" "http://192.168.1.101:8080/rest/items/masterbedroom_light/state"