Amazed about JSR223 and Jython - Hue Groups Rule

I have written some complicated DSL code full of nested lambdas, it was a pain.
Now with fresh and weak knowledge of Python:

I managed to port all my old DSL, which more or less a one to one porting.

But now I created a new project:

Support of Hue Groups (As this is not supported by the binding)

Here how it works:

You define an Item representing the group item in OH:

Dimmer TreppeGang "Treppe und Gang Licht [%d %%]" {hue="group.rule" [group_name="Treppe Gang Gruppe"]}

Metadata has to contain the hue group name, here it is “Treppe Gang Gruppe”, which has to be defined by an external tool.

And here the rule to manage the groups(s):

from core.triggers import ItemStateUpdateTrigger, CronTrigger #, ItemStateUpdateTrigger
from core.jsr223.scope import events
from core.log import logging, LOG_PREFIX
from core.rules import rule
import requests
import yaml, json
from time import sleep
from core import osgi

metadata_registry = osgi.get_service(
        "org.openhab.core.items.MetadataRegistry"
    ) or osgi.get_service(
        "org.eclipse.smarthome.core.items.MetadataRegistry"
    )


HUE_BRIDGE = "vevedock-09"  # add ip address or name of gateway
API_CODE = "72B99A4ECE"     # API-Token     
POLL_INTERVAL = 15          # poll interval for getting values set outside of openhab
    
log = logging.getLogger(LOG_PREFIX + ".huegroups.log") 

class HueGroup(object):
    def __init__(self, group_id, group_data, meta_hash):
        self.group_id = group_id
        self.group_data = group_data
        self.meta_hash = meta_hash
        self.stored_bri = 0

    def setVal(self, value):

        if str(value) != "0":
            self.stored_bri = round(value.floatValue()*255/100)
            self.setValHTTP(True, self.stored_bri)
        else:
            if self.stored_bri > 1:
                self.setValHTTP(False, 1)
                sleep(0.1)
            self.setValHTTP(False, None)

    def setValHTTP(self, on, bri):
        post_data = {}

        if on != None:
            post_data["on"] = on

        if bri != None:
            post_data["bri"] = bri

        if len(post_data) > 0:    
            r =requests.put('http://{}/api/{}/groups/{}/action'.format(HUE_BRIDGE, API_CODE, self.group_id), json.dumps(post_data))


    def getVal(self): 
        self.getValHTTP()

    def getValHTTP(self):
        r =requests.get('http://{}/api/{}/groups/{}/'.format(HUE_BRIDGE, API_CODE, self.group_id))
        response = yaml.load(r.content)
        got_bri = round(response["action"]["bri"]*100/255)
        if response["action"]["on"]:
            if abs(self.stored_bri - got_bri) > 1 :
                self.stored_bri = got_bri
                events.postUpdate(self.meta_hash[self.group_data["name"]], str(got_bri))
        else:
            events.postUpdate(self.meta_hash[self.group_data["name"]], "0")
            self.stored_bri = 0

                                                  
class HueGroupMgr(object):
    def __init__(self):
        self.meta_hash = self.getMetaData()
        self.all_group_data = self.loadGroups()
        self.group_items = { itm: HueGroup(itm, self.all_group_data[itm], self.meta_hash) for itm in self.all_group_data }
  
    def loadGroups(self):
        r =requests.get('http://{}/api/{}/groups'.format(HUE_BRIDGE, API_CODE))
        if r.status_code == 200:            
            return { group : attributes for (group, attributes) in  yaml.load(r.content).items() if attributes['name'] in self.meta_hash}
        else:
            return {}

    def getMetaData(self):
        meta_data = metadata_registry.getAll()
        return { meta.getConfiguration()["group_name"] : str(meta.getUID()).split(":")[1] for meta in  meta_data.toArray() if meta.getValue() == "group.rule"}

    def getAllTriggers(self):
        all_trig = []
        for item in self.all_group_data:
            all_trig.append(ItemStateUpdateTrigger(self.meta_hash[self.all_group_data[item]["name"]], None, str(item)).trigger)

        if len(all_trig) > 0:
            all_trig.append(CronTrigger("0/{} * * * * ?".format(POLL_INTERVAL), "POLL").trigger,)
#            all_trig.append(ItemStateUpdateTrigger("proxySW", None, "POLL").trigger)

        return all_trig

    def execute(self, module, inputs):
        cmd = str(inputs["module"]).split("_")[0] if len(inputs) > 0 else "POLL"
        if cmd == "POLL":
            for group_id in self.group_items:
                self.group_items[group_id].getVal()
        else:
            self.group_items[cmd].setVal(inputs["state"])
     
hueGroupMgr = HueGroupMgr()

@rule("Hue  Group Manager Rule", description="This is an rule for adding groups support for hue groups")
class HueGroupsRule(object):
    def __init__(self):
        self.triggers = hueGroupMgr.getAllTriggers()    

    def execute(self, module, inputs):
        hueGroupMgr.execute(module, inputs)

The work with Visual Studio Code and Jython is amazing.
The code is far away from perfect, feedback what could be done better is appreciated.

The next thing I will add are scenes.

A big “Thanks” to all who have contributed to JSR223 and Jython for the current state.

2 Likes

I’ve pointed this out to you before, but it still surprises me that you choose to write things from scratch rather than using the available helper libraries. For example, this would be much simpler using core.metadata and the core.triggers.when decorator. The use of classes and the helper library extensions instead of the decorators to generate a rule is likely going to scare most people away from scripted automation. For others reading this… it can be done much simpler!

I’m not familiar with Hue groups… why did you chose to use metadata rather than OH groups?

1 Like

These examples are great, but I also didn’t understand why you would write a whole class by hand and not use the decorators :slight_smile: I thought before that maybe my Python knowledge is not that great and it has some benefits which I can’t think of :slight_smile:

The triggers a constructed on the fly, how should I do that with trigger decorations?

People who have do deal with ZigBee lights like hue, tradfri, lightfy,… know why: OH groups handle the light one-by-one. ZigBee group are handled by ZigBee broadcast, which switches all group lights at once and the ZigBee traffic is much lower (duty cycle)

How can I use such a decorator to dynamically build up a list of triggers?
In the example above I use a list returned from metadata and from hue bridge in order to compile a trigger list.
core.metadata does not have a possibility to retrieve all metadata at once, at least I did not see a method. I needed to find out which item has a certain metadata defined, which is not possible with core.metadata (method getAll() is missing)

Yes, I am familiar with this, but I was meaning the use of groups to organize the Items rather than flagging them with metadata. Groups are a nice bucket for holding Items.

I am sure that I have shown you examples in other discussions that we have had in other forum topics. There is also an example in Mode (Time of Day). Basically, if you are using classes to generate rules, you are doing something the hard way.

You mean all of an Item’s metadata at once?

https://openhab-scripters.github.io/openhab-helper-libraries/Python/Core/Packages%20and%20Modules/metadata.html#core.metadata.get_metadata

This is why I was asking about using groups vs metadata, since groups seems to be a lot better way to do this.

For completeness though, you can do this with core.metadata, but your approaching it from the wrong side. To generate a list of all Items with a particular namespace, iterate through all Items in the ItemRegistry (not all metadata in the MetadataRegistry) and search their metadata for the namespace that you are looking for…

items_with_namespace = [item for item in itemRegistry.getAll() if get_metadata(item.name, "Treppe Gang Gruppe") is not None]

It is not a grouping here but just creating a one-to-one relation between an OH item and a HUE group.
The OH item represents a group of lights, though it does not care how many lights are inside the HUE group.
I am implementing scenes as well then the item definition will look like:

Dimmer TreppeGang "Treppe und Gang Licht [%d %%]" {hue="group.rule" [group="Treppe Gang Gruppe"]}
Switch TreppeGangSceneMorning "Treppe und Gang Scene Morgen" {hue="scene.group.rule" [group="Treppe Gang Gruppe", scene="Morgen"]}
Switch TreppeGangceneEvening "Treppe und Gang Scene Abend" {hue="scene.group.rule" [group="Treppe Gang Gruppe", scene="Abend"]}

This problem is not solvable by groups for sure

I prefer decomposition in classes, it is the lack of knowledge that makes me feel that decorations are not the easy way.
What I also need to get is info which particular trigger fired, with class decoration it is easy for me to name the trigger and parse the inputs[“module”] to find out what trigger fired (except cron, which does not deliver anything back)

cmd = str(inputs["module"]).split("_")[0] if len(inputs) > 0 else "POLL"

I will definitely look deeply into you example above

I have a lot of items, so my thinking was to do do it the opposite way. I will rewrite it according to suggestions. Time does not matter as it is done just once at startup.