Skip to content

Logik RaffstoreAutomatik

msinn edited this page Jan 15, 2021 · 13 revisions

Ziel

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.

Neue Lösung

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:

Alte Lösung

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.

Konfiguration

Voraussetzungen

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

Items zur Steuerung

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.

Items für mögliche Positionen

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

Coding

####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)