Mit Problemen umgehen - wie ich es mache

Nachdem mein Beitrag zum Thema automatische Lichtsteuerung ganz interessantes Echo erzeugt hatte, setze ich mich mal an ein weiteres Thema, das mich sehr interessiert und dessen Bedeutung für mich über die Jahre stark zugenommen hat. Es geht auch wieder um (leidvolle) Lernerfahrungen - denn lernen tun wir ja häufig insbesondere aus Fehlern.

Fire & Forget
Als ich mit Home Automation anfing war meine sämtliche Aktorik fire and forget. So nennen wir es in der Software salopp, wenn wir irgendeinen Befehl irgendwohin absenden und einfach darauf vertrauen, dass schon alles gut gehen wird. Ich sendete (damals per ELV FS10) halt so meine Schaltbefehle an die Lampen raus, irgendwann periodisch als mir auffiel, dass die Kommandos manchmal nicht ankommen. Das war sozusagen schon eine Stufe über dem klassischen fire and forget, praktisch ein agnostisches Dauerfeuer. Klappte ganz okay.

Auch auf der Sensorseite vertraute ich blind und ohne großartige Überprüfung auf die ermittelten Werte. Ich tat allerdings auch nicht viel mehr, als diese irgendwo anzuzeigen. Die Heizungsanalge zu steuern hatte ich mich damals noch nicht getraut und das war wohl auch besser so.

Über die Jahre wuchs die Komplexität meines Systems mit dem Anspruch, nicht bloß simpelste Logik vorzusehen, die nur funktioniert solange jedes Glied in der Kette intakt ist und das richtige tut. Zu oft kam ich in die Situation, dass irgendwo irgendein Teil versagte, aber ich es erst mitbekam, als es richtig störende Folgen hatte. Klassiker war meine Heizungssteuerung. Zuerst nutzte ich zur Gewinnung der Außentemperatur eine externe API. Dass diese seit mehr als einem Monat keine aktuellen Daten mehr hatte, fiel mir erst auf als es draußen kälter wurde und die Heizung aber nicht richtig warm wurde. Später hatte ich einen Temperaturfühler von EQ3 (HomeMatic oder noch HMS) im Einsatz, aber plötzlich schaltete sich mitten im Winter die Heizung ab - es war Wasser in den Fühler eingedrungen und er meldete eine Außentemperatur von 80°C.

Aus diesen Erfahrungen und der anschließenden, mitunter auch recht zermürbenden Fehlersuche bin ich relativ pingelig geworden, was die Überwachung des korrekten Funktionierens so ziemlich jedes Gerätes in meinem System betrifft. Noch immer ist es bei neuen Geräten oft so, dass ich sie voller Euphorie einbinden will, aber das Abfragen von Störungen (es ist doch nagelneu, was soll schon schiefgehen?) eher nervige Pflicht ist. Trotzdem lohnt es sich, immer einen Blick darauf zu haben, was alles schief laufen könnte. Wir kennen ja Murphy’s Gesetz…

Bei der Hardware fängt es an
Grundsätzlich ist von Vorteil, wenn die eingesetzte Hardware bereits Diagnosefunktionen bzw. Kanäle zur Störungsmeldung besitzt. Ein Gerät, das so freundlich ist uns selbst mitzuteilen wenn es nicht mehr korrekt funktioniert ist großartig, denn dieser Fall erspart uns viel educated guessing anhand von Sensordaten (dazu später mehr). Ideal sind Geräte wie die von HomeMatic, die spezielle Datenpunkte haben, um Störungen unterscheidbar zu melden. Auch meine Heizungsanlage kann relativ detailliert berichten, wenn etwas schief läuft. Andere Bausteine, etwa das Funkinterface zu den CO- und Hitzemeldern, haben einfach nur einen Ausgang „Sammelstörung“ und wenn diese Meldung vorliegt, ist trotzdem einiges an Herumprobieren nötig, um Fehlerort und -ursache zu finden. Aber generell ist es (wie auch bei unseren Mitmenschen) erfreulich, wenn das Gerät mit uns spricht - auch und gerade über Probleme.

Die Verarbeitung von Störungsmeldungen aller verbundenen Geräte ist Pflicht und das mache ich bei wirklich jedem Gerät, das solche Features bietet. Andere Geräte meide ich nach Möglichkeit ganz.

Tote reden nicht
Nicht in jedem Fehlerfall kann uns ein Gerät freundlich über Störungen informieren. Mitunter ist die Störung so gravierend, dass selbst der Teil des Gerätes, der Störungsmeldungen verschicken oder verarbeiten soll, defekt ist. Oder es ist einfach das Kabel defekt, die Spannungsversorgung ausgefallen, oder es gibt einen sonstigen Schaden an der Kommunikationsinfrastruktur.

Daher ist es neben allen anderen Prüfungen und Fehlerbehandlungen immer notwendig, sog. Watchdogs zu installieren, also Mechanismen, die ein Ausbleiben von Statusmeldungen, Sensormesswerten oder ähnlichen „Lebenszeichen“ des Gerätes detektieren.

Watchdogs setze ich bei jedem Gerät ein, das mir regelmäßige „Lebenszeichen“ sendet.

Plausiblitätsprüfung
Bei Geräten, die mit hohen Sicherheitsanforderungen konstruiert wurden (etwa einer Heizung) ist davon auszugehen, dass diese selbsttätig die Funktion ihrer Sensoren und in einigen Fällen auch die Plausiblität der gemeldeten Daten überprüfen. Wenn wir allerdings selbst Sensoren benutzen, um damit etwas zu steuern, dann sollten auch wir uns die Mühe machen, einige Prüfungen anzustellen, bevor wir gemeldeten Daten einfach blind vertrauen. Es ist, wenn schon nicht gefährlich, doch zumindest lästig, wenn wir fehlerhafte Sensordaten erst anhand von merkwürdigen Folgeproblemen bemerken.

Die einfachste Plausiblitätsprüfung sind Ober- und Untergrenzen. Da wir nicht in Sibirien leben ist eine gemeldete Außentemperatur von -25°C vermutlich nicht korrekt, hier im Norden war das Kälteste was ich jemals gemessen hatte -12°C. Und im Sommer sollte es auch nicht 80°C werden, jedenfalls nicht auf einem (noch?) bewohnbaren Planeten wie der Erde. Anhand sinnvoller Grenzwerte kann man also schon Probleme erkennen.

Außerdem wird ein gemessener Wert gewissen Schwankungen unterliegen. Bekommen wir von einer Datenquelle (egal ob eigener Sonsor oder fremde API) stets den exakt gleichen Messwert über längere Zeit, dann ist dies ein Hinweis darauf, dass etwas nicht stimmt.

Wer es ganz genau nimmt, kann noch nach auffälligen Sprüngen von Messwerten suchen, die auf Probleme hinweisen können - das ist allerdings dann nicht mehr ganz trivial und birgt auch ein hohes Risiko für Fehlalarme. Ich mache hiervon keinen Gebrauch und setze bei steuerungskritischen Werten lieber auf Redundanz

Plausiblitätsprüfung mache ich bei allen Messwerten, die ich für Steuerungsaufgaben nutze oder an prominenter Stelle anzeige.

Doppelt und Dreifach
In der Luftfahrt und bei KFZ sind einige Sensordaten so kritisch, dass es nicht ausreicht, einfach nur Fehler zu detektieren und zu melden - würden die Sensoren einfach ersatzlos ausfallen, so würden plötzlich wichtige Steuerungen versagen und es könnte zu gefährlichen Situationen kommen. Das System soll möglich auch bei Ausfall einzelner Sensoren weiter funktionieren (natürlich sollte die Störung immer trotzdem gemeldet und alsbald behoben werden).

Darum sind in den genannten Bereichen viele Sensoren mehrfach vorhanden. Dies bietet neben der Ausfallsicherheit auch die Möglichkeit, die Sensorwerte zu vergleichen, und damit einen weiteren Plausiblitätscheck vorzunehmen.

Angenommen, ich habe drei oder mehr Datenquellen für die aktuelle Außentemperatur. Melden diese dann 23°C, 22.7°C und 12°C, so deutet dies daraufhin, dass letzterer Wert vermutlich fehlerhaft ist, da die anderen beiden Werte relativ nah beieinander liegen. Voraussetzung für so eine Überprüfung ist natürlich, dass die Datenquellen ausreichend unterschiedlich funktionieren (also nicht bspw den HomeMatic-Doppel-Temperatursensor benutzen, um zwei Temperaturwerte zu bekommen, denn im Fehlerfall könnten diese ja beide vom selben Fehler betroffen sein).

Redundanz benutze ich bei kritischen Messwerten in Steuerskripten, soweit es technisch mit vertretbarem Aufwand möglich ist.

Bitte melde dich!
Wenn ich nun also mit viel Fleiß alle denkbaren Probleme, Fehler und Störungen überwache, was tue ich mit diesen Informationen? Es nutzt ja nicht viel, wenn ich diese Dinge irgendwo in ein Log schreibe, aber dann nie in dieses Log hinein sehe. Oder wenn die Werte zwischen uninteressanten Statusmeldungen einfach untergehen.

Ich habe bei meinem System eine tägliche Benachrichtigung per Email, in der alle Meldungen, kritische und eher unkritische zusammengefasst sind. Das ist sehr nützlich und ich lese diese auch wirklich jeden Tag (auch wenn manche Batteriemeldung auch mal ein, zwei Tage lang unbearbeitet bleibt).

Zusätzlich gibt es bestimmte Störungen, über die ich mich sofort informieren lasse. Beispielsweise will ich sofort (und nicht erst am nächsten Morgen mit dem täglichen Report) mitbekommen, wenn die Heizung ausfällt, oder sonstige wichtige Komponenten wie die Verbindung zum LCN oder zu HomeMatic. Ähnlich verhält es sich natürlich mit Alarmmeldungen, aber das ist nochmal ein anderes Thema.

Konsequenz und Selbstheilung
Bei gewissen Problemen ist es logisch, dass man als Konsequenz bestimmte Aktionen auslösen möchte. Besitzt man beispielsweise ein elektrisches Ventil, um die Wasserzufuhr zu unterbrechen, so möchte man bei Hinweisen auf austretendes Wasser dieses vermutlich schließen.

In einigen seltenen Fällen ist es auch bei „Softwareproblemen“ möglich und sinnvoll, automatisch Skripte zur Behebung des Problems auszuführen. Früher hatte ich sogar mal Skripte, die automatisch IPS oder den Rechner neu starten. Das funktionierte aber selten gut und habe ich so nicht mehr in Betrieb.

Was ich aktuell noch in Betrieb habe ist ein Skript zum automatischen Beheben von Verbindungsproblemen über Netzwerk-USB-Hubs. Nicht weil das besonders elegant wäre, sondern weil es leider nötig ist, aber immerhin klappt es, das zu automatisieren. Auch bei den Ebusd-MQTT-Devices für die Heizung habe ich Skripte, die bei Instanzfehlern automatisch diese reinitialisieren.

Solche Methoden sind aber stets als Workarounds zu betrachten, denn sie beheben ja nicht die eigentliche Ursache des Problems, sondern mildern lediglich die Folgen ab.

Profil zeigen
Neben der oben beschriebenen Meldung per „Wartungs-Übersichts-Seite“/Email bin ich dazu übergegangen, bei Problemen, die ein bestimmtes Gerät betreffen, dies auch in der Bedienoberfläche des Gerätes anzuzeigen. Also beispielsweise möchte ich, wenn ein LCN-Busmodul nicht erreichbar ist, gerne beim Bedienfeld für die betroffenen Leuchten, dass dort statt dem üblichen Slider ein Fehlerstatus angezeigt wird. Ähnlich ist es bei den Raumthermostaten - wenn ein HomeMatic-Raumthermostat UNREACH ist, dann will ich dort eine Meldung im WebFront anstatt der üblichen Bedienelemente. Bei Sensoren wiederum will ich eben auch eine Warnung anzeigen, wenn etwas nicht stimmt und zwar an der gleichen Stelle, wo ich sonst den Messwert sehe.

Ich löse dies über Skripte, die dynamisch das Variablenprofil der entsprechenden Variable (Sensorwert oder auch Dimmstufe / Stellwert / Modus) durch ein spezielles „Fehlerprofil“ austauschen können. Letzteres besteht einfach nur aus einer einzigen Association in rot mit einer Warnmeldung als Text. Dadurch erreiche ich einen sehr übersichtlichen und eindeutigen Look im WebFront und habe nicht das Problem dass bei Ausfällen einfach irgendwelche Steuerelemente angezeigt werden, aber nicht mehr reagieren. Ich sehe dort, wo ich normalerweise etwas bedienen würde, dass es nicht funktioniert.

Diese Methode hat sich sehr bewährt und ich wende sie nach und nach auf sämtliche Bedienlemente an, auch wenn das ein nicht unerheblicher Aufwand ist. Aber es wirkt auf mich alles viel „solider“ seitdem ich davon ausgehen kann, dass ein normal aussehendes Bedienelement im WebFront bereits darauf hindeutet, dass das entsprechende Gerät intakt und erreichbar ist.

Fazit
Ich hoffe, dieser Beitrag ist auch ohne konkreten Code nützlich für manche von euch, und sei es nur als Anstoß, eure eigenen Erfahrungen und Gedanken zum Thema mitzuteilen. Es gibt noch Aspekte die ich gar nicht beleuchte, etwa dass man bei Fehlermeldungen in komplexen Systemen manchmal bestimmte Zusammenhänge beachten und danach priorisieren sollte, was man meldet (Beispiel: Ein Switch fällt aus - hier möchte ich vor allen Dingen erkennen können, dass der Switch nicht erreichbar ist, statt mit tausenden Meldungen über Folgeprobleme zugemüllt zu werden). Aber der Post ist ja schon recht lang geworden. :rolleyes:

Hi :slight_smile:
Vielen Dank fuer den sehr detailierten Bericht!
Aber eine Frage:
Wie genau pruefst du die Variablen bzw. Aktoren/Sensoren im IPS?
Ich erwarte jetzt keinen Code oder so aber hast du fuer jede Variable ein Ereignis bei Aenderung dafuer oder
wie genau machst du das?
Wuerde mich ueber eine Antwort freuen, da ich z.B. nur die Battiestatusvariablen zyklisch alle 12h per
script uebepruefe. bei sowas wie Temperatursensoren wuesste ich nicht wie ich das machen soll.

Ich schaue mal, dass ich das Skript teile. Ist allerdings komplex :wink:

Hier ein Skript, welches ich verwende. Das Skript könnt ihr prinzipiell übernehmen, wenn ihr die Konfiguration oben durch eure eigene ersetzt. Bitte habt Verständnis, dass ich für dieses Skript gerade keinen echten Support leisten kann. Ihr dürft natürlich trotzdem Fragen stellen.

<?

/*
	Definitions array: "entry_name" => array( ... definition ... )
	
	Definitons can be either:
	* Single source / value verification or
	* Redundancy - multiple source values to provide redundancy for a target
		variable
	
	For single value definitions, simply specify the "source parameters" directly
	inside the definition array.
	
	For redundancy definitions, all sources must be defined in a separate
	sub-array named "sources", directly under the definition array:
	"entry_name" => array(
		"sources" => array(
			"my_source" => array( ... source parameters ... ),
			"my_other_source" => array( ... source parameters ... ), // and so on
		)
	),
	
	Source parameter fields:
	* "id_value" => required, id of the variable holding the value for this source
	* "max_age" => required, the maximum age of the value from this source before
		it's considered unreliable and not used to calculate the target.
	* "id_status" => optional, id of a status variable to check before regarding
		the id_value variable. If the status variable's value is != what's provided
		in the field "status_ok", this source is regarded as having an error state
		and not used for the target. Note: You can also sepcify a simple array of
		multiple ids here, in which case the same number of check values must be
		provided in the field "status_ok" as a simple array as well.
	* "is_local" => optional, true if this source is local, false by default.
		Used to determine whether the resulting target value uses (factors in) local
		source(s).
	* "multiplier" => optional, if you use sources with different units (such as,
		for example, Pa and hPa for atmospheric pressure), you can specify a multi-
		plier in order to adapt to the target variables' unit
	* "valid_min" => optional, the lowest allowed value for this source. If the
		value of the variable specified by "id_value" is lower, the source is regar-
		ded as erroneous and not used for calculating the target.
	* "valid_max" => optional, the highest allowed value for this source. If the
		value of the variable specified by "id_value" is higher, the source is re-
		garded as erroneous and not used for calculating the target.
	* "use_error_profile" => optional, true if a special variable profile shall
		be used in order to flag the "id_value" variable as erroneous.
	* "smoothing" => optional, if true the average over the last 1h is used to
		calculate the target (rather than the current value). Logging must be ena-
		bled for the id_value variable.
	
	Supported redundancy types:
	* failover - take reading from the topmost valid source
	* avergage - calculate average of readings from valid sources
	* median   - determine median of readings from valid sources
	* none     - use no redundancy (only one source can be provided)
*/

$definitions = array(
	"current_outside_brightness" => array(
		"id_value" => 28899,
		"id_status" => 37436,
		"status_ok" => false,
		"max_age" => 900,
		"use_error_profile" => true
	),
	"soil_humidity" => array(
		"redundancy_type" => "average",
		"sources" => array(
			"irrigated" => array(
				"is_local" => true,
				"id_value" => 31662,
				"id_status" => 34841,
				"status_ok" => true,
				"max_age" => 900,
				"smoothing" => true,
				"use_error_profile" => true
			),
			//"unirrigated" => array(
			//	"is_local" => true,
			//	"id_value" => 20057,
			//	"id_status" => 37626,
			//	"status_ok" => true,
			//	"max_age" => 900,
			//	"smoothing" => true,
			//	"use_error_profile" => true
			//),
			"patio" => array(
				"is_local" => true,
				"id_value" => 27657,
				"id_status" => 23074,
				"status_ok" => true,
				"max_age" => 900,
				"smoothing" => true,
				"use_error_profile" => true
			),
			//"shed" => array(
			//	"is_local" => true,
			//	"id_value" => 45640,
			//	"id_status" => 23371,
			//	"status_ok" => true,
			//	"max_age" => 900,
			//	"smoothing" => true,
			//	"use_error_profile" => true
			//)
		),
		"id_target" => 20261
	),
	"current_outside_pressure" => array(
		"max_deviation" => 10, // maximum deviation of a sources' reading from the average of the other sources' readings (0=disabled)
		"redundancy_type" => "failover",
		"sources" => array(
			"shed_sensor" => array(
				"is_local" => true,
				"id_value" => 36425,
				"multiplier" => 0.01,
				"valid_min" => 850,
				"valid_max" => 1100,
				"id_status" => 14929,
				"status_ok" => true,
				"max_age" => 300,
				"use_error_profile" => true
			),
			"haw" => array(
				"is_local" => false,
				"id_value" => 42155,
				"valid_min" => 850,
				"valid_max" => 1100,
				"max_age" => 1500,
				"use_error_profile" => true
			),
			"owm" => array(
				"is_local" => false,
				"id_value" => 45369,
				"valid_min" => 850,
				"valid_max" => 1100,
				"max_age" => 1500,
				"use_error_profile" => true
			),
		),
		"id_target" => 53134,
	),
	"outside_temp_min" => array(
		"max_deviation" => 0, // maximum deviation of a sources' reading from the average of the other sources' readings (0=disabled)
		"redundancy_type" => "average",
		"decimal_places" => 1,
		"sources"  => array(
			"owm" => array(
				"is_local" => false,
				"id_value" => 31618,
				"valid_min" => -25,
				"valid_max" => 45,
				"max_age" => 1500,
				"use_error_profile" => true
			),
		),
		"id_target" => 16344,
	),
	"outside_temp_max" => array(
		"max_deviation" => 0, // maximum deviation of a sources' reading from the average of the other sources' readings (0=disabled)
		"redundancy_type" => "average",
		"decimal_places" => 1,
		"sources"  => array(
			"owm" => array(
				"is_local" => false,
				"id_value" => 52790,
				"valid_min" => -25,
				"valid_max" => 45,
				"max_age" => 1500,
				"use_error_profile" => true
			),
		),
		"id_target" => 46981,
	),
	"current_outside_temp" => array(
		"max_deviation" => 2.5, // maximum deviation of a sources' reading from the average of the other sources' readings (0=disabled)
		"redundancy_type" => "failover",
		"decimal_places" => 1,
		"sources" => array(
			"shed_station" => array(
				"is_local" => true,
				"id_value" => 59767,
				"valid_min" => -25,
				"valid_max" => 45,
				"id_status" => 29762,
				"status_ok" => true,
				"max_age" => 300,
				"use_error_profile" => true
			),
			"shed_sensor" => array(
				"is_local" => true,
				"id_value" => 46875,
				"valid_min" => -25,
				"valid_max" => 45,
				"id_status" => 48445,
				"status_ok" => true,
				"max_age" => 300,
				"use_error_profile" => true
			),
			"haw" => array(
				"is_local" => false,
				"id_value" => 48966,
				"valid_min" => -25,
				"valid_max" => 45,
				"max_age" => 1500,
				"use_error_profile" => true
			),
			"owm" => array(
				"is_local" => false,
				"id_value" => 45375,
				"valid_min" => -25,
				"valid_max" => 45,
				"max_age" => 1500,
				"use_error_profile" => true
			),
		),
		"id_target" => 17551,
		"id_is_local" => 12055,
		"id_valid" => 14401,
	),
	"current_outside_hum" => array(
		"max_deviation" => 15,
		"redundancy_type" => "average",
		"sources" => array(
			"shed_station" => array(
				"is_local" => true,
				"id_value" => 42515,
				"valid_min" => 0,
				"valid_max" => 98,
				"id_status" => 24130,
				"status_ok" => true,
				"max_age" => 300,
				"use_error_profile" => true
			),
			"haw" => array(
				"is_local" => false,
				"id_value" => 26845,
				"valid_min" => 0,
				"valid_max" => 100,
				"max_age" => 1500,
				"use_error_profile" => true
			),
			"owm" => array(
				"is_local" => false,
				"id_value" => 38580,
				"valid_min" => 0,
				"valid_max" => 100,
				"max_age" => 1500,
				"use_error_profile" => true
			),
		),
		"id_target" => 15451
	)
);

$mailer_id = 28440;
$err_profile_text = "Fehler";
$report = "";

process_entries();

$saved_report_id = @IPS_GetObjectIDByIdent('report', $_IPS['SELF']);
if($saved_report_id === false) {
	$saved_report_id = IPS_CreateVariable(3);
	IPS_SetIdent($saved_report_id, 'report');
	IPS_SetParent($saved_report_id, $_IPS['SELF']);
	IPS_SetName($saved_report_id, 'Report');
}
$saved_report = GetValue($saved_report_id);

if($saved_report != $report) {
	SetValue($saved_report_id, $report);
	if($_IPS['SENDER'] != 'Execute') {
		//send_report();
	}
}

function send_report() {
	global $report, $mailer_id;
	if($report == "") return;
	if($mailer_id == 0) return;
	SMTP_SendMail(
		$mailer_id,
		IPS_GetName($_IPS['SELF']),
		$report
	);
}

function process_entries() {
	global $definitions;
	// loop over all the defined entries
	report_info("About to process " . count($definitions) . " entries.");
	foreach($definitions as $entry_name => $entry) {
		if(!array_key_exists("sources", $entry)) {
			if(array_key_exists("id_value", $entry)) { // assume single unnamed source
				//report_info("Entry \"" . $entry_name . "\" is single-source.");
				$entry["sources"] = array(
					"unnamed" => $entry
				);
			} else {
				report_error("No sources defined for value \"" . $entry_name . "\" (entry \"sources\" missing)!");
				$definitions[$entry_name]["valid"] = false;
				continue;
			}
		}
		$sources = $entry["sources"];
		if($_IPS['SENDER'] == 'Variable') { // if called by update script, process only update
			$this_entry_update = false;
			foreach($sources as $source) {
				if(array_key_exists("id_status", $source)) {
					if(is_array($source["id_status"])) {
						foreach($source["id_status"] as $id) {
							if($id == $_IPS['VARIABLE']) {
								$this_entry_update = true;
								break 2;
							}
						}
					} else if($source['id_status'] == $_IPS['VARIABLE']) {
						$this_entry_update = true;
						break;
					}
				}
				if(array_key_exists("id_value", $source) && $source['id_value'] == $_IPS['VARIABLE']) {
					$this_entry_update = true;
					break;
				}
			}
			if(!$this_entry_update) continue;
		} // if
		
		if(count($sources) == 0) {
			report_error("No sources defined for value \"" . $entry_name . "\"!");
			$definitions[$entry_name]["valid"] = false;
			continue;
		} else if(count($sources) == 1 && array_key_exists("redundancy_type", $entry)
			&& $entry["redundancy_type"] != "none") {
			report_warning("Only single source defined for value \"" . $entry_name .
				"\" - redundancy type \"" . $entry["redundancy_type"] .
				"\" can not be provided!");
		} else if(count($sources) > 1 && array_key_exists("redundancy_type", $entry)
			&& $entry["redundancy_type"] == "none") {
			report_warning("Multiple sources defined for value \"" . $entry_name .
			"\" - which is configured to have no redundancy. Defaulting to redundancy " .
			"type \"failover\".");
		} else {
			report_info("Entry \"" . $entry_name . "\" has " . count($sources) . " defined source(s).");
		}
		
		$valid_sources = array();
		foreach($sources as $source_name => $source) {
			if(array_key_exists("enabled", $source)) {
				report_info("Source \"" . $source_name . "\" for entry \"" .
					$entry_name . "\" is disabled.");
				if(!$source["enabled"]) continue;
			}
			
			if(array_key_exists("id_status", $source)) {
				if(array_key_exists("status_ok", $source)) {
					if(is_array($source["id_status"]) && is_array($source["status_ok"]) &&
						count($source["id_status"]) == count($source["status_ok"])) {
						for($i = 0; $i < count($source["id_status"]); $i++) {
							if(!verify_status($source["id_status"][$i], $source["status_ok"][$i], $source_name, $entry_name)) {
								set_error_profile($source, "err_status");
								continue 2;
							}
						}
					} else if(!is_array($source["id_status"]) && !is_array($source["status_ok"])) {
						if(!verify_status($source["id_status"], $source["status_ok"], $source_name, $entry_name)) {
							set_error_profile($source, "err_status");
							continue;
						}
					}
					report_info("Status check(s) on source \"" . $source_name . "\" for entry \"" .
						$entry_name . "\" successful.");
				} else if($source["id_status"]) {
					report_warning("Source \"" . $source_name . "\" for entry \"" . $entry_name . "\" has field \"id_status\" without corresponding \"status_ok\".");
				}
			} // if
			
			if(!array_key_exists("id_value", $source)) {
				report_warning("Definition \"id_value\" for source \"" . $source_name . "\" for entry \"" . $entry_name . "\" missing.");
				continue;
			} else if(!IPS_VariableExists($source["id_value"])) {
				remove_event_for($source["id_value"]);
				report_warning("Value variable of source \"" . $source_name . "\" for entry \"" . $entry_name . "\" not found.");
				continue;
			}
			ensure_event_exists_for($source["id_value"], 0);
			
			if(array_key_exists("smoothing", $source) && $source["smoothing"]) {
				$ac_id = IPS_GetInstanceListByModuleID("{43192F0B-135B-4CE7-A0A7-1475603F3060}")[0];
				$aggr = AC_GetAggregatedValues(
					$ac_id, $source["id_value"], 0, time() - 60 * 60, time(), 0);
				if(count($aggr) >= 1) {
					$aggr = $aggr[0];
					if(array_key_exists("Avg", $aggr)) {
						$value = $aggr["Avg"];
						report_info("Values of source \"" . $source_name .
							"\" for entry \"" . $entry_name . "\" smoothed using logged history.");
					} else {
						report_warning("Values of source \"" . $source_name .
							"\" for entry \"" . $entry_name . "\" could not be smoothed - no history found.");
						$value = GetValue($source["id_value"]);
					}
				} else {
					report_warning("Values of source \"" . $source_name .
						"\" for entry \"" . $entry_name . "\" could not be smoothed - no history found.");
					$value = GetValue($source["id_value"]);
				}
			} else {
				$value = GetValue($source["id_value"]);
			}
			if(array_key_exists("multiplier", $source)) {
				$value *= $source["multiplier"];
			}
			$updated_ts = IPS_GetVariable($source["id_value"])['VariableUpdated'];
			$age = time() - $updated_ts;
			
			if(array_key_exists("max_age", $source)) {
				if($age > $source["max_age"]) {
					set_error_profile($source, "too_old");
					report_warning("No current reading from source \"" . $source_name . "\" for entry \"" . $entry_name . "\". (Last reading is " . $age . " > " . $source["max_age"] . " seconds old).");
					continue;
				}
			}
			
			if(array_key_exists("valid_min", $source)) {
				if($value < $source["valid_min"]) {
					set_error_profile($source, "too_low");
					report_warning("Reading " . $value . " from source \"" . $source_name . "\" for entry \"" . $entry_name . "\" is too low (" . $value . " < " . $source["valid_min"] . ").");
					continue;
				}
			}
			if(array_key_exists("valid_max", $source)) {
				if($value > $source["valid_max"]) {
					set_error_profile($source, "too_high");
					report_warning("Reading " . $value . " from source \"" . $source_name . "\" for entry \"" . $entry_name . "\" is too high (" . $value . " > " . $source["valid_max"] . ").");
					continue;
				}
			}
			
			report_info("Value retrieved from source \"" . $source_name .
				"\" for entry \"" . $entry_name . "\": " . $value);
			set_error_profile($source, "");
			$source['value'] = $value;
			
			$valid_sources[$source_name] = $source;
		} // foreach $sources
		
		if(!array_key_exists("id_target", $entry)) {
			if(array_key_exists("redundancy_type", $entry) && $entry["redundancy_type"] != "none") {
				report_error("Definition \"id_target\" missing for entry \"" . $entry_name . "\".");
			}
			continue;
		}
		
		if(count($valid_sources) == 0) {
			report_error("Entry \"" . $entry_name . "\" has no usable sources!");
			$definitions[$entry_name]["valid"] = false;
			continue;
		} else {
			report_info("Entry \"" . $entry_name . "\" has " . count($valid_sources) .
				" source(s) providing usable values.");
		}
		
		if(array_key_exists("max_deviation", $entry)) {
			$max_deviation = $entry["max_deviation"];
		} else {
			$max_deviation = false;
		}
		if($max_deviation) {
			if(count($valid_sources) < 3) {
				report_warning("Less than three sources providing readings for entry \"" . $entry_name . "\" - deviation check skipped!");
			} else {
				// remove sources whose readings deviate too much from the (average of the) others.
				$count_reliable = count($valid_sources);
				report_info("About to perform deviation check on " . $count_reliable .
					" value(s) for entry \"". $entry_name . "\".");
				do {
					$remove_source = false;
					$highest_deviation_found = 0;
					$count_deviating_too_much = 0;
					foreach($valid_sources as $source_name => $source) {
						$other_avg = 0;
						$other_count = 0;
						foreach($valid_sources as $other_source_name => $other_source) {
							if($source_name != $other_source_name) {
								$other_avg += $other_source["value"];
								$other_count++;
							}
						}
						$other_avg /= $other_count;
					
						$deviation = abs($source["value"] - $other_avg);
						if($deviation > $max_deviation) {
							if($deviation > $highest_deviation_found) {
								$remove_deviation = $deviation;
								$remove_value = $source["value"];
								$remove_other_avg = $other_avg;
								$remove_source = $source_name;
								$highest_deviation_found = $deviation;
							}
							$count_deviating_too_much++;
						}
					} // foreach $valid_sources
					
					if($remove_source !== false) {
						$count_reliable = count($valid_sources) - $count_deviating_too_much;
						unset($valid_sources[$remove_source]);
						report_warning("Value " . $remove_value . " retrieved from source \"" .
							$remove_source . "\" for entry \"" . $entry_name .
							"\" deviates too much from the average (" . $remove_other_avg .
							") of the other readings (deviation " . $remove_deviation .
							" > " . $max_deviation . ") and will be discarded.");
					}
				} while($remove_source !== false && count($valid_sources) > 2);
				// if we don't have at least two "reliable" readings left, they are
				// either all over the place, or the threshold has been set too low.
				if($count_reliable < 2) {
					report_warning("Deviation check for entry \"" . $entry_name . "\" " .
						"flagged values from all sources, leaving no reliable result. " .
						"Ensure sources are okay and try increasing the maximum allowed " .
						"deviation.");
				} else {
					report_info("Deviation check for entry \"". $entry_name . "\" " .
						"leaves " . $count_reliable . " values to be used for end result.");
				}
			}
		} // if
		
		$sum = 0;
		$sortable = array();
		$count = 0;
		$value = NULL;
		$is_local = false;
		if(!array_key_exists("redundancy_type", $entry)) {
			$entry['redundancy_type'] = "failover";
		}
		foreach($valid_sources as $source_name => $source) {
			switch($entry["redundancy_type"]) {
				case "none":
				case "failover":
					report_info("Redundancy setting \"failover\" for entry \"" .
						$entry_name . "\": Topmost value source (\"" . $source_name .
						"\") is used.");
					if(array_key_exists("is_local", $source)) {
						$is_local = $source["is_local"];
					}
					$value = $source["value"];
					break 2;
				case "average":
					if(array_key_exists("is_local", $source)) {
						if($source["is_local"]) $is_local = true;
					}
					$sum += $source["value"];
					$count++;
					break;
				case "median":
					if(array_key_exists("is_local", $source)) {
						if($source["is_local"]) $is_local = true;
					}
					$sortable[] = $source["value"];
					break;
				default:
					$definitions[$entry_name]["valid"] = false;
					report_error("Redundancy type \"" . $entry["redundancy_type"] .
						"\" for entry \"" . $entry_name . "\" is not supported.");
					continue 2;
			} // switch
		} // foreach $valid_sources
		
		if($count > 0) { // calculate average
			report_info("Redundancy setting \"average\" applied for entry \"" .
				$entry_name . "\".");
			$value = $sum / $count;
		} else if(count($sortable) > 0) { // get median
			report_info("Redundancy setting \"median\" applied for entry \"" .
				$entry_name . "\".");
			$count = count($sortable);
			sort($sortable);
			$mid_index = floor($count / 2);
			if($count % 2) { // odd
				$value = $sortable[$mid_index];
			} else if($count > 0) { // even
				$value = ($sortable[$mid_index] + $sortable[$mid_index + 1]) / 2;
			}
		}
		
		if(array_key_exists("decimal_places", $entry)) {
			$value = round($value, $entry["decimal_places"]);
		}
		
		if(is_null($value)) {
			report_error("No result could be calculated for entry \"" . $entry_name . "\".");
			$definitions[$entry_name]["valid"] = false;
			continue;
		} else {
			report_info("Result for entry \"" . $entry_name . "\" is " . $value . ".");
		}
		
		$target_var_id = $entry["id_target"];
		if(!IPS_VariableExists($target_var_id)) {
			report_error("Target variable for value \"" . $entry_name . "\" not found.");
			continue;
		}
		
		if(SetValue($target_var_id, $value)) {
			$definitions[$entry_name]["valid"] = true;
			$definitions[$entry_name]["is_local"] = $is_local;
			$definitions[$entry_name]["count_sources"] = count($valid_sources);
		} else {
			report_error("Target variable could not be set for value \"" . $entry_name . "\".");
			$definitions[$entry_name]["valid"] = false;
			continue;
		}
	} // foreach $definitions
	
	foreach($definitions as $entry_name => $entry) {
		if(array_key_exists("valid", $entry)) {
			if(array_key_exists("id_valid", $entry)) {
				if(IPS_VariableExists($entry["id_valid"])) {
					SetValue($entry["id_valid"], $entry["valid"]);
				} else {
					report_warning("Validity status variable for value \"" . $entry_name .
						"\" not found.");
				}
			}
		}
		if(array_key_exists("is_local", $entry)) {
			if(array_key_exists("id_is_local", $entry)) {
				if(IPS_VariableExists($entry["id_is_local"])) {
					SetValue($entry["id_is_local"], $entry["is_local"]);
				} else {
					report_warning("Local data availability status variable for value \"" . $entry_name .
						"\" not found.");
				}
			}
		}
		if(array_key_exists("count_sources", $entry)) {
			if(array_key_exists("id_count_sources", $entry)) {
				if(IPS_VariableExists($entry["id_count_sources"])) {
					SetValue($entry["id_count_sources"], $entry["count_sources"]);
				} else {
					report_warning("Valid source count variable for value \"" . $entry_name .
						"\" not found.");
				}
			}
		}
	} // foreach $definitions
} // process_entriess

function ensure_event_exists_for($var_id, $trigger_type) {
	$ident = "var_event_" . $var_id;
	$event_id = @IPS_GetObjectIDByIdent($ident, $_IPS['SELF']);
	if($event_id === false) {
		$event_id = IPS_CreateEvent(0);
		IPS_SetParent($event_id, $_IPS['SELF']);
		IPS_SetIdent($event_id, $ident);
		IPS_SetEventTrigger($event_id, $trigger_type, $var_id);
		IPS_SetEventActive($event_id, true);
	}
} // ensure_event_exists_for

function remove_event_for($var_id) {
	$ident = "var_event_" . $var_id;
	$event_id = @IPS_GetObjectIDByIdent($ident, $_IPS['SELF']);
	if($event_id !== false) {
		IPS_DeleteEvent($event_id);
	}
} // remove_event_for

// verify a sources' status variable
function verify_status($id, $status_ok, $source_name, $entry_name) {
	if($id) {
		if(!IPS_VariableExists($id)) {
			remove_event_for($id);
			report_warning("Status variable with id " . $id . " of source \"" .
				$source_name . "\" for value \"" . $entry_name . "\" not found.");
			return true;
		}
		ensure_event_exists_for($id, 1);
		$status = GetValue($id);
		if($status != $status_ok) {
			report_warning("Source \"" . $source_name . "\" for value \"" .
				$entry_name . "\" has an error status (".
				($status_ok === true ? 'not ' : ($status_ok === false ? '' : $status_ok . "!=")) .
				"\"" . IPS_GetName($id) . "\"" .
				") and will not be used.");
			return false;
		}
	}
	return true;
} // verify_status

function set_error_profile($source, $suffix) {
	if(!array_key_exists("use_error_profile", $source)) return;
	if(!array_key_exists("id_value", $source)) return;
	$prefix = "err_prof_";
	$id = $source["id_value"];
	$var = IPS_GetVariable($id);
	$profile_name = $var["VariableCustomProfile"];
	if(substr($profile_name, 0, strlen($prefix)) == $prefix) {
		$original_profile_name = IPS_GetObject($id)["ObjectInfo"];
	} else {
		IPS_SetInfo($id, $profile_name);
		$original_profile_name = $profile_name;
	}
	if($suffix == "") {
		$profile_name = $original_profile_name;
	} else {
		$var_type = $var["VariableType"];
		ensure_error_profile_exists($var_type, $suffix);
		$profile_name = $prefix . $var_type . "_" . $suffix;
	}
	IPS_SetVariableCustomProfile($id, $profile_name);
} // set_error_profile

// create the specified error profile if necessary
function ensure_error_profile_exists($var_type, $suffix) {
	global $err_profile_text;
	$prefix = "err_prof_";
	switch($suffix) {
		case "too_high": $icon = "HollowDoubleArrowUp"; break;
		case "too_low": $icon = "HollowDoubleArrowDown"; break;
		case "too_old": $icon = "Hourglass"; break;
		case "err_status": $icon = "Warning"; break;
		default: $icon = "Cross";
	}
	$profile_name = $prefix . $var_type . "_" . $suffix;
	if(!IPS_VariableProfileExists($profile_name)) {
		IPS_CreateVariableProfile($profile_name, $var_type);
	}
	IPS_SetVariableProfileAssociation($profile_name, -1000, $err_profile_text,
		$icon, 0xFF0000);
} // ensure_error_profile_exists

function report_error($msg) {
	global $report;
	if($_IPS['SENDER'] == 'Execute') {
		echo "[ERR!] " . $msg . "
";
	} else {
		IPS_LogMessage(IPS_GetName($_IPS['SELF']), $msg);
	}
	$report .= "[ERR!] " . $msg . "
";
} // report_error

function report_warning($msg) {
	global $report;
	$msg = "[WARN] " . $msg . "
";
	if($_IPS['SENDER'] == 'Execute') {
		echo $msg;
	}
	$report .= $msg;
} // report_warning

function report_info($msg) {
	global $report;
	$msg = "[INFO] " . $msg . "
";
	if($_IPS['SENDER'] == 'Execute') {
		echo $msg;
	}
	$report .= $msg;
} // report_info

?>

Sehr sehr gute Idee. Danke!

Das Skript ist wirklich komplex. Das ist nichts, was man mal eben durch einfaches draufgucken innerhalb von 5 Minuten versteht.
Mein Tipp: Ein Skript zyklisch laufen lassen und dabei schauen, wann die Werte aktualisiert wurden. Wenn der Sensor beispielsweise alle 2 Minuten die Werte sendet, könnte man - je nachdem wir kritisch die Werte sind - alle 2:30 Minuten ein Skript laufen lassen, welches prüft, ob der Wert nicht älter als 2 Minuten ist. Die Werte muss man natürlich auf die Gegebenheiten anpassen.

Alternativ kann man auch ein Skript immer auf Aktualisierung triggern und dieses Skript über einen SkriptTimer Alarm schlagen lassen, wenn das Ereignis nicht ausgelöst hat.

Viele Wege führen nach Rom.

Auf jeden Fall ein sehr guter Beitrag von sokkederheld!

Und als Ergänzung für User welche nicht so komplexe Scripte einsetzen wollen; es gibt im Modul Store mindestens zwei Module ( Watchdog und Variablenüberwachung) welche auf ausbleibende Änderungen/Aktualisierungen reagieren können.
Michael

Hallo sokkederheld,
will mich hier für deinen ausgiebigen Text und dein Skript bedanken.
Ist schon öfters mal schade, dass man sich um so vieles selbst kümmern muss.
Eine Plausibilitätsprüfung der Variablenänderungen sowie eine Überwachung sollte eigentlich schon längst in Symcon integriert sein - meine Meinung nach. So einfach wie man eine Variable archivieren kann (toggle) so einfach sollte auch eine Plausibilitätsprüfung und die zeitliche Überwachung aktivierbar sein.

Wunschdenken ist doch erlaubt, oder?

Grüße
Stefan

Ich bin ja sehr glücklich über all die Features, die Symcon hat - insbesondere die Tatsache, dass ich selbst jederzeit fehlende Features ergänzen kann.

Ich verstehe allerdings auch, wenn man an diesem Punkt - Symcon ist ja eine recht stabile und ernstzunehmende Angelegenheit geworden - dann schnell auch solche Features vermisst, die vielleicht für reine „Hobbyisten“ uninteressant sind, aber für einen Einsatz nach professionellen Maßstäben schon wichtig wären. Die muss man sich derzeit oft selbst stricken.

Aber wer weiß, ich habe ja bspw. schon mal als es noch hieß, dass MQTT nicht relevant sei (oder so ähnlich) einen MQTT-Server für Symcon geschrieben und einige Monate später kam das als festes Feature (auch durchaus noch besser integriert und natürlich viel performanter als mein Skript).

Insofern, ja, man darf sich bestimmt was wünschen. Schneller geht es aber oft, wenn man es selbst macht :wink:

Um das ganze wasserdicht zu machen, müsste jeder Aktor von einem Sensor überwacht werden, und im Fehlerfall sofort Meldung geben.
Das wird schon mal teuer im LCN.:smiley:
Also so nicht machbar, bzw richtig teuer,

Aber der Lösungsansatz ist richtig, so müsste es laufen.

Moin sokkederheld,

100% ACK. Mir gefällt dein „zentraler“ Lösungsansatz zur Überwachung. Da werde ich mal was für mich rausziehen, danke dafür! Bisher wird hier doch eher jede kritische Variable einzeln mit eigenem Checkskript überwacht.

Einen weiteren praktischen Tipp habe ich noch: gerade bei kleinen Helferskripten für Routineaufgaben schreibt man gerne nur mal schnell sowas wie
HM_WriteValueBoolean(12345,‚STATE‘,true); oder RequestAction(23456,true);
Das ist eigentlich schonmal FALSCH! Wird dieses Skript nun z.B. zeitgesteuert irgendwann mal gestartet und der Aktor hat einen Fehler, landet bestenfalls nur eine Meldung im Log. Das liest ja wieder keiner :wink:
Die meisten Aktorik-Befehle sind Bidirektional, das vergisst man oft! Ich versuche mich also nun häufiger zu zwingen, den Befehl nach diesem Schema aufzubauen: if(RequestAction(23456,true)==false) { Pseudo(Warnung/Mail-verschicken/Alarm) };

Typisches Beispiel für „fire and forget“ :smiley:

Sollte man in der Tat nicht einfach so machen.