1. Starting point:
In my house, I have window blinds controllers (actuators) from HomeMatic. These actuators are connected with direct programs to wall-mount switches from Homematic, in order guarantee a function even in the case of Homematic server or Openhab server outages (worst case).
Following functions are implemented by direct programs (device-to-device-communication):
- Full up
- Full down
- One micro-step up
- One micro-step down
The channel of the window blinds actuator has limited capabilities in order to get some better out-of-the-box solution. The channel supplies the following controlling capabilities:
- Start moving up
- Start moving down
- Stop moving
And additionally a feedback channel about the actual position of the window blind. This channel is only updated at the moment the blinds stop moving (finish position).
2. Expectation
My target was to implement the following functionalities independent from HomeMatic direct programming capabilities (and even going further):
- Microstep movement up and down
- Setting the flaps at 45 degrees at the current position or forcing them to be full down.
- Setting the flaps full open either at the current position or forcing them to be full down.
- Implementing automatic shading based on the sun position and season (tbd)
- Detecting open Tersasse doors in order to prevent locking out by automatic blinds positioning (tbd).
3. What has been done so far
I have created a Jython rule which manages the window shades in a way that makes it possible to execute a standard set of commands which can be attached to any type of switch or to a MainUI widget command.
Following standard commands are implemented:
- Move full up
- Move full down
- Move micro-step up
- Move micro-step down
- Open flaps 45 degrees at the current position
- Fully open flaps at the current position
- Move full down and open flaps 45 degrees
- Move full down and fully open flaps
The code sample of windowShadeControlLib.py and windowShadeControl.py are attached below.
I have created a sample MainUi widget. It is my first widget, so don’t judge me on that, it’s more to show how to interact from UI with the rule and pass-over parameter.
The icons, items, and yaml file are attached below.
Reuse and Feedback is appretiated
Item definition
String WSRuleRuleTrigger "Window Shade Rule Trigger Proxy"
Widget definition
uid: wRSController
tags: []
props:
parameters:
- context: name
description: Name of the card
label: Name
name: name
required: false
- context: item
description: Roller Shutter Item Name
label: Roller Shutter
name: rsItem
required: false
parameterGroups: []
timestamp: Oct 27, 2021, 2:52:57 PM
component: f7-card
config:
style:
- width: 450px
- height: 200px
- border-radius: 15px
slots:
default:
- component: f7-card-content
slots:
default:
- component: f7-row
config:
style:
height: 50px
slots:
default:
- component: oh-button
config:
style:
position: absolute
left: 0px
top: 0px
width: 450px
height: 42px
background: "#505050"
- component: Label
config:
text: "=props.name === undefined ? 'SHUTTER CONTROL' : props.name"
style:
position: absolute
left: 12px
top: 3px
fontSize: 25px
fontWeight: 300
color: "#A1A1A1"
- component: f7-row
config:
style:
height: 65px
slots:
default:
- component: f7-col
slots:
default:
- component: oh-icon
config:
icon: "= props.rsItem === undefined ? 'oh:rollershutter' : 'oh:rollershutter-' + items[props.rsItem].state"
height: 80
- component: f7-col
slots:
default:
- component: oh-link
config:
tooltip: All the other tooltip yet to be done
slots:
default:
- component: oh-icon
config:
icon: oh:rsfullup
height: 50
action: command
actionItem: RSRuleRuleTrigger
actionCommand: ="FUP_" + props.rsItem
- component: f7-col
slots:
default:
- component: oh-icon
config:
icon: oh:rsstepup
height: 50
action: command
actionItem: RSRuleRuleTrigger
actionCommand: ="SOP_" + props.rsItem
- component: f7-col
slots:
default:
- component: oh-icon
config:
icon: oh:rs90
height: 50
action: command
actionItem: RSRuleRuleTrigger
actionCommand: ="90DEG_" + props.rsItem
- component: f7-col
slots:
default:
- component: oh-icon
config:
icon: oh:rs90fulldown
height: 50
action: command
actionItem: RSRuleRuleTrigger
actionCommand: ="90DEGFD_" + props.rsItem
- component: f7-row
config:
style:
height: 70px
slots:
default:
- component: f7-col
slots:
default:
- component: oh-icon
config:
height: 50
- component: f7-col
slots:
default:
- component: oh-icon
config:
icon: oh:rsfulldown
height: 50
action: command
actionItem: RSRuleRuleTrigger
actionCommand: ="FDN_" + props.rsItem
- component: f7-col
slots:
default:
- component: oh-icon
config:
icon: oh:rsstepdown
height: 50
action: command
actionItem: RSRuleRuleTrigger
actionCommand: ="SCL_" + props.rsItem
- component: f7-col
slots:
default:
- component: oh-icon
config:
icon: oh:rs45
height: 50
action: command
actionItem: RSRuleRuleTrigger
actionCommand: ="45DEG_" + props.rsItem
- component: f7-col
slots:
default:
- component: oh-icon
config:
icon: oh:rs45fulldown
height: 50
action: command
actionItem: RSRuleRuleTrigger
actionCommand: ="45DEGFD_" + props.rsItem
Window Shade Controller Rule
windowShadeControlLib.py:
from core.triggers import ChannelEventTrigger, ItemStateChangeTrigger, ItemStateUpdateTrigger
from core.log import logging, LOG_PREFIX
from personal.timers import SingleTimer
import yaml
from core.jsr223.scope import events, items, itemRegistry, CLOSED
import inspect
import traceback
log = logging.getLogger(LOG_PREFIX + ".window-blinds-rule.log")
'''
Window Blinds Parameter Abbreviations
ORIN Physical Location Orientation of Window Blinds (0...360 - automatic shading to be implemented)
FUSU Full stroke up time, time to move from lower to top
FUSD Full stroke down time, time to move from top to down
FLOP Flap open time (full close to 90 degree - horizontal)
FLMS Flap microstep (slight move up or down)
45OP Flap 45 degree open time (open to 45 degrees)
FLCL Flap close time (full open to 0 degree - vertical)
AUTO Auto mode, setting according to season and astro
WDCO Window or door contact item
Trigger Type Abreviations
CRON CronTrigger** - fires based on cron expression
ISCT ItemStateChangeTrigger** - fires when the specified Item's state changes
ISUT ItemStateUpdateTrigger** - fires when the specified Item's state is updated
ICOT ItemCommandTrigger** - fires when the specified Item receives a Command
GEVT GenericEventTrigger** - fires when the specified occurs
IEVT ItemEventTrigger** - fires when am Item reports an event (based on ``GenericEventTrigger``)
TEVT ThingEventTrigger** - fires when a Thing reports an event (based on ``GenericEventTrigger``)
TSCT ThingStatusChangeTrigger** - fires when the specified Thing's status changes **(requires S1636, 2.5M2 or newer)**
TSUT ThingStatusUpdateTrigger** - fires when the specified Thing's status is updated **(requires S1636, 2.5M2 or newer)**
CEVT ChannelEventTrigger** - fires when a Channel reports an event
DEVT DirectoryEventTrigger** - fires when a directory's contents changes
IRET ItemRegistryTrigger** - fires when the specified Item registry event occurs
IADT ItemAddedTrigger** - fires when an Item is added (based on ``ItemRegistryTrigger``)
IRET ItemRemovedTrigger** - fires when an Item is removed (based on ``ItemRegistryTrigger``)
IUPT ItemUpdatedTrigger** - fires when an Item is updated (based on ``ItemRegistryTrigger``)
STUT StartupTrigger** - fires when the rule is activated **(implemented in Jython and requires S1566, 2.5M2 or newer)**
'''
def log_trace_back():
frame = inspect.currentframe()
stack_trace = traceback.format_stack(frame)
log.error(''.join(stack_trace))
RS_GLOB_SETTING = '''
{
"ORIN" : 0,
"FUSU" : 50,
"FUSD" : 50,
"FLOP" : 2.1,
"45OP" : 1.1,
"FLCL" : 1.4,
"FLMS" : 0.2,
"AUTO" : 0,
"WDCO" : "None"
}
'''
class wbAttributes(object):
"""
Class for managing the window blinds item attributes:
- item specific attributes
- global attributes
- gui changable attributes
"""
def __init__(self, attr):
"""
initiate item specific and global attributes
"""
self.attr = attr
# set default attr values from LIGHT_DEFAULTS group members
self.global_attr = yaml.safe_load(RS_GLOB_SETTING)
# attributes managed trhough UI
self.ui_default_attr = {}
def getGlobAttr(self, attr_name):
"""
return globaly defined attribute
"""
return int(str(self.global_attr[attr_name]))
def getItemAttr(self, attr_name):
"""
return item specific attribute if exist
else
return the globaly defined attribute
"""
if str(attr_name) in self.attr:
return int(self.attr[attr_name]) if str(self.attr[attr_name]).isdigit() else self.attr[attr_name]
else:
return int(self.global_attr[attr_name]) if str(self.global_attr[attr_name]).isdigit() else self.global_attr[attr_name]
def setItemAttr(self, attr_name, value):
"""
set item specific attribute
else
return the globaly defined attribute
"""
self.attr[attr_name] = value
def loadUIDefaults(self):
"""
load UI defined attributes over global attributes
"""
self.ui_default_attr = {str(item.name).split("_")[2] :item.state for item in itemRegistry.getItem("LIGHT_DEFAULTS").members}
class wbItem(object):
"""
Class for managing the blinds item states
"""
def __init__(self, item_name, attr):
"""
"""
# set basic attributes
self.item_name = item_name
self.item_attrs = wbAttributes(attr)
# load the config parameters for particular blind
self.fsUpTime = self.item_attrs.getItemAttr("FUSU")
self.fsDwnTime = self.item_attrs.getItemAttr("FUSD")
self.flOpen = self.item_attrs.getItemAttr("FLOP")
self.fl45Open = self.item_attrs.getItemAttr("45OP")
self.flClose = self.item_attrs.getItemAttr("FLCL")
self.flMS = self.item_attrs.getItemAttr("FLMS")
# eliminate initial value setting when sending command
self.moving_start_time = None
self.stored_items_state = 0
self.window_state = CLOSED
self.getPosition()
def sendCommandQueue(self, cmd_quue):
"""
Sends a queue of commands to the window blinds actor
"""
cum_tmr = 0
tmr = []
for cmd in cmd_quue:
tmr.append(SingleTimer('SIN_{}_SimTimer'.format(cmd[0]), cum_tmr, lambda cmd = cmd: events.sendCommand(self.item_name, cmd[0])))
cum_tmr = cum_tmr + cmd[1]
def getPosition(self):
"""
Get and store actual position of window blinds
"""
self.stored_items_state = items[self.item_name].floatValue()
def getWindowState(self, itm):
"""
Get open/close state of the assitiated window
"""
self.window_state = items[itm]
def wb45deg(self):
"""
Set flaps to 45 degrees
"""
cmd =[["DOWN", self.flClose], ["STOP",0.1], ["UP", self.fl45Open], ["STOP", 0.1]]
self.sendCommandQueue(cmd)
def wb90deg(self):
"""
set flaps to 90 degrees (horisontally)
"""
cmd =[["DOWN", self.flClose], ["STOP",0.1],["UP", self.flOpen], ["STOP", 0.1]]
self.sendCommandQueue(cmd)
def wb45degFD(self):
"""
Send blinds to bootom and set flaps to 45 degrees
"""
cmd =[["DOWN", self.calcFDTime()], ["STOP",0.1], ["UP", self.fl45Open], ["STOP", 0.1]]
self.sendCommandQueue(cmd)
def wb90degFD(self):
"""
Send blinds to bootom and set flaps to 90 degrees (horisontal)
"""
cmd =[["DOWN", self.calcFDTime()], ["STOP",0.1], ["UP", self.flOpen], ["STOP", 0.1]]
self.sendCommandQueue(cmd)
def calcFDTime(self):
"""
calculate time for blindas to travell full down (from current position)
"""
return self.fsDwnTime * (100 - self.stored_items_state) / 100
def calcFUTime(self):
"""
calculate time for blindas to travell full up (from current position)
"""
return self.fsUpTime * self.stored_items_state / 100
def wbFullUp(self):
"""
Move blinds full up
"""
self.sendCommandQueue([["UP", self.calcFUTime()], ["STOP",0.1]])
def wbFullDown(self):
"""
Move blinds full down
"""
self.sendCommandQueue([["DOWN", self.calcFDTime()], ["STOP",0.1]])
def wbStepOpen(self):
"""
Open binds flaps a microstep
"""
self.sendCommandQueue([["UP", self.flMS], ["STOP",0.1]])
def wbStepClose(self):
"""
Close blinds flaps a microstep
"""
self.sendCommandQueue([["DOWN", self.flMS], ["STOP",0.1]])
def cleanUp(self):
"""
Stop any movement
"""
events.sendCommand(self.item_name, "STOP")
class wbItemManager(object):
"""
Class for managing the window blinds items:
- forwarding the events
- creating the trigger definition for the rule class
- cleanup timers by calling involved classes
"""
def __init__(self, attrJsn):
# initiate light items
all_attr = yaml.safe_load(attrJsn)
all_items = [item for (item, atr) in yaml.safe_load(attrJsn).items() ]
self.wb_items = { itm: wbItem(itm, all_attr[itm][0]) for itm in all_items }
# set trigger values for lights
self.all_trig = { item : attributes[1] for (item, attributes) in yaml.safe_load(attrJsn).items() }
def execute(self, module, inputs):
"""
called when rule executed
"""
# extract the rule action parameters
act = str(inputs["module"]).split("_")[0]
itm = str(inputs["module"]).split("_")[1]
# has the rule been called by a MainUI widget
if act == "UI-TRIG":
# extract the parameters supplied by the UI
value = inputs[inputs["module"]+".state"]
act = str(value).split("_")[0]
itm = str(value).split("_")[1]
if act == "FUP":
self.wb_items[itm].wbFullUp()
elif act == "FDN":
self.wb_items[itm].wbFullDown()
elif act == "SOP":
self.wb_items[itm].wbStepOpen()
elif act == "SCL":
self.wb_items[itm].wbStepClose()
elif act == "45DEG":
self.wb_items[itm].wb45deg()
elif act == "90DEG":
self.wb_items[itm].wb90deg()
elif act == "45DEGFD":
self.wb_items[itm].wb45degFD()
elif act == "90DEGFD":
self.wb_items[itm].wb90degFD()
elif act == "UPO":
self.wb_items[itm].getPosition()
elif act == "WDC":
self.wb_items[itm].getWindowState(itm)
def getAllTriggers(self):
# get triggers from json defintion
raw_trigger = [[trigger[0], trigger[1], trigger[2], trigger[3], key_item_mode+"_"+key_item]for (key_item, item_all_triggers) in self.all_trig.items() for (key_item_mode, item_mode_triggers) in item_all_triggers.items() for trigger in item_mode_triggers]
all_trig = []
for trigger in raw_trigger:
if trigger[0] == "CET":
all_trig.append(ChannelEventTrigger(trigger[1], trigger[2], trigger[4]).trigger)
elif trigger[0] == "ISCT":
all_trig.append(ItemStateChangeTrigger(trigger[1], None, None, trigger[4]).trigger)
else:
log.error("********* {} ********* is not implemented yet".format(trigger[0]))
# add trigger for MainUI widget
all_trig.append(ItemStateUpdateTrigger("WSRuleRuleTrigger", None, "UI-TRIG").trigger)
return all_trig
def cleanUp(self):
"""
call all items cleanup
"""
for itm in self.wb_items.values():
itm.cleanUp()
windowBlindsControl.py:
from core.log import logging, LOG_PREFIX
from core.rules import rule
log = logging.getLogger(LOG_PREFIX + ".window-blinds-rule.log")
import imp
import personal.windowBlindsControlLib
imp.reload(personal.windowBlindsControlLib)
from personal.windowBlindsControlLib import wbItemManager, log_trace_back
'''
Window Blinds Commands Abbreviations
FUP Move up to the end
FDN move down to the end
45DEG set in current position flaps to 45 degree
45DEGFD set at down position flaps to 45 degree
90DEG set in current position flaps to 90 degree
90DEGFD set at down position flaps to 90 degree
SOP open flaps for 15 degrees
SCL close flaps for 15 degrees
UPO position update (internal)
WDC window contact update (interna)
Window Blinds Parameter Abbreviations
ORIN Physical Location Orientation of Window Blinds (0...360 - automatic shading to be implemented)
FUSU Full stroke up time, time to move from lower to top
FUSD Full stroke down time, time to move from top to down
FLOP Flap open time (full close to 90 degree - horizontal)
FLMS Flap microstep (slight move up or down)
45OP Flap 45 degree open time (open to 45 degrees)
FLCL Flap close time (full open to 0 degree - vertical)
AUTO Auto mode, setting according to season and astro
WDCO Window or door contact item
Trigger Type Abreviations
CRON CronTrigger** - fires based on cron expression
ISCT ItemStateChangeTrigger** - fires when the specified Item's state changes
ISUT ItemStateUpdateTrigger** - fires when the specified Item's state is updated
ICOT ItemCommandTrigger** - fires when the specified Item receives a Command
GEVT GenericEventTrigger** - fires when the specified occurs
IEVT ItemEventTrigger** - fires when am Item reports an event (based on ``GenericEventTrigger``)
TEVT ThingEventTrigger** - fires when a Thing reports an event (based on ``GenericEventTrigger``)
TSCT ThingStatusChangeTrigger** - fires when the specified Thing's status changes **(requires S1636, 2.5M2 or newer)**
TSUT ThingStatusUpdateTrigger** - fires when the specified Thing's status is updated **(requires S1636, 2.5M2 or newer)**
CEVT ChannelEventTrigger** - fires when a Channel reports an event
DEVT DirectoryEventTrigger** - fires when a directory's contents changes
IRET ItemRegistryTrigger** - fires when the specified Item registry event occurs
IADT ItemAddedTrigger** - fires when an Item is added (based on ``ItemRegistryTrigger``)
IRET ItemRemovedTrigger** - fires when an Item is removed (based on ``ItemRegistryTrigger``)
IUPT ItemUpdatedTrigger** - fires when an Item is updated (based on ``ItemRegistryTrigger``)
STUT StartupTrigger** - fires when the rule is activated **(implemented in Jython and requires S1566, 2.5M2 or newer)**
'''
RS_ATTRIBUTES='''
{
"GuetsRoomRoSh":
[
{
"ORIN" : 180,
"FUSU" : 53,
"FUSD" : 53,
"AUTO" : 0,
"WDCO" : "studyWindow"
}
,
{
"FUP" : [
["CET", "deconz:switch:00212E00C488:04cf8cdf3c77c3a2010012:buttonevent", "6001", ""]
],
"FDN" : [
["CET", "deconz:switch:00212E00C488:04cf8cdf3c77c3a2010012:buttonevent", "5001", ""]
],
"45DEG" : [
["CET", "deconz:switch:00212E00C488:04cf8cdf3c77c3a2010012:buttonevent", "3002", ""]
],
"90DEG" : [
["CET", "deconz:switch:00212E00C488:04cf8cdf3c77c3a2010012:buttonevent", "4002", ""]
],
"45DEGFD" : [
["CET", "deconz:switch:00212E00C488:04cf8cdf3c77c3a2010012:buttonevent", "3001", ""]
],
"90DEGFD" : [
["CET", "deconz:switch:00212E00C488:04cf8cdf3c77c3a2010012:buttonevent", "4001", ""]
],
"SOP" : [
["CET", "deconz:switch:00212E00C488:04cf8cdf3c77c3a2010012:buttonevent", "6002", ""]
],
"SCL" : [
["CET", "deconz:switch:00212E00C488:04cf8cdf3c77c3a2010012:buttonevent", "5002", ""]
],
"UPO" : [
["ISCT", "StudyRoomRoSh", "", ""]
],
"WDC" : [
["ISCT", "studyWindow", "", ""]
]
}
]
}
'''
try:
wbMgr = wbItemManager(RS_ATTRIBUTES)
except Exception as e:
log_trace_back()
@rule("Window Blinds Manager Rule", description="This is an rule for controlling window blinds")
class wbManagerRule(object):
def __init__(self):
self.triggers = wbMgr.getAllTriggers()
def execute(self, module, inputs):
wbMgr.execute(module, inputs)
def scriptUnloaded():
wbMgr.cleanUp()
rs45.svg
rs45fulldown.svg
rs90.svg
rs90fulldown.svg
rsfulldown.svg
rsfullup.svg
rsstepdown.svg
rsstepup.svg