The core problem here is strangeness with val
and var
and not being able to get two file-level identifiers refer to the same object instance.
The difficulties here have arisen because, at least as far as I have seen, and no matter the choices of val
vs. var
here
val Number x = 1
val Number y = x
results in a null
value for y
This is certainly unexpected, especially as val
is supposed to be immutable. Even if there was a trigger that could be used when the Rules were loaded from the file, you wouldn’t be able to change the value of y
The problem encountered was not that the second parameter was failing to be passed properly, but that it was null
from the above.
Was:
Maybe I’m missing something, but trying to get to even modest code reuse in Rules is driving me nuts.
TL;DR
Designer indicates that Procedure2.apply() is the proper way to “call” the procedure/function referenced by a variable, in my case
void Procedure2.apply(GroupItem arg0, LinkedHashMap<String, Number> arg1)
This appears to fail at runtime with the second argument seen by the Procedure2 being null
Note that calling a one-argument Procedure1 in this way works as expected.
First off, why
- 20 dimmers
- 10 remotes
- Each with six buttons
- No desire to replicate code 10, 20, 60, or more times
- Keep configuration readable, diff-able,
Please don’t give me the whole “You have to think differently” line. If you’ve got a system that’s based on the latest and greatest shiny objects of OSGi, Karaf, and a leading vendors IDE and proprietary languages, then it should give you basic Programming 101 concepts of modularity and reuse, even if you can’t define your own first-class objects.
Overview of Problem
So far, I have two tasks that are common over every remote:
- Send a
STOP
command to all in a group of dimmers (which is blocked by the group) - For each dimmer in a group, look up the preset value in a map and, if not null, set the dimmer level to that value
Let’s look at these one at a time, to see what works, and what doesn’t.
First off, trusting http://docs.openhab.org/configuration/rules-dsl.html that the Xtend documentation somehow describes the acceptable rules syntax is a little optimistic. There is quite a bit that Xtend can do, that you can’t do in rules, such as defining your own classes and methods.
From what I can gather, somename.rules
starts generating a class SomenameRules. The var
and val
statements become fields of the class, and the rule
declarations become methods, names prefaced with an underscore.
##Defining and Using Single-Argument Methods
Since you can’t define your own general methods, but you can define lambda expressions
val dimmer_group_send_stop = [ GroupItem dimmer_group |
dimmer_group.members.forEach [ dimmer |
sendCommand(dimmer, STOP)
]
]
works quite nicely to be able to define a single-parameter method/function that has the “easy” scope of the rules file. I might be able define it externally and do some crazy gymnastics to get to it through its auto-generated class, but I just want to get things working for now.
Yes, it works just fine in a rule like
rule "Test_Pico_Button_Up_Down_released"
when
Item pico_Test_Pico_up changed from ON to OFF or
Item pico_Test_Pico_down changed from ON to OFF
then
dimmer_group_send_stop.apply(group_Kitchen_dimmers)
end
Defining and Failing With Two-Argument Methods
Map Dimmer to Value
Since the MAP
and other transformation approaches in openHAB seem focused around strings, and dimmers need a number for the value, looking to something simpler was in order. Yes, I could define an Item for every dimmer, for every scene, and set them to the level for the scene, oh, yeah, need a UI for that… Are you crazy? That’s an insane amount of work for little gain. I just want to stuff the levels into a file and use them, at least for now. Once I get past trying to get the basic keypad functionality working half as well as the Lutron app provides, then I go to the binding and fix it so that it can activate the scenes managed by the hub itself, rather than duplicating it again in openHAB.
import java.util.LinkedHashMap
val LinkedHashMap<String, Number> scene_dim_bedroom_map =
newLinkedHashMap(
'caseta_dimmer_Bedroom_East_Cans' -> 30,
'caseta_dimmer_Bedroom_West_Spots' -> 0,
'caseta_dimmer_Bedroom_South_Cans' -> 30,
'caseta_dimmer_Bedroom_Closet_Spots' -> 30
)
Lots of ways to define it. Dimmer.name is unique, and, at least the way I have set them, readable and understandable. Lots of syntax options as well, but the Java-like, explicit definition helps remove ambiguity over something like
val scene_dim_bedroom_map =
newLinkedHashMap(
'caseta_dimmer_Bedroom_East_Cans' -> 30 as Number,
'caseta_dimmer_Bedroom_West_Spots' -> 0,
'caseta_dimmer_Bedroom_South_Cans' -> 30,
'caseta_dimmer_Bedroom_Closet_Spots' -> 30
)
or the unruly approach of not declaring the value type and hoping it can be deduced and cast properly.
Now, that works nicely in a rule action like
then
group_Bedroom_dimmers.members.forEach [ dimmer |
val String dimmer_name = dimmer.name
val Number dimmer_level = scene_dim_bedroom.get(dimmer_name)
if ( dimmer_level != null) {
sendCommand(dimmer, dimmer_level)
}
]
(Before you jump on me, yes. I’m aware of allMembers
but I’m interested in solving the core of the problem, not in tweaking my code to perfection for another of those “Design Pattern” postings at this point.)
Now, to generify that, so that it can be used for the ten or more times I need a button to set a scene
val dimmer_group_set_to_preset = [ GroupItem dimmer_group, LinkedHashMap<String, Number> preset_map |
dimmer_group.members.forEach [ dimmer |
val String dimmer_name = dimmer.name
val Number dimmer_level = preset_map.get(dimmer_name)
if ( dimmer_level != null) {
sendCommand(dimmer, dimmer_level)
}
]
]
This, however fails at runtime in what should be effectively the same rule action
then
dimmer_group_set_to_preset( group_Bedroom_dimmers.members, scene_dim_bedroom)
Adding logging shows that the preset_map
is null
on entry to the Procedure.
Designer gives apply()
as the suggestion, with the proper argument types, doesn’t indicate an error, and does show information to suggest that it would work as expected.
Hovering dimmer_group_set_to_preset
Procedure2<GroupItem, LinkedHashMap<String, Number>> PicoRules.dimmer_group_set_to_preset
Hovering dimmer_group_set_to_preset.apply
void Procedure2.apply(GroupItem arg0, LinkedHashMap<String, Number> arg1)
Yet when it runs, the second argument is seen as null
by the unmodified code, as well as the second logging statement in
val dimmer_group_set_to_preset = [ GroupItem dimmer_group, LinkedHashMap<String, Number> preset_map |
logInfo("DimmerGroupPreset", "Entry with dimmer_group '{}'",
dimmer_group.toString)
logInfo("DimmerGroupPreset", "Entry with preset_map '{}'",
preset_map.toString)
(Yeah, I know, .toString
can’t be accessed from null
)
So What Gives?
Why isn’t the second argument being picked up by the apply
call and delivered to the code’s execution environment?