Controlling Window Shades by Switches and MainUI

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.

image

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