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.