Interesting. Can you elaborate on what error Designer is giving you when you only have Items in your hashmap? It sounds like a bug if you are forced to put something else in there that you won’t use. I’ve only used Items as the Key in my hashmaps , usually with a Timer as the object so I’ve not encountered this problem and I’m curious.
I have a similar feature in my lights rules. I let the lights turn on or off based on what Yahoo says the weather is like (if it is cloudy they come on, sunny they go off). However, if someone manually turns on or off the light it disables the weather rule for the rest of the day. I solved the problem though a rule which gets called every time a light is toggled and checks whether it was manually toggled, a Map<SwitchItem, Boolean> which gets set to true when the switch is toggled manually (either through OH or by physically pressing the switch), and a that lambda checks the override before turning on or off the light. At sunset, when the weather rule is turned off anyway, the override booleans are reset.
With this approach the lambda doesn’t really need to know who called it and whether or not a command is overridden is controlled in the rules, leaving the lambda completely independent of the rules. I don’t know if this is better or worse than other approaches but I’m happy and I like to keep my code chunks as generic as possible to maximize readability and re-usability. However, unlike you who seems to have his lighting pretty squared away, I am still adding lights and I like not needing to touch the rules file to add a light and get it to behave as I want.
I’ve included my full lights rules here as it shows what I did better than I can explain and it might prove useful to other readers.
Items
Group:Switch:OR(OFF,ON) gLights "All Lights" <light>
Group gWeatherLights "Lights controlled by weather conditions and twilight" <light>
Group gOfftimerLights "Off Timer Lights" <light>
Group gSunsetTimerLights "Sunset on Lights" <light>
Switch S_L_Front "Front Room Lamp" <light> (gLights, gOffTimerLights, gWeatherLights) {zwave...
Switch S_L_Family "Family Room Lamp" <light> (gLights, gOffTimerLights, gWeatherLights) {zwave...
Switch S_L_Porch "Front Proch" <light> (gLights, gOffTimerLights, gSunsetTimerLights) {zwave...
Switch S_L_All "All Lights" <light>
Rules
import org.openhab.core.types.*
import org.openhab.core.library.items.*
import org.eclipse.xtext.xbase.lib.*
import java.util.Map
import java.util.Set
val String TIMER = "TIMER"
val String WEATHER = "WEATHER"
val String MAUNAL = "MANAUAL"
var String whoCalled = ""
var boolean day = true
val Map<SwitchItem, Boolean> overridden = newHashMap
val Set<String> cloudyIds = newImmutableSet("0", "1", "2", "3", "4", "5", "6", "7", "8",
"9", "10", "11", "12", "13", "14", "15", "16", "17",
"18", "19", "20", "26", "28", "35", "41", "43", "45",
"46", "30", "38")
val Functions$Function3 applySwitch = [State state, boolean override, SwitchItem light |
if(state != light.state) {
if(!override) sendCommand(light, state.toString)
}
]
rule "Lights System Started"
when
System started
then
whoCalled = MANUAL
gLights?.members.forEach[light | overridden.put(light as SwitchItem, false) ]
end
// Triggered when any light in gLights receives a command, either through OH or from the switch
rule "Light triggered"
when
Item gLights received update
then
Thread::sleep(250) // give lastUpdate time to populate
if(whoCalled == MANUAL) overridden.put(gLights?.members.sortBy[lastUpdate].last as SwitchItem, true)
end
rule "All Lights"
when
Item S_L_All received command
then
gLights?.members.forEach(item | sendCommand(item, S_L_All.state.toString))
end
rule "Lights Twilight"
when
Item Twilight_Event received update
then
whoCalled = TIMER
day = false // deactivates the weather rule
gWeatherLights?.members.forEach[light |
overridden.put(light as SwitchItem, false)
applySwitch.apply(ON, false, light)
]
end
rule "Lights Sunset"
when
Item Sunset_Event received update
then
whoCalled = TIMER
gSunsetTimerLights?.members.forEach[light |
overridden.put(light as SwitchItem, false)
applySwitch.apply(ON, false, lights)
]
end
rule "Lights Bedtime"
when
Time cron "0 0 23 * * ? *"
then
whoCalled = TIMER
gOffTimerLights?.members.forEach[light |
overridden.put(light as SwitchItem, false)
applySwitch(OFF, false, light)
]
end
rule "Weather Lights On"
when
Item Contition_Id changed
then
if(day) {
var State state = OFF
if (cloudyIds.contains(Condition_Id.state)) state = ON
whoCalled = WEATHER
val i = gWeatherLights?.members.iterator
while(i.hasNext){
val light = i.next
if(overridden.get(light) == null) overridden.put(light as SwitchItem, false)
applySwitch.apply(state, overridden.get(light).booleanValue, light)
}
Thread::sleep(500)
whoCalled = MANUAL
}
end
This is indeed a limitation of my approach. My way to work with this limitation is to create (sometimes temporarily) a switch item to trigger my rule and I just manually trigger it to make sure everything is as I want it to be as a test rather than relying on an error in the logs. If I do have an error it is usually because I have a typo in the group I’ve assigned an Item to and I would much rather the rest of the rules and lights work than have the whole system crash. The benefit of being able to add, remove, and modify the behavior of an Item without having to touch the rules (and potentially mucking things up) plus being able to have very easy to read and maintain rules when I do need to touch them is more than worth needing to do this part manually to me.
It is a different coding philosophy closer to weakly typed programming languages (Ruby/JavaScript/etc.) than strongly typed languages (C/C++/Java/C#/etc.). The former prioritizes flexibility over compile-time error checking. This flexibility comes at the cost of most errors only being detected at runtime. The latter prioritizes early error detection over the flexibility offered by weakly typed languages. XTend as a language is closer to a weakly typed language than a strongly typed language so if you want the kind of error checking you are looking for you need to code it yourself, as you have found.
Assuming your item is part of a Group, you can filter on the name you care about. This might get you by. I agree it won’t get implemented in OH1 but maybe in OH2.
if(group?.members.filter(s|s.name("Name")).empty){
// Item doesn't exist
}
Even having studied your rules I’m not sure I understand why you need to do this. I guess what you are looking for is to have your code be static and crash if you have an error like one of your switches not part of a rule but it should be? This is probably a difference in coding philosophy again. I personally would much rather have the missing light not turn on than have the whole rule set crash so it wouldn’t occur to me to want to do this, even back when I was implementing everything with Maps instead of Groups.
It is actually a lot shorter if you count the code that populates the hashmap you are iterating over. I’d also argue that the code is easier to read and maintain because the declarative stuff (i.e. populating the hashmap) is all in one place (Items files) and the executive stuff is all in one place (Rules files). But we have different coding philosophies and you want some load time checks to be performed that you can’t really do with my approach so you would probably have the hashmap stuff in anyway to get your load time error checks
This was fun and I learned a lot. Even retyping in my lights rules above caused me to think of a few more ways to make them simpler and more concise.
Thank you!
Rich
[Edits: some formatting and a couple errors in the rules]