Variables vs. helper items

Hey,

in the course of transfering my rule set from RuleDSL to JRuby I am questioning a concept I am currently using to find out whether it could be achieved more easily.

There are situations where I am using (abusing?) items as variables and I am not so sure whether this is a good approach. Those virtual items are strongly coupled to real world items.
Example: I have a rule which opens specific blinds in winter when a certain window was opened for better air circulation (Germans do love “Stosslueften” ^^).

Once the window was closed, the respective blinds get closed again to the original position they had before the usecase started. To close the blinds in RuleDSL I do something like this:

var String raumName = triggeringItem.name.split("_").get(2) // In which room was the window closed?

val jalousienZumSchliessen = gJalousienZumLueften.members.filter[ jalousie | jalousie.name.toString.contains(raumName) ]	
        if (jalousienZumSchliessen.size == 0) {
            logInfo("Fenster: Stoßlüften Jalousie(n) runterfahren", "Keine Jalousien zum Schließen im Raum: " + raumName)
            return; // This room does not have any blinds to be opened/closed for air circulation
        }
            
        jalousienZumSchliessen.forEach[ jalousie | 
            val NumberItem v_Jalousie_vorherPos = gJalousienVorherPos.members.findFirst[ jalousieVorherPos | 
                jalousieVorherPos.name.toString.contains(raumName)] as NumberItem // Let's get this blind's previous position before it was opened	
            
            jalousie.sendCommand(v_Jalousie_vorherPos.state as Number)  // Let's close the blind
        ]

This is just an example for virtual items I am using. There are more.
I’m just wondering whether there are better concepts out there. If so, how can the relationship between an item, in this case the blind’s position and the blind’s previous position before the window was opened, be managed easily? I would like to avoid of course to create one variable per blind like this:

blind_kitchen_prevPos = 0
blind_livingRoom_prevPos = 0
...

Doing so I would not win a lot as I need to use reflection again to find the correct variable.

Any idea or is the current approach not that bad?

I would do this using Semantic model + metadata to store the previous position. Paste your relevant items (windows / blinds) here and I’ll give you an example how it can be done.

Man, you are such a helpful person… :pray:

So, here is the living room window contact

Contact                     v_Fenster_Wohnzimmer
                            "Fenster Küche Nord [MAP(position.map):%s]"
                            <window>
                            (gFensterkontakte, gKueche)
                            ["Window"]              
                            { channel="deconz:openclosesensor:raspi4:Fenstersensor_KuecheNord:open" }

When it gets opened I want the following blinds to open and closed once the window gets closed. This is currently solved by having them in the “blind for air circulation” group (gJalousienZumLueften).

Rollershutter               va_Jalousie_SollPos_Wohnzimmer_KuecheNord
                            "Küche Nord"
                            <rollershutter>
							(gJalousien, gJalousienZumLueften, gKueche, gJalousienEG, gJalousien_Nord)   
                            ["Blinds"]
                            { channel="shelly:shelly25-roller:<ID>:roller#control" [profile="ruby:jalousie"], homekit = "WindowCovering, WindowCovering.CurrentPosition, WindowCovering.TargetPosition, WindowCovering.PositionState"}                     


Rollershutter               va_Jalousie_SollPos_Wohnzimmer_Terrasse
                            "Wohnzimmer Terrasse"
                            <rollershutter>							
                            (gJalousien, gJalousienZumLueften, gJalousien_Sued, gWohnzimmer, gJalousienEG)        
                            ["Blinds"]                                            
                            { channel="shelly:shelly25-roller:<ID>:roller#control" [profile="ruby:jalousie"], homekit = "WindowCovering, WindowCovering.CurrentPosition, WindowCovering.TargetPosition, WindowCovering.PositionState"}

And just for the sake of completeness, these are the items I would like to get rid of:

Number                      v_Jalousie_vorherPos_Wohnzimmer_KuecheNord
                            "Position der Jalousie Küche Nord vor dem Stoßlüften: [%d]"
                            (gJalousienVorherPos)

Number                      v_Jalousie_vorherPos_Wohnzimmer_Terrasse
                            "Position der Jalousie Terrasse vor dem Stoßlüften: [%d]"
                            (gJalousienVorherPos)

Just so I understand your setup better, do you have other contact sensors that you want to associate with other blinds, or is this the only contact and two blinds you’re wanting to automate in this manner in the whole house?

The va_Jalousie_SollPos_Wohnzimmer_Terrasse is not located inside gKueche?

PS I don’t know German so I’ll need your help with the meaning of those group names :slight_smile:

And the next question: are you OK to store the previous blinds position in memory, losing it if you reload the script file or restart openhab?

Yes, loosing the values is OK for me as the system usually runs and runs and runs…

Yeah, this kitchen (Kueche) and living room (Wohnzimmer) thing is a very special thing in my house which I would actually get rid of in the refactor.
Kitchen and living room are actually the same room. It is the typical “open room” concept so there is nothing which separates the kitchen from the living room. Means: in the refactoring we can consider that both blinds are inside the same location in the future (would be gWohnzimmer ).

I need this functionality in every room. Usually it is just one blind per room which gets opened and that is the blind of the window having the window contact. However, the living room is that big that it makes sense to open two blinds.

I wanted to make the number of blinds to be opened in a dynamic way back then in case I borrow my code to a friend who has a completely different house but wants the same feature. Might be that in a another place once window contact triggers 5 different blinds.

For money saving reasons I have only one window contact per room and not at every window. The living room however does not have a contact as the kitchen already has and we just learned: kitchen === living room. :joy:

OK Here’s the first iteration of the code. Here, I chose to use a custom Hash so you are completely free to associate any window with any blinds in the house. If on the other hand, you always have one window per room, and you want to open all blinds within that room, we can just use the semantic model to find all the blinds in the room, and we won’t need to use this custom Hash. But for now give this a go.

Notice I added for: 5.seconds in the changed trigger. This is so that you can quickly open and close the window (as many times as you like) in less than 5 seconds interval, and the blinds won’t immediately react. Feel free to remove if you don’t want that feature.

# This Hash links the contact sensor to the blinds that need to be opened/closed accordingly
WINDOWS_TO_BLINDS = {
  v_Fenster_Wohnzimmer => [va_Jalousie_SollPos_Wohnzimmer_KuecheNord, va_Jalousie_SollPos_Wohnzimmer_Terrasse]
}.freeze

@saved_positions = {}

rule "Open/Close Blinds When Window Opens/Closes" do
  changed gFensterkontakte.members, to: [OPEN, CLOSED], for: 5.seconds 
  run do |event|
    next unless WINDOWS_TO_BLINDS.key?(event.item) # Only respond if it's one of the windows listed above

    if event.open? # The window was opened
      blinds = WINDOWS_TO_BLINDS[event.item]
      @saved_positions[event.item] = store_states(*blinds)
      blinds.up
    else
      @saved_positions.delete(event.item)&.restore
    end
  end
end

Great, thanks! That makes sense. So to add another window to the hash it would go like this?

WINDOWS_TO_BLINDS = {
  v_Fenster_Wohnzimmer => [va_Jalousie_SollPos_Wohnzimmer_KuecheNord, va_Jalousie_SollPos_Wohnzimmer_Terrasse], another_window_contact => another_blind
}.freeze

In the end it would probably make more sense to open the blind which has an associated window contact so adding another sensor to my 2nd living room window.

PS. I just came across this site. It looks like the metadata is quite powerful for these kind of usecases: https://boc-tothefuture.github.io/openhab-jruby/usage/misc/metadata/

To add another window

WINDOWS_TO_BLINDS = {
  v_Fenster_Wohnzimmer => [va_Jalousie_SollPos_Wohnzimmer_KuecheNord, va_Jalousie_SollPos_Wohnzimmer_Terrasse], 
  another_window_contact => [another_blind]
}.freeze

https://boc-tothefuture.github.io/openhab-jruby/ is for the older version of the library (v4.x). The new and official version is at https://openhab.github.io/openhab-jruby/

Yes you can use metadata, and it will survive an openhab restart. In the new library (v5) you’ll need to call provider! to make it persistent. For your use case maybe using a variable like in my first example would be sufficient.

Here’s a version when you have your semantic model set up correctly.

@saved_positions = {}

rule "Open/Close Blinds When Window Opens/Closes" do
  changed gFensterkontakte.members, to: [OPEN, CLOSED], for: 5.seconds
  run do |event|
    next unless event.item.location # Skip windows that don't have a semantic location

    blinds = event.item.location.members
                  .equipments(Semantics::Blinds).members
                  .points(Semantics::OpenLevel)
    next if blinds.empty?

    if event.open? # The window was opened
      @saved_positions[event.item] = store_states(*blinds)
      blinds.open
    else
      @saved_positions.delete(event.item)&.restore
    end
  end
end

Note: Currently you’ve assigned Window and Blinds to your actual contact/rollershutter items. Whilst this is allowed, it’s not recommended.

Window and Blinds are Equipment, and they are usually associated with a group. One of the things about setting up the semantic model is to have to create virtual groups that act as the equipment for the actual item.

For myself, I do the same as you for Window: Assign it directly to the Contact item. But for the Blinds, I do it like this:

// This is the Location semantic 
Group gLivingRoom ["LivingRoom"]

Contact        v_Fenster_Wohnzimmer (gLivingRoom) ["Window"]

Group LivingRoom_Blinds1 (gLivingRoom) ["Blinds"]
Rollershutter  va_Jalousie_SollPos_Wohnzimmer_KuecheNord (LivingRoom_Blinds1) ["OpenLevel"]

Group LivingRoom_Blinds2 (gLivingRoom) ["Blinds"]
Rollershutter  va_Jalousie_SollPos_Wohnzimmer_Terrasse (LivingRoom_Blinds2) ["OpenLevel"]

So the rule above will work by finding all the Blinds (equipment) in the same room as the Window and then finding the OpenLevel (Point) that belongs to the Blinds

If you have persistency switched on, you could simply get the last status from there.
But this does only help if you do not change the position manually before Stoßlüften is finished

If you want all blinds in the room to open when any window is opened, then the code will work as is, even if you add more window sensors in the same room.

If you want to open only the blinds that belong to the same window that opens… just adjust the code and your semantic model slightly, so that you have

Equipment (LivingRoom_Window1)
- Point: contact sensor for that window
- Point: OpenLevel (rollershutter for the blind on that window)

Then you would find the associated blinds like this:

    blinds = event.item.points(Semantics::OpenLevel)

The rest of the code is the same.

Indeed that’s the weakness of using persistence with everyChange strategy. You can use manual strategy and when the window opened, you can tell persistence to persist. Then you can use the previous value because other changes won’t automatically be saved. In essence using persistence as your storage instead of in-memory variable, or metadata.

@JimT 's jRuby solution is one perfectly good way to go here, so I just want to comment on one fact.

  1. OH is as powerful as it is because it is so flexible. It’s flexible because it’s a platform for creating your own home automation system; it is not a pre-built home automation system that you have to shoehorn your own home into. As such, there aren’t a lot of “using X wrong” situations. If you are using it and it works, then you’ve found a viable solution. Sure, for every solution there might be an experienced forum user or dev who can help you streamline it or improve it a little, but the most important part is that it works for you and you understand it.

  2. There’s no distinction between items and variables. Items are just the user facing variables of OH.
    You can see many examples all over this forum where someone is struggling for days/weeks to implement something that is unworkably complex just because they are trying to avoid using an item out of the mistaken impression that they shouldn’t be using items for anything other than connecting to devices. The advanced OH setups will often have more of these “virtual” items then they do device-linked items.

For me, I always try to store information via an item first. If an item doesn’t work for my needs (for example the information is too complex) then I investigate whether metadata is more appropriate. If metadata won’t work (for example, if the information changes really rapidly at times), then I will go for the full on rule.

This flow has changed a little with the introduction of the new shared and private rules caches. If you are comfortable with scripting in any of the non-DSL languages then there are many times when storing information in the cache is just as effective as using an item. Even then, as hinted at above, items still come out on top if you need the value stored between reboots of OH or the value is important for some aspect of your UI.

2 Likes

I can’t do much better than already covered here by @JustinG and @JimT but for completeness I’ll list all the ways to save data like this. Which is most appropriate will largely depend on the situation.

Technique Advantages Disadvantages
Items get restored on startup limited in the types of data that can be stored, may require a String that later needs to be parsed or a proliferation of Items, sometimes can be auto-populated through transformations and/or profiles
cache can share data between files, not just rules, Timers automatically get canceled, in some cases can even be shared between rules of different languages doesn’t get restored on startup
Persistence mostly get this for free can be slower than rules causing timing issues, only useful when needing to compare/use some previous state
Item Tags available in all rules languages, a nice way to dynamically group Items for some reason, great for boolean info some tag names are reserved, complicated data is not supported
Group membership similar to tags similar to tags
Item Metadata metadata persists until you delete it, becomes part of OH config not available in Rules DSL
Item Naming particularly easy to use with replace and split, great to be used with the semantic model best for static data, not data that changes at runtime (see Design Pattern: Encoding and Accessing Values in Rules)
Global Variables lives close to the code not possible from managed rules (see cache for alternative), only exist in the file they are defined
Transformations (e.g. .map files) can be set up separate from the rules limited in utility to mapping one value to another (e.g. an Item name to a timeout value)
Text Files (load a config from a JSON or .properties file) best for static or rarely changing data requires different formats and approaches depending what’s available in the language, can be slow

The cache and global variables and I think even Item metadata can be used with the storeStates and restoreStates actions as a way to capture the current state of a bunch of Items and restore them later.

I think that covers all the options.

To provide some examples in how I use some of the above:

  • Many of my semantic Equipment have a Status Item. I follow the same naming convention for all these so in a rule that detects that a piece of Equipment has gone down, I can get the Equipment and from there get the Status Item to update (this is for cases where I don’t detect an Equipment has gone offline except by monitoring something indirectly such as how long has it been since a sensor has reported or the Thing’s ONLINE status).

  • I autocontrol some lamps based on the cloudiness but sometimes we want the light to be ON or OFF regardless of cloudiness. When a lamp is triggered manually, I set an override Item metadata to stop changing the light based on cloudiness. The flag gets cleared at the next time of day. In hind sight a tag would probably be more appropriate.

  • I use Design Pattern: Associated Items all over the place but have doing less so as I adopt using the semantic model from my rules more.

  • For keeping track of the last time an Item was updated I use a separate Item and the timestamp profile.

That’s what Maps are for. One variable that you populate with key/value pairs.

4 Likes

I don’t think I seen you summarize this info like this before. This is IMO worthy of porting directly to the Getting Started - Tips and Tricks doc.

1 Like

This was supposed to be captured by that “Encoding” Design pattern but that DP (as do most of them really) needs some major updating.

I’m not sure Getting Started is the best place for it. But I can see it being part of the rules docs rewrite. Getting Started is already too long so I think making it part of the reference docs and linking to it from Getting Started might be better.

Wow, this thread really develops into a great “Getting started and a little more…” discussion!
I’ve learned so much already and need to digest this a little.

I fully agree and as you mentioned: I could actually solve all my usecases so far with RuleDSL. The system works to my needs although it has grown over time.

But: I noticed that I made some special choices in the past which make the adaptation of my system, let’s say I add a new color temperature light which I would like to control mainly in homekit, more complex than it needs to be. To be honest: I sometimes look at my code and think: “Geez… I hope this will never break. This looks more complex than it needs to be!”

I always wondered about tiny details which bloated my code a lot: Why do I need such a complicated syncing mechanism between Kelvin and Mired items (see Synchronize 2 dependent items), why do I need to manually take care of timers in typical usecases (see Avoid state flickering with JRuby scripts)?
So I started my investigations and from a gut feeling found that JRuby seems to be quite powerful as it can handle lots of things in the background. This combined with the use of the semantic model.

That is why I would like to refactor my current system. The goal should be: Way less code to achieve the same behaviour of the system.