Support for Hue groups and scenes through NGRE

The Story:
I have a zoo of actors and sensors for Illumination at home. Lamps controlled through HoemMatic actors, Lightify and Tradfri ones. The lights are a mix of dimming, switched (ON/OFF), white temperature and color ones. Sensors (switches) from HomeMatic traditional and IP with motion detection and without. Motion sensors have an environment Illumination sensor included.

Some time ago I moved some time ago from preparatory ZigBee gateways to deConz with RaspBee. So now all my ZigBee lights are connected to one gateway/bridge.

I was disappointed that the Hue binding does not support groups and scenes (yet). So I played around with the amazing NGRE and now I want to present the result.

Constructive critics are welcome.

Prerequisites:

I know there are better solutions but for fun, I am using a module which is not part of the standard distribution, request.

I am installing it with this pip instruction:

sudo pip install --upgrade --target=/datavol/openhab-2.5.1/openhab_conf/automation/lib/python requests

I have mounted my docker container conf directory to /datavol/openhab-2.5.1, other installations need adaptations.

Items Metadata Definition

For my implementation, I decided to use metadata in order to logically connect an Openhab item to a Hue group or scene. Openhab group item can be of type Dimmer or Switch, the Openhab scene item is type Switch type only.

Her an example items definition:

Dimmer TestGroup            "Test Hue Group [%d %%]" {hue="group.rule" [group="Test Gruppe", ct_item="TestGroupColorTemp"]}
Dimmer TestGroupColorTemp   "Test Hue Group Clor Temperature [%d %%]" 
Switch TestGroupSW          "Test Hue Group Switch" {hue="group.rule" [group="Test Gruppe"]}
Switch TestGroupSceneMorning "Test Hue Group Scene Morning" {hue="group.rule" [group="Test Gruppe", scene="Morning"]}
Switch TestGroupSceneEvening "Test Hue Group Scene Evening" {hue="group.rule" [group="Test Gruppe", scene="Evening"]}
Switch TestGroupSceneMix     "Test Hue Group Scene Evening" {hue="group.rule" [group="Test Gruppe", scene="Middle OFF"]}

hue = Namespace
group.rule = Value
group = Hue Group Name
ct_item = name of OH item controlling the color temperature
scene = Hue Scene name (needs group definition, both group and scene do not support unicode chars)

Python Rule Definition

Classes:
HueGroupsRule -1- member -1-> HueGroupMgr -1-member-n-> HueGroup

I had some discussion about why not using decorations.
Simple answer:
I do not have a clue how I would manage this with decorations and dynamically generated triggers based on item metadata and hue groups (a mixture of ignorance and lack of knowledge)


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 json
from core.metadata import get_metadata
    
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):
    """
    Class for managing hue groups or scenes
    """
    def __init__(self, group_data):
        self.group_data = group_data
        self.group_id = self.group_data["hue_group_id"]
        self.is_switch_type = PercentType not in itemRegistry.getItem(self.group_data["oh_item"]).acceptedCommandTypes
        self.stored_bri = 0
        self.stored_ct = 500

    def setVal(self, value):
        """
        setting value of goup, e.g. brightness or ON/OFF
        """

        if self.is_switch_type:
            self.postToBridge(True if value == ON else False, None)
        else:
            # PercentType may receive also ON/OFF 
            if value == ON:
                self.stored_bri = 1 if self.stored_bri == 0 else self.stored_bri
            elif value == OFF:
                self.stored_bri = 0
            else: 
                self.stored_bri = round(value.floatValue()*255/100)

            if self.stored_bri != 0:
                self.postToBridge(True, None if value == ON else self.stored_bri)
            else:
                self.postToBridge(False, None)

    def setScene(self, value):
        """
        setting scene ON/OFF
        """
        if value == ON:
            requests.put('http://{}/api/{}/groups/{}/scenes/{}/recall'.format(HUE_BRIDGE, API_CODE, self.group_id, self.group_data["scene_id"]))
        else:
            self.setVal(OFF)

    def setColorTemperature(self, value):
        """
        set color temperature of group
            value: (0...100, 0=cold, 100=warm)
        """
        post_data = {}
        self.stored_ct = 153 + (value.intValue()*347/100)

        post_data["colormode"] = "ct"
        post_data["ct"] = self.stored_ct

        if len(post_data) > 0:    
            requests.put('http://{}/api/{}/groups/{}/action'.format(HUE_BRIDGE, API_CODE, self.group_id), json.dumps(post_data))
 
    def postToBridge(self, on, bri):
        """
        post data to the hue bridge
            on : True/False/None 
            bri: (0...255)
        """
        post_data = {}

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

        if bri != None:
            post_data["bri"] = bri
            post_data["transitiontime"] = 0
            post_data["colormode"] = "ct"
            post_data["ct"] = self.stored_ct
            

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

    def getFromBridge(self, response):
        """
        get values from bridge (in case the values wre changed by other means)
            response: json response of the bridge containing all hue groups
        """
        if "scene_id" not in self.group_data:
            got_bri = round(response[self.group_id]["action"]["bri"]*100/255)
            is_on = response[self.group_id]["action"]["on"]
            item_state = ir.getItem(self.group_data["oh_item"]).state
            post_data = "0"
            
            if is_on:
                self.stored_bri = got_bri
                post_data = got_bri if not self.is_switch_type else ON
            else:
                post_data = 0 if not self.is_switch_type else OFF
                self.stored_bri = 0

            if str(post_data) != str(item_state):
                events.postUpdate(self.group_data["oh_item"], str(post_data))
                                                  
class HueGroupMgr(object):
    """
    class for managing of all groups and scenes
    """
    def __init__(self):
        self.all_group_meta = self.getMetaData()
        self.all_group_data = self.loadGroupData()
        self.group_items = { group_data["index"]: HueGroup(group_data) for group_data in self.all_group_data }
        self.getFromBridge()

  
    def loadGroupData(self): 
        """
        load all group data from hue bridge and connect to existin
        items with corresponding meta data
        return: map with all groups configurations with and index key
        """
        group_data = []
        index = 100
        try:
            r =requests.get('http://{}/api/{}/groups'.format(HUE_BRIDGE, API_CODE))
            if r.status_code == 200:  
                all_attributes = json.loads(r.content).values()
                for (key, meta) in self.all_group_meta.items():
                    for attributes in  all_attributes:
                        if meta["group"] == attributes["name"]:
                            if "scene" in meta:
                                for scene in  attributes["scenes"]:
                                    if scene["name"] == meta["scene"]:
                                        index += 1
                                        group_data.append({"index" : str(index), "hue_group_id" : attributes["id"], "hue_group_name" : attributes["name"], "oh_item" : key, "scene_id" : scene["id"], "scene_name" : scene["name"]})
                            else:
                                index += 1
                                if "ct_item" in meta:
                                    group_data.append({"index" : str(index), "hue_group_id":attributes["id"], "hue_group_name" : attributes["name"], "oh_item" : key, "ct_item" : meta["ct_item"]}) 
                                else:
                                    group_data.append({"index" : str(index), "hue_group_id":attributes["id"], "hue_group_name" : attributes["name"], "oh_item" : key}) 
        except:
            return group_data 
        
        return group_data

    

    def getMetaData(self):
        """
        get all items and return meta data map with name space "hue" and value "group.rule"
        return: map with all items metadata
        """
        return {item.name : get_metadata(item.name, "hue").getConfiguration() for item in itemRegistry.getAll() if get_metadata(item.name, "hue") is not None and get_metadata(item.name, "hue").getValue() == "group.rule"}
    
    def getFromBridge(self):
        """
        Call iterative all existing groups to extract the group values
        """
        r =requests.get('http://{}/api/{}/groups'.format(HUE_BRIDGE, API_CODE))
        response = json.loads(r.content)
        for group in self.group_items.values():
            group.getFromBridge(response)

    def getAllTriggers(self):
        """
        compose al triggers for group and scene items
        GRS: Group Csne Items
        GRI: Groups Items
        GRC: Group Color Items
        return: trigger lsit
        """
        all_trig = []
        for item in self.all_group_data:
            trigger_name = None
            if "scene_id" in item:
                all_trig.append(ItemStateUpdateTrigger(item["oh_item"], None, 'GRS:{}'.format(item["index"])).trigger)
            else:
                if "ct_item" in item:
                    all_trig.append(ItemStateUpdateTrigger(item["ct_item"], None, 'GRC:{}'.format(item["index"])).trigger)
                all_trig.append(ItemStateUpdateTrigger(item["oh_item"], None, 'GRI:{}'.format(item["index"])).trigger)

            

            all_trig.append(ItemStateUpdateTrigger(item["oh_item"], None, trigger_name).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):
        """
        handle incomming trigger events and forward to coresponding group members
        """
        cmd = str(inputs["module"]).split("_") if len(inputs) > 0 else ["POLL"]
        if cmd[0] == "POLL":
            self.getFromBridge()
        elif cmd[0] == "GRI":
            self.group_items[cmd[1]].setVal(inputs["state"])
        elif cmd[0] == "GRS":
            self.group_items[cmd[1]].setScene(inputs["state"]) 
        elif cmd[0] == "GRC":
            self.group_items[cmd[1]].setColorTemperature(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 rule is responsive and I made some measurement and on my Pi4/4GB:
The rule starts in less than 1s.
The refresh(15s) takes 150-200ms.

For sure there are some bugs, though I will work on it, as I put it in production already.

1 Like

added support for color temperature

1 Like

This topic was automatically closed 41 days after the last reply. New replies are no longer allowed.