-
Notifications
You must be signed in to change notification settings - Fork 91
Logik RaffstoreAutomatik
Raffstores sollen flexibel und unabhängig gesteuert werden. Dabei sollen verschiedene Parameter berücksichtigt werden. Zudem soll eine automatische Nachführung des Lamellenwinkels auf Basis des Sonnenstands möglich sein.
Mit einer Logik muss bei einer Steuerung der Raffstores vieles mehrfach berechnet/ermittelt werden. Ein Plugin hat diese Nachteile nicht und bietet zudem noch viele weitere Vorteile. Daher wurde die unten beschriebene Logik mittlerweile zu einem Plugin weiterentwickelt. Details zum Plugin finden sich unter https://github.com/i-am-offline/smarthome.plugin.autoblind
Der Nachfolger bzw. die Weiterentwicklung des autoblind Plugins ist das stateengine Plugin, welches Bestandteil von SmartHomeNG ist.
Die Logik-Lösung soll hier aber weiterhin stehen bleiben:
Zu jedem Raffstore-Item werden in der Item-Konfiguration bestimmte Positionen festgelegt und mit Bedingungen versehen. Eine Logik prüft in regelmäßigen Abständen die Items ab und wählt für jeden Raffstore die erste Position aus, bei der alle Bedingungen erfüllt sind. Diese Position wird angesteuert.
Zunächst einmal wird davon ausgegangen, dass für einen zu steuernden Raffstore bereits Items zur Ansteuerung einer bestimmten Behanghöhe und eines bestimmten Lamellenwinkels vorhanden sind. Diese Items heißen "hoehe" und "lamelle":
####items/*.conf####
[raum]
[[raffstore]]
name = Raffstore
[[[hoehe]]]
type = num
knx_dpt = 5.001
knx_send = 1/1/1
knx_init = 1/1/2
visu_acl = rw
cache = on
[[[lamelle]]]
type = num
knx_dpt = 5.001
knx_send = 1/1/3
knx_init = 1/1/4
visu_acl = rw
cache = on
Zu einem zu steuernden Raffstore wird nun ein Item "RaffstoreAutomatik" ergänzt innerhalb dessen alle Konfigurationen etc. für die Logik vorgenommen werden. ####Erweiterung des Items in items/*.conf####
[[[RaffstoreAutomatik]]]
item_helligkeit = meine.wetterstation.helligkeit
[[[[Aktiv]]]]
type = bool
value = 1
visu_acl = rw
cache = on
[[[[LetztePositionId]]]]
type = str
cache = on
[[[[LetztePositionName]]]]
type = str
visu_acl = r
cache = on
- Attribut "item_helligkeit": Kompletter Item-Pfad des Items, über das der für diesen Raffstore auszuwertende Helligkeitswert ermittelt werden kann.
- Subitem "Aktiv": Item zum Aktivieren bzw. Deaktivieren der Automatik für diesen Raffstore. Solange das Item den Wert 1 hat, wird die Automatik ausgeführt.
- Subitem "LetztePositionId": Item zum Zwischenspeichern der letzten angefahrenen Position für diesen Raffstore. Das Attribut "cache = on" muss hier unbedingt gesetzt sein.
- Subitem "LetztePositionName": Item zum Zwischenspeichern der Bezeichnung der letzten angefahrenen Position für diesen Raffstore. Das Attribut "cache = on" muss hier unbedingt gesetzt sein. Über dieses Item kann z. B. in der Visu die Positionsbezeichnung angezeigt werden.
Nun fehlt noch die Definition der Raffstore-Positionen und der zugehörigen Bedingungen. Hierzu werden beliebig viele Weitere Items unterhalb von "RaffstoreAutomatik" angelegt. Jedes Item definiert eine mögliche Position. Diese Items müssen mindestens die folgenden Attribute haben:
####Minimale Form eines Positions-Items####
[[[[Default]]]]
name = Alles auf Halb # Name für das Positions-Item
position = 50,50 # Raffstore Position
Für das Attribut "position" gibt es zwei mögliche Angaben:
- Über eine Angabe in der Form [%-Höhe],[%-Lamelle] kann eine statische Position forgegeben werden.
- Wird der Wert auto angegeben, so wird die Höhe auf 100% gesetzt, zudem werden die Lamellen senkrecht zur Sonne ausgerichtet.
Grundsätzlich ist die Reihenfolge der Positions-Items relevant, denn die Logik prüft alle Items der Reihe nach durch, die Position aus dem ersten Item, bei dem alle angegebenen Bedingungen erfüllt sind, wird angesteuert.
Folgende Bedingungen können derzeit in den Items angegeben werden:
- min_helligkeit und max_helligkeit: Mindest- und Höchsthelligkeit. Sinnigerweise sollte bei gleichzeitiger Angabe die Mindesthelligkeit kleiner als die Höchsthelligkeit sein.
-
min_zeit und max__zeit: Mindest- und Maximaluhrzeit. Die Zeiten werden dabei jeweils als Wertepaar [Stunde],[Minute] angegeben. Hier darf die Maximaluhrzeit auch kleiner als die Mindestuhrzeit sein.
min_zeit = 17,0 max_zeit = 8,0
schränkt den Zeitraum auf die Zeit zwischen 17 Uhr und 8 Uhr am nächsten Morgen ein. - min_sun_altitude und max_sun_altitude: Mindest- und Maximalhöhe der Sonne. Auch hier sollte der Mindestwert kleiner als der Maximalwert sein.
-
min_sun_azimut und max_sun_azimut: Mindest und Maximalrichtung der Sonne. Hier sind wieder Maximalwert möglich, die kleiner als der Minimalwert sind.
min_sun_azimut = 270 max_sun_azimut = 90
wäre damit der (beschattungstechnisch auf der nördlichen Erdhalbkugel nicht unbedingt sinnvolle) Bereich von Westen über Norden nach Osten
Um ein zu häufiges Wechseln der Positionen z. B. beim Wechsel zwischen Sonne und Wolken zu verhindern, können zusätzlich Bedingungen festgelegt werden, die erfüllt sein müssen, damit eine einmal angesteuerte Position wieder verlassen wird. Hier können prinzipiell die gleichen Attribute wie zuvor erläutert verwendet werden. Den Attributen wird jeweils ein "leave_" vorangestellt.
Die Bedingung leave_max_helligkeit = 30000
sorgt zum Beispiel dafür, dass eine Position erst dann verlassen werden kann, wenn der Helligkeitswert unter 30000 sinkt.
Genug der langen Worte: Hier ein Beispiel:
[[[[Nacht]]]]
name = Nacht
max_helligkeit = 200
min_zeit = 17,0
max_zeit = 8,0
position = 100,0
[[[[DaemmerungMorgens]]]]
name = Dämmerung Morgens
min_helligkeit = 200
max_helligkeit = 500
min_zeit = 0,0
max_zeit = 12,0
position = 100,25
[[[[DaemmerungAbends]]]]
name = Dämmerung Abends
min_helligkeit = 200
max_helligkeit = 500
min_zeit = 12,0
max_zeit = 24,0
position = 100,75
[[[[TagNachfuehren]]]]
name = Tag (nachführen)
min_helligkeit = 45000
leave_max_helligkeit = 30000
min_sun_altitude = 20
min_sun_azimut = 170
max_sun_azimut = 270
position = auto
[[[[TagStatisch]]]]
name = Tag (statisch)
min_zeit = 6,0
max_zeit = 22,0
position = 0,100
- Positions-Item Nacht:
- Helligkeit: Höchstens 200
- Zeitraum: Zwischen 17:00 Uhr Abends und 8:00 Uhr Morgens
- Position: Ganz herunter, ganz geschlossen
- Positions-Item DämmerungMorgens:
- Helligkeit: Zwischen 200 und 500
- Zeitraum: Zwischen 00:00 Uhr und 12:00 Uhr
- Position: Ganz herunter, Lamellen auf ca. 45° gekippt
- Positions-Item DämmerungAbends:
- Helligkeit: Zwischen 200 und 500
- Zeitraum: Zwischen 12:00 Uhr und 24:00 Uhr
- Position: Ganz herunter, Lamellen auf ca. 135° gekippt
- Positions-Item TagNachfuehren:
- Helligkeit: Mindestens 45000
- Position erst verlassen, wenn die Helligkeit 30000 wieder unterschreitet
- Sonnenhöhe: Mindestens 20°
- Sonnenrichtung: zwischen 170° und 220°
- Position: Automatisch Nachführen (Ganz herunter, Lamellen senkrecht zur Sonne)
- Positions-Item TagStatisch:
- Zeitraum: Zwischen 06:00 Uhr und 22:00 Uhr
- Position: Ganz oben
####logics/raffstoreautomatik.py####
# Raffstore Automatik V2
#
# ThEr081014 Initiale fertigstellung
# ThEr141014 Aktivierung über Subitem "Aktiv" anstatt über Attribut "aktiv"
# ThEr141014 Item für letzte Position auch unterhalb von "RaffstoreAutomatik"
# ThEr141014 Zusätzliches Item für den Namen der letzten Position
#
class RaffstoreAutomatik:
# Konstruktor
def __init__(self, sh):
import math
logger.info("Initialisiere Raffstore-Automatik")
# Daten übernehmen
self.sh = sh
self.item = None
# Zeit ermitteln
now = time.localtime()
self.akt_zeit = [now.tm_hour,now.tm_min]
# Position der Sonne ermitteln und in Dezimalgrad umrechnen
azimut, altitude = self.sh.sun.pos()
self.sun_azimut = math.degrees(float(azimut))
self.sun_altitude = math.degrees(float(altitude))
# Automatik für alle Items durchführen, die ein Subitem "RaffstoreAutomatik" mit Subitem "Aktiv = 1" haben
items = sh.match_items('*.RaffstoreAutomatik.Aktiv')
for item in items:
if (item() == 1):
self.__run(item.return_parent())
# Führt die Automatik für ein Raffstore-Item durch
def __run(self, item):
logger.info("Starte Raffstore-Automatik mit Item {0}".format(item.id()))
# Daten übernehmen
self.item = item.return_parent()
self.config = item.conf
# Items holen
self.item_letzte_position_id = self.__get_child_item(item,"LetztePositionId")
self.item_letzte_position_name = self.__get_child_item(item,"LetztePositionName")
self.item_helligkeit = self.sh.return_item(self.config["item_helligkeit"])
if self.item_helligkeit == None:
raise AttributeError("Das für 'item_helligkeit' angegebene Item '%s' ist unbekannt." %(self.config['item_helligkeit']))
self.items_position = self.sh.find_children(self.item, "position")
# Relevante Helligkeit ermitteln
self.helligkeit = self.item_helligkeit()
# Bisherige Position ermitteln
old_pos_item_id = self.item_letzte_position_id()
old_pos_item = self.sh.return_item(old_pos_item_id)
if old_pos_item != None and not self.__check_leave_pos_item(old_pos_item):
logger.info("Position kann nicht verlassen werden")
new_pos_item = old_pos_item
else:
# Passende Position heraussuchen
new_pos_item = self.__find_pos_item()
if new_pos_item == None:
logger.info("Keine passende Position gefunden!")
return
# Position im Item "Modus" speichern
new_pos_item_id = new_pos_item.id()
new_pos_item_name = new_pos_item._name
self.item_letzte_position_id(new_pos_item_id)
self.item_letzte_position_name(new_pos_item_name)
logger.info("Neue Position: '{0}' ({1})".format(new_pos_item_name,new_pos_item_id))
# Raffstoreposition aus dem Positions-Item ermitteln
position = self.__get_position_from_pos_item(new_pos_item)
# Raffstoreposition anfahren
if position == None: return
logger.info("Fahre auf Höhe {0}%, Lamelle {1}%".format(position[0],position[1]))
#Items für Raffstoresteuerung holen
item_hoehe = self.__get_child_item(self.item,"hoehe")
item_lamelle = self.__get_child_item(self.item,"lamelle")
# Fahrbefehl für Höhe nur senden, wenn wir um mindestens 10% verändern
hoehe_delta = item_hoehe() - position[0]
if (abs(hoehe_delta) > 10):
item_hoehe(position[0])
# Fahrbefehl für Lamelle nur, wenn der Raffstore um mindestens 10% herabgelassen ist
if (position[0] > 10):
item_lamelle(position[1])
else:
# Ansonsten auf 100% (Nomaler Stand beim anheben)
item_lamelle(100)
# Liest die Positionsinformationen aus einem Item und gibt Sie im Format "Liste [%Höhe,%Lamelle]" zurück
def __get_position_from_pos_item(self, item):
if not 'position' in item.conf:
id = item.id()
logger.error("Das Item '{0}' enthält kein Attribut 'position'".format(id))
return None
value = item.conf['position']
if value == 'auto':
return self.__get_position_from_sun()
value_parts = value.split(",")
if len(value_parts) != 2:
id = item.id()
logger.error("Das Konfigurations-Attribut '{0}' im Item '{1}' muss im Format '###, ###' angegeben werden.".format(attribute, id))
return None
else:
try:
hoehe = int(value_parts[0])
lamelle = int(value_parts[1])
return [hoehe,lamelle]
except ValueError:
id = item.id()
logger.error("Das Konfigurations-Attribut '{0}' im Item '{1}' muss im Format '###, ###' angegeben werden.".format(attribute, id))
return None
# Liefert eine Positionsangabe für den Raffstore basierend auf dem Sonnenstand
# Zur Nachführung wird der Raffstore ganz heruntergefahren und versucht,
# den Lamellenwinkel senkrecht zur Sonne zu stellen.
def __get_position_from_sun(self):
logger.info("Sonnenposition: Azimut {0} Altitude {1}".format(self.sun_azimut,self.sun_altitude))
# Raffstore senkrecht zur Sonne stellen
winkel = 90-self.sun_altitude
logger.info("Winkel auf {0}°".format(winkel))
# Umrechnen auf Wert (90° = 0%, 0° = 50%, -90° = 100%)
prozent = 50-winkel/90*50
logger.info("Lamelle auf {0}%".format(prozent))
return [100,prozent]
# Sucht ein bestimmtes Item unterhalb eines gegebenen Items
# Wenn das Item gefunden wird, wird es zurückgegeben
# Wird das Item nicht gefunden, wird ein AttributeError geworfen
def __get_child_item(self, item, child_id):
search_id = item.id()+"."+child_id
for child in item.return_children():
if child.id() == search_id:
return child
itemId = self.item.id()
raise AttributeError("Unterhalb des Items '%s' fehlt ein Item '%s'" %(itemId, child_id))
# Loopt durch alle Positionen und liefert die erste Position zurück, bei der alle Bedingungen erfüllt sind
def __find_pos_item(self):
logger.info("Suche Item für Zeit = {0}, Helligkeit = {1}".format(self.akt_zeit, self.helligkeit))
for item in self.items_position:
if self.__check_enter_pos_item(item):
return item
return None
# Prüft, ob die in einem Positions-Item erfassten Leave-Bedingungen erfüllt sind, so dass die Position wieder verlassen werden darf
# position: Positions-Item mit den Bedingungen als Attribute
# Rückgabe: TRUE: Position darf verlassen werden, FALSE: Position darf nicht verlassen werden
def __check_leave_pos_item(self, position):
id = position.id()
logger.info("Prüfe ob Position '{0}' verlassen werden darf".format(id))
# Helligkeitsbedingung
if 'leave_min_helligkeit' in position.conf and self.helligkeit < int(position.conf['leave_min_helligkeit']):
logger.info(" -> zu dunkel")
return False;
if 'leave_max_helligkeit' in position.conf and self.helligkeit > int(position.conf['leave_max_helligkeit']):
logger.info(" -> zu hell")
return False;
# Zeitbedingung
if 'leave_min_zeit' in position.conf or 'leave_max_zeit' in position.conf:
min_zeit = self.__get_time_attribute(position,"leave_min_zeit",[0,0])
max_zeit = self.__get_time_attribute(position, "leave_max_zeit", [24,00])
if self.__compare_time(min_zeit, max_zeit) != 1:
# min </= max: Normaler Vergleich
if self.__compare_time(self.akt_zeit, min_zeit) == -1 or self.__compare_time(self.akt_zeit, max_zeit) == 1:
logger.info(" -> außerhalb der Zeit (1)")
return False
else:
# min > max: Invertieren
if self.__compare_time(self.akt_zeit, min_zeit) == 1 and self.__compare_time(self.akt_zeit, min_zeit) == -1:
logger.info(" -> außerhalb der Zeit (2)")
return False
# Sonnenhöhe
if 'leave_min_sun_altitude' in position.conf and self.sun_altitude < int(position.conf['leave_min_sun_altitude']):
logger.info(" -> Sonne zu niedrig")
return False
if 'leave_max_sun_altitude' in position.conf and self.sun_altitude > int(position.conf['leave_max_sun_altitude']):
logger.info(" -> Sonne zu hoch")
return False
# Sonnenrichtung
if 'leave_min_sun_azimut' in position.conf or 'leave_max_sun_azimut' in position.conf:
min_azimut = 0
max_azimut = 90
if 'leave_min_sun_azimut' in position.conf:
min_azimut = int(position.conf['leave_min_sun_azimut'])
if 'leave_max_sun_azimut' in position.conf:
max_azimut = int(position.conf['leave_max_sun_azimut'])
if min_azimut < max_azimut:
if self.sun_azimut < min_azimut or self.sun_azimut > max_azimut:
logger.info(" -> außerhalb der Sonnenrichtung (1)")
return False;
else:
if self.sun_azimut > min_azimut and self.sun_azimut < max_azimut:
logger.info(" -> außerhalb der Sonnenrichtung (2)")
return False;
# Alle Bedingungen erfüllt
logger.info(" -> passt".format(position.id()));
return True
# Prüft, ob die in einem Positions-Item erfassten Bedingungen erfüllt sind, so dass die Position geeignet ist
# position: Positions-Item mit den Bedingungen als Attribute
# Rückgabe: TRUE: Position ist geeignet, FALSE: Position ist nicht geeignet
def __check_enter_pos_item(self, position):
id = position.id()
logger.info("Prüfe ob Position '{0}' geeignet ist ".format(id))
# Helligkeitsbedingung
if 'min_helligkeit' in position.conf and self.helligkeit < int(position.conf['min_helligkeit']):
logger.info(" -> zu dunkel")
return False;
if 'max_helligkeit' in position.conf and self.helligkeit > int(position.conf['max_helligkeit']):
logger.info(" -> zu hell")
return False;
# Zeitbedingung
if 'min_zeit' in position.conf or 'max_zeit' in position.conf:
min_zeit = self.__get_time_attribute(position,"min_zeit",[0,0])
max_zeit = self.__get_time_attribute(position, "max_zeit", [24,00])
if self.__compare_time(min_zeit, max_zeit) != 1:
# min </= max: Normaler Vergleich
if self.__compare_time(self.akt_zeit, min_zeit) == -1 or self.__compare_time(self.akt_zeit, max_zeit) == 1:
logger.info(" -> außerhalb der Zeit (1)")
return False
else:
# min > max: Invertieren
if self.__compare_time(self.akt_zeit, min_zeit) == 1 and self.__compare_time(self.akt_zeit, min_zeit) == -1:
logger.info(" -> außerhalb der Zeit (2)")
return False
# Sonnenhöhe
if 'min_sun_altitude' in position.conf and self.sun_altitude < int(position.conf['min_sun_altitude']):
logger.info(" -> Sonne zu niedrig")
return False
if 'max_sun_altitude' in position.conf and self.sun_altitude > int(position.conf['max_sun_altitude']):
logger.info(" -> Sonne zu hoch")
return False
# Sonnenrichtung
if 'min_sun_azimut' in position.conf or 'max_sun_azimut' in position.conf:
min_azimut = 0
max_azimut = 90
if 'min_sun_azimut' in position.conf:
min_azimut = int(position.conf['min_sun_azimut'])
if 'max_sun_azimut' in position.conf:
max_azimut = int(position.conf['max_sun_azimut'])
if min_azimut < max_azimut:
if self.sun_azimut < min_azimut or self.sun_azimut > max_azimut:
logger.info(" -> außerhalb der Sonnenrichtung (1)")
return False;
else:
if self.sun_azimut > min_azimut and self.sun_azimut < max_azimut:
logger.info(" -> außerhalb der Sonnenrichtung (2)")
return False;
# Alle Bedingungen erfüllt
logger.info(" -> passt".format(position.id()));
return True
# Ermittelt und prüft ein Zeit-Attribut und liefert es im Format "Liste [Stunde, Minute]" zurück
def __get_time_attribute(self, item, attribute, default):
if not attribute in item.conf: return default
value = item.conf[attribute]
value_parts = value.split(",")
if len(value_parts) != 2:
id = item.id()
logger.error("Das Konfigurations-Attribut '{0}' im Item '{1}' muss im Format '###, ###' angegeben werden.".format(attribute, id))
else:
try:
stunde = int(value_parts[0])
minute = int(value_parts[1])
return [stunde,minute]
except ValueError:
id = item.id()
logger.error("Das Konfigurations-Attribut '{0}' im Item '{1}' muss im Format '###, ###' angegeben werden.".format(attribute, id))
return default
# Vergleicht zwei Zeitwerte (als Liste [Stunde, Minute])
# -1: Zeit1 < Zeit2
# 0: Zeit1 = Zeit2
# 1: Zeit 1 > Zeit 2
def __compare_time(self, zeit1, zeit2):
if zeit1[0] < zeit2[0]:
return -1
elif zeit1[0] > zeit2[0]:
return 1
else:
if zeit1[1] < zeit2[1]:
return -1
elif zeit1[1] > zeit2[1]:
return 1
else:
return 0
# Raffstore-Automatik aufrufen (Klasse instanziieren, den Rest macht der Konstruktor ...)
RaffstoreAutomatik(sh)
Die aktuellen Release Notes und die Release Notes der zurückliegenden Versionen sind in der Dokumentation im Abschnitt Release Notes zu finden.