Rolladensteuerung mit OpenHAB
8. August 2020 / Joachim Wilke

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
"""
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
Permanentlink:
https://joachim-wilke.de/blog/2020/08/08/Rolladensteuerung-mit-OpenHAB/