Get group by name

Hi all

Has anyone got a good/easy way to get a group, which may be nested many levels down, from a parent group?

Thanks

Assuming you know the name of the nested group you can probably do something like:

ParentGroup?.members.filter(sg | sg.name == "Subgroup")?.members.filter(g | g.name == "Goal group").head as GroupItem

I’m not 100% certain of syntax above but I think it should work.

Rich

@rlkoshak Thanks, I’ll give it a go.

I know the “GoalGroup”, and the “ParentGroup” but not how many far down the nesting goes to get to that GoalGroup.

Out of interest. what does the ? after the ParentGroup do?

That makes it a lot more complicated. I really really don’t know if this will work but Designer doesn’t have a problem with it.

val ArrayList<GroupItem> found = newArrayList

val Functions$Function4 findGr = [ String name, GroupItem gr, ArrayList<GroupItem> found, Functions$Function4 func |
    gr?.members.filter(g | g instanceof GroupItem).forEach(g |
        if(g.name == name) found.add(g as GroupItem)
        else func.apply(name, g, found, func)
    )
    true
]

rule "My Rule"
when
    // trigger
then
    // do stuff

    var gr = null
    findGr.apply(name, ParentGroup, found, findGr)
    if(found.empty) logError("Rule", "Couldn't find group" + name)
    else if(found.size > 1) logError("Rule", "Found more than one match for name!")
    else {
        gr = found.head
        found.clear

        // Do stuff with gr
    }
end

The “?” basically interprets the ParentGroup?.members as:

if(ParentGroup != null) ParentGroup.members

Its a nice shorthand way to avoid null pointer exceptions.

Rich

Hi Rich

I think the general idea is good, however the recursive call seems to cause erratic behaviour. The first time I run it I get an error on the found.add line due to found (apparently) being null. If I run it again I get the message where found.size > 1 and there’s definitely only one group that matches the name.

Any other ideas? I can’t imagine that what I’m doing is that far-fetched that there isn’t a simple solution.

Here’s an example:

rule “process speech”
when
Item VoiceCommand received command
then
var String group = “lights”
var String state = “OFF”

// Pseudo code:
// GroupItem gr = findGroup(group)
// if (gr != null) {
// gr.sendCommand(state)
//}

end

I wasn’t sure whether the recursion from a lambda would work or not. It is weird that it is complaining about found being null though. If it is null there is should be null everywhere and for all time because it is immutable (val is like using final in Java). Weird.

Have you printed out what is in found on the second run? I wonder if the first item is actually added to the ArrayList and because of the error the ArrayList isn’t being cleared so both items are there at that point. The code probably needs to be reviewed and modified to make sure that found gets cleared even in the case of an error.

I suspect that the code just needs to be debugged. However, before spending time on that…

Actually what you are trying to do is pretty far from the typical use case. OH 1.x wasn’t really designed to get a handle on Items and Groups dynamically (e.g. construct the name of the item or group as a String and resolve it from there) and the work arounds require a the code to have at least some knowledge of the item’s name.

However, from looking at your pseudocode I wonder, can’t you just use sendCommand(String, String) where the first String is the name of the item or group and the second String is the state? Then you don’t even have to recursively traverse your Group hierarchy. You already know the name of the Group you want (which I presume you are building as a String somehow from the VoiceCommand) and all Groups have to have a unique name anyway so that shouldn’t trip you up.

Rich

Wow…

Using the sendCommand(String, String) for a group is just the type of thinking outside the box I was hoping/searching for! I’ll try it later and let you know.

However…having said that, the example in the pseudo code was the most basic example and doesn’t cover the more complicated (usual) usage.

Here’s how I how it planned out, say I’ve got the following item file:

Group all
Group living_room (all)
Group bedroom (all)
Group lights (all)
Group tvs (all)

String bedroom_light (bedroom, lights)
String bedroom_lamp (bedroom, lights)
String bedroom_tv (bedroom, tvs)

Voice command: “turn off the bedroom lights”

If I parse the VoiceCommand and find that the location is “bedroom” and device is “lights” I want to turn off all devices where those group’s items intersect (e.g. bedroom_light, bedroom_lamp) as they are in the bedroom AND is a light.

Can you think of how I might solve this bit?

I would and have used the the following approach. Basically I’d flatten the Group hierarchy and instead embed hierarchy in the name. Come up with a naming scheme for your groups: e.g. - and create a Group for all combinations you want to control in this manner. In your rule construct the name of the group from the text of your voice command and send the command using sendCommand(name, state).

For example:
Items:

Group bedroom-lights
Group bedroom-tv

Switch bedroom_light (bedroom-lights)
Switch bedroom_lamp (bedroom-lights)
Switch bedroom_tv (bedroom-tv)

Rules:

rule "Voice command"
when
    Item VoiceCommand received command
then
    var parts = VoiceCommand.state.split
    var cmd = if(parts[1] == "on") "ON" else "OFF"
    var grp = parts[2] + "-" + parts[3]
    sendCommand(grp, cmd)
end

Not the code above is notional. You might need to use a StringBuilder to construct the grp name and I just typed it in so there can be major errors as is. I assume you already figured out how to parse the VoiceCommand string to get the location and device type.

Your Items files will probably look a little uglier but your Rules files will be a lot cleaner. In my opinion that is a fair trade.

Another approach, perhaps a better one, is to use the groupNames method on the Items. This approach will leave your Items files as is, assuming that everything is a member of all (either directly or as part of a subgroup).

rule "Voice Command"
when
    Item VoiceCommand received command
then
    val location = // location string
    val device = // device group string
    val state = // state string

    all.allMembers.filter(i | i.groupNames.contains(location) && i.groupNames.contains(device)).forEach(i | i.sendCommand(state))
end

The magic happens on that last line. The allMembers method gets all the Items to include those items that are members of subgroups but it doesn’t return the subgroups. The filter method returns a list of just those Items that match the condition (i.e. Items who are members of the location and device groups). The forEach iterates over each item return by filter and calls sendCommand with the new state.

NOTE: This is a much simpler problem to solve compared to your original question. You only have two Groups you are trying to match against rather than finding a Group by name in an arbitrarily deep hierarchy. If you do need to search for Items that are members of an arbitrary number of Group names you won’t be able to do it all on one line. Its possible to do it but it will take a lot more looping and book keeping.

Rich

At first I thought that last one liner would do it but then realised it doesn’t handle nesting.

The reason my groups are hierarchical is so that I can, if necessary, send commands to a parent group to control all child group/items.

I’ve been tweaking my items file again:

Group all

Group house (all)
Group upstairs (house)
Group downstairs (house)
Group bedroom (upstairs)
Group bathroom (upstairs)
Group living_room (downstairs)
Group kitchen (downstairs)

Group devices (all)
Group lights (devices)
Group tvs (devices)

String bedroom_light (bedroom, lights)
String bedroom_tv (bedroom, tvs)

String living_room_light (living_room, lights)
String living_room_tv (living_room, tvs)

String bathroom_light (bathroom, lights)

The hierarchical structure should allow me to do things like “turn on the lights in the house” and then all the children of both house (e.g. everything upstairs and downstairs) and lights would turn on.

I’ll see if I can get the function working. I’ve noticed though that functions don’t seem to execute in the same environment as the rules.

That is correct. They are completely isolated. Any variables, Items, or anything else you want to use in a lambda has to be passed into it as an argument. Also note that you are limited to no more than six or seven arguments (I can’t remember which).

Well, the simplest thing to do would be to simply collapse the hierarchy and just directly assign the parent groups to the appropriate items. It isn’t as clean in the Items definitions but the Rule becomes simpler.

String bedroom_light (all, house, upstairs, bedroom, lights)

Another approach to collapsing the hierarchy would be to encode it in a single group name and then use String’s .contains to find the group. This might be a little cleaner in the Items files, maybe not.

String bedroom_light (all_house_upstairs_bedroom, lights)

val location = // location string
val device = // device group string
val state = // state string

all.allMembers.filter(i | !i.groupNames.filter(n| n.contains(location)).empty && i.groupNames.contains(device)).forEach(i | i.sendCommand(state))

Beyond that, it occurred to me that one should be able to re-implement the recursion in a loop. See if this approach works better. It grabs all the groups and puts them into a HashMap keyed on their names. Then in your rule you can grab the location group by name from the HashMap and then filter for those items that are members of the device group to send the command to:

val HashMap<String, GroupItem> grps = newHashMap

rule "System started, get all the groups"
when
    System started
then
    val ArrayList<GroupItem> grpsList = newArrayList
    all.members.filter(g | g instanceof GroupItem).forEach(g | grpsList.add(g as GroupItem))
    var int index = 0
    while(index < grpsList.size){
        grpsList.get(index).members.filter(g | g instanceof GroupItem).forEach(g | grpsList.add(g as GroupItem))
        index = index+1
    }

    // Put them into the global map for later reference
    grpsList.members.forEach(g | grps.put(g.name, g)
end

rule "Voice Command"
when
    Item VoiceCommand received command
then
    val location = // location
    val device = // device
    val state = // state

    if(grps.get(location) != null) {
        val locGrp = grps.get(location)
        locGrp.allMembers.filter(g | g.groupNames.contains(device)).forEach(i | i.sendCommand(state))
    }
    else {
        logWarn("VoiceCommand", location + " not found")
    }
end

Again, I’ve just typed this in. There may be errors.

Rich

Hi Rich

Sorry it’s taken me so long to get back to you. Your latest idea is great. It’s taken me a couple of days of troubleshooting to get the System started rule to work (grpList doesn’t have the members field so you need to call forEach on grpList itself) but now I’m able to retrieve a group by name :smile:

I need to work on my VoiceCommand rule but once I’m happy with it I’ll post an update in my other thread https://community.openhab.org/t/voice-control-wit-ai-my-progress-setup-tips-advice-needed/2522

1 Like