Rolladensteuerung mit OpenHAB

Rolladensteuerung mit OpenHAB
Inhaltsverzeichnis

Mit OpenHAB lassen sich Rollos abhängig vom Sonnenstand steuern und so nur schliessen, wenn die Fensterfläche tatsächlich von der Sonne beschienen wird. In die Berechnung fliessen dabei neben ein paar Konstanten der eigenen Wohnung der aktuelle Sonnenstand mit ein.

Konzept

Übersicht über die verwendeten Variablen und Konstanten:

  • Sonnenstand aus dem OpenHAB Astro Binding
  • Höhe der Fenster bis zum Dachüberstand (WINDOW_HEIGHT)
  • Höhe des Dachüberstands über den Fenstern (DACH_UEBERSTAND)
  • Ausrichtung der Südfront des Gebäudes relativ zur tatsächlichen Südrichtung (AZIMUT_OFFSET), hier hilft ein Kompass oder die Ausrichtung über das Luftbild auf Google Maps zu überprüfen

Durch die Steuerung wird alle 5min der aktuelle Sonnenstand abgefragt und mit den Konstanten die maximale Sonnenhöhe errechnet für jede Fensterfront errechnet. Alle Fenster einer Fensterfront sind dabei in einer Gruppe erfasst (z.B. Rollos_Sued). Kann die Sonne sowohl von der Richtung her als auch von der Höhe her das Fenster bescheinen, wird der Rolladen geschlossen. Offene Fenster werden nicht berücksichtigt (“Aussperrschutz”). Der Link zwischen den Rolladensteuerungs-Items und den Fensterkontakten wird über Metadaten hergestellt.

Komplex wird die Steuerung unter anderem dadurch, dass eine manuelle Änderung der Rolläden durch den Bewohner berücksichtigt werden soll. Ist das Rollo durch die Steuerung geschlossen worden und wird danach manuell geöffnet, soll es nicht wieder automatisch geschlossen werden. Ebenso soll ein bereits morgens manuell geschlossenes Rollo abends nicht durch die Steuerung geöffnet werden, da es ja auch nicht automatisch geschlossen wurde. Die Steuerung protokolliert daher die zuletzt automatisch durchgeführte Aktion sowie deren Zeitpunkt um die korrekten Entscheidungen treffen zu können. Aktionen vom Vortag werden dabei nicht berücksichtigt.

Damit die Rollos im Winter oder bei schlechtem Wetter nicht schliessen, gibt es einen zentralen Schalter Automatischer_Sonnenschutz über den das komplette Regelwerk ativiert bzw. deaktiviert werden kann.

Schematischer Ablauf

Der Regel-Ablauf ist im Detail wie folgt dargestellt:

Winkelberechnungen

Die Berechnungen erfolgen mittels klassischer Dreiecks-Mathematik:

Der Sourcecode

Es kommt das neue Regelwerk von OpenHAB zum Einsatz, siehe Next-Gen Rules Engine. Zusätzlich greife ich auf das OpenHAB Scripters Framework zurück um die Regelerstellung zu erleichtern. Beide müssen im OpenHAB also aktiviert bzw. installiert sein um die folgenden Regeln verwenden zu können.

Die Regel für OpenHAB im Gesamten sieht dann wie folgt aus:

"""
Automatic window blind closing due to sunlight
"""

import math
import time
# pylint: disable=import-error, no-name-in-module
from core.rules import rule
from core.triggers import when
from core.metadata import get_value, get_key_value, set_metadata
# pylint: enable=import-error, no-name-in-module

"""
Global constant parameters that are used for calculations
"""
# set the offset between north side of building vs. compass north (0°)
AZIMUT_OFFSET = 23
# dachueberstand if sun is orthogonal to window
DACH_UEBERSTAND = 86
# upper end of windows from floor in cm
WINDOW_HEIGHT = 200
# minimum sun height
MIN_HEIGHT = 3
# define window groups and azimuths
GROUPS = [
    {"targetAzimuth": 0, "windowGroup": "Rollos_Nord"},
    {"targetAzimuth": 90, "windowGroup": "Rollos_Ost"},
    {"targetAzimuth": 180, "windowGroup": "Rollos_Sued"},
    {"targetAzimuth": 270, "windowGroup": "Rollos_West"},
]
# metadata keys
METADATA_NAMESPACE = "SunWindowCovering"
METADATA_KEY_LASTAUTOACTION = "LastAutoAction"
METADATA_KEY_LASTAUTOACTION_TIMESTAMP = "LastAutoActionTimestamp2"
CONTACT_ITEM_NAMESPACE = "ContactItem"
VENETIAN_CONTROL_ITEM_NAMESPACE = "VenetianControl"


def get_max_height(sun_azimuth, target_azimuth):
    # calculate dachueberstand
    effective_ueberstand = DACH_UEBERSTAND / \
        math.sin(math.radians(90-abs(sun_azimuth-target_azimuth)))
    # calculate max sun height for sun to hit windows
    max_height = math.degrees(math.atan(WINDOW_HEIGHT/effective_ueberstand))
    return max_height


def is_not_today(timestamp):
    day_timestamp = time.strftime("%Y%j", time.localtime(float(timestamp)))
    today_timestamp = time.strftime("%Y%j")
    sun_calc.log.debug("Checking if timestamp {} of day {} is today {}".format(
        str(timestamp), day_timestamp, today_timestamp))
    return day_timestamp < today_timestamp


def process_window_group(group, action):
    for member in ir.getItem(group).members:
        sun_calc.log.debug(
            "|--> Checking group member {}".format(member))
        if action == CLOSED:
            # check for possible reasons NOT to close now
            contact_item = get_value(member.name, CONTACT_ITEM_NAMESPACE)
            if contact_item:
                state = items[contact_item]
                sun_calc.log.debug(
                    "|----> Checking status of connected contact item {} with state {}".format(contact_item, state))
                if state == OPEN:
                    sun_calc.log.debug("|------> Skipping because window contact is open.")
                    continue
        sun_calc.log.debug("|----> Checking target state {} vs. current state {}".format(
            action, items[member.name]))
        last_auto_action = get_key_value(
            member.name, METADATA_NAMESPACE, METADATA_KEY_LASTAUTOACTION) or NULL
        last_auto_action_timestamp = get_key_value(
            member.name, METADATA_NAMESPACE, METADATA_KEY_LASTAUTOACTION_TIMESTAMP) or 0
        # window covering is different to requested target state (action)
        if (items[member.name] >= PercentType(30) and action == OPEN) or \
           (items[member.name] < PercentType(30) and action == CLOSED):
            sun_calc.log.debug("Last auto action was {} on {}.".format(
                last_auto_action, last_auto_action_timestamp))
            if last_auto_action == NULL or \
               (action == CLOSED and is_not_today(last_auto_action_timestamp)) or \
               (last_auto_action == OPEN and items[member.name] < PercentType(30)) or \
               (last_auto_action == CLOSED and items[member.name] >= PercentType(30)):
                sun_calc.log.info(
                    "|--> Running target action {} on {}.".format(action, member))
                if action == OPEN:
                    events.sendCommand(member, UP)
                if action == CLOSED:
                    events.sendCommand(member, DOWN)
                    venetian_control_item = get_value(member.name, VENETIAN_CONTROL_ITEM_NAMESPACE)
                    if venetian_control_item:
                        sun_calc.log.debug("|----> Ventian blind detected. Triggering control item {}.".format(venetian_control_item))
                        time.sleep(1)
                        events.sendCommand(ir.getItem(venetian_control_item), ON)
                set_metadata(member.name, METADATA_NAMESPACE,
                             {METADATA_KEY_LASTAUTOACTION: action, METADATA_KEY_LASTAUTOACTION_TIMESTAMP: time.time()})
            else:
                sun_calc.log.debug("|----> Do not touch window cover.")
        else:
            sun_calc.log.debug("|----> Window cover already in correct position.")
            if action == OPEN and last_auto_action == CLOSED:
                sun_calc.log.debug("|----> Window cover was opened manually, resetting auto down status to OPEN.")
                set_metadata(member.name, METADATA_NAMESPACE,
                             {METADATA_KEY_LASTAUTOACTION: action, METADATA_KEY_LASTAUTOACTION_TIMESTAMP: time.time()})


@rule("Sun window convering", description="Calculates which windows are hit by sunshine and closes them.")
@when("Item Sonne_Hoehe received update")
@when("Member of Fensterkontakte changed")
@when("Item Automatischer_Sonnenschutz changed to ON")
@when("System started")
def sun_calc(event):
    if items["Automatischer_Sonnenschutz"] != ON:
        return
    time.sleep(5)
    sun_height = items["Sonne_Hoehe"].floatValue()
    sun_azimuth = items["Sonne_Azimut"].floatValue()
    sun_calc.log.debug("Sun is at height {:0.1f} and azimuth {:0.1f}. Checking window covering...".format(
        sun_height, sun_azimuth))

    for group in GROUPS:
        target_azimuth = group["targetAzimuth"] + AZIMUT_OFFSET
        window_group = group["windowGroup"]
        sun_calc.log.debug("*** Processing window group {} ***".format(window_group))
        sun_calc.log.debug(
            "|--> Checking target azimuth {} for window group {}".format(target_azimuth, window_group))
        if (sun_azimuth > target_azimuth - 90 and sun_azimuth < target_azimuth + 90):
            sun_calc.log.debug("|----> Sun azimuth fits this window group.")
            max_height = get_max_height(sun_azimuth, target_azimuth)
            if (sun_height > MIN_HEIGHT and sun_height < max_height):
                sun_calc.log.debug(
                    "|----> Current sun height {:0.1f} hits this window group (min={:0.1f}, max={:0.1f}). Window blinds should be CLOSED.".format(sun_height, MIN_HEIGHT, max_height))
                process_window_group(window_group, CLOSED)
            else:
                sun_calc.log.debug(
                    "|----> Sun height {:0.1f} does NOT hit this window group (min={:0.1f}, max={:0.1f}). Window blinds should be OPEN.".format(sun_height, MIN_HEIGHT, max_height))
                process_window_group(window_group, OPEN)
        else:
            sun_calc.log.debug(
                "|----> Sun does not hit this window group (sun azimuth out of scope). Window blinds should be OPEN.")
            process_window_group(window_group, OPEN)

Future Work

Mögliche Ansätze für Verbesserungen und Weiterentwicklungen:

  • Automatische Berücksichtigung des Wetters, z.B. Bewölkung bei der Entscheidungsfindung zum Schliessen / Öffnen der Rolläden
  • Minimaler Höhenwinkel aufgrund von Hindernissen im Gelände berechnen, aktuell wird nur der maximale Höhenwinkel berücksichtigt, der minimale Höhenwinkel ist konstant bei 3 Grad