Skip to content

Logik RaffstoreAutomatik

i-am-offline edited this page Oct 14, 2014 · 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.

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": [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

Coding

`# Raffstore Automatik V2

ThEr081014 Initiale fertigstellung

ThEr141014 Aktivierung über Subitem "Aktiv" anstatt über Attribut "aktiv"

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_position = self.__get_child_item(self.item,"AutomatikPosition")
	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_position()
	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_position(new_pos_item_id)		
		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)`