X-Sense XS0B-MR Rauchmelder und X-Sense XP0A-MR Brandalarm Automation

Hier anbei mal ein Skript für die X-Sense Rauchmelder bei auslösen eines Alarms.

<?php

################################################################################
# Script:   Rauchmelder.Brandalarm.Logik.ips.php
# Version:  1.0.20260510
# Author:   ChatGPT / angepasst für IP-Symcon
#
# Zentrale Brandalarm- und CO-Alarm-Logik für X-Sense Rauchmelder per MQTT.
#
# Aufgabe:
# Dieses Skript überwacht alle eingebundenen X-Sense Rauchmelder, erkennt
# Rauchalarm, Testalarm, Wartungszustände und Diagnosemeldungen und steuert
# darauf basierend Benachrichtigungen, Licht, Rollläden und optionale Sirenen.
#
# Hintergrund:
# Die X-Sense Rauchmelder liefern ihre Zustände über MQTT/Zigbee2MQTT nach
# IP-Symcon. Dieses Skript bündelt die Alarm- und Wartungslogik zentral, damit
# Brandalarm, Eskalation, Quittierung und Diagnose einheitlich verarbeitet werden.
#
# Funktionen:
# - sucht Rauchmelder automatisch unter definierten Root-Kategorien
# - erkennt aktiven Rauchalarm über Smoke Status
# - unterstützt Testmodus mit frei definierbarem Testmelder
# - setzt zentrale Status-, Quellen-, Zeit- und Diagnosevariablen
# - sendet WebFront Push-Benachrichtigungen
# - sendet Pushover-Nachrichten mit Tag-/Nacht-Priorität
# - schaltet bei Alarm nachts definierte Lichtgruppen ein
# - öffnet bei Alarm definierte Rollläden
# - unterstützt optionale Sirenen
# - ermöglicht Quittierung über Sonoff SNZB-01P
# - startet Testmodus über Sonoff SNZB-01P Long-Press
# - führt eine Alarmhistorie mit maximal 50 Einträgen
# - überwacht Offline-, Batterie- und EndOfLife-/Fault-Zustände
# - erkennt aktiven CO-Alarm über CO Status
# - setzt getrennte CO-Alarm-Status-, Quellen-, Zeit- und Historienvariablen
# - führt getrennte Eskalationslogik für Rauchalarm und CO-Alarm aus
#
# Das Skript arbeitet mit:
# - $searchRootIDs                -> Suchbereiche für Rauchmelder
# - $varBrandalarmAktivID         -> Brandalarm aktiv
# - $varBrandalarmQuelleID        -> auslösender Rauchmelder
# - $varBrandalarmZeitID          -> Zeitpunkt des Alarms
# - $varBrandalarmStatusID        -> aktueller Brandalarm-Status
# - $varBrandalarmAnzahlID        -> Anzahl aktiver Rauchmelder
# - $varBrandalarmEskalationID    -> Eskalationsstatus
# - $varBrandalarmTimerphaseID    -> interne Timerphase
# - $varBrandalarmWartungID       -> Wartungstext
# - $varBrandalarmTestmodusID     -> Testmodus
# - $varBrandalarmTestmelderID    -> Name des Testmelders
# - $varSicherheitsalarmQuittiertID -> gemeinsame Quittierung für Rauch- und CO-Alarm
# - $varSonoffActionID            -> Action-Variable des Sonoff-Tasters
# - $varRMGesamtstatusID          -> Rauchmelder-Gesamtstatus
# - $varRMBatterieID              -> Batteriewarnungen
# - $varRMEndOfLifeID             -> EndOfLife-/Fault-Meldungen
# - $varRMOfflineID               -> Offline-Meldungen
# - $varRMLetzterTestID           -> letzter Testzeitpunkt
# - $varBrandalarmHistorieID      -> Alarmhistorie
# - $webfrontID                   -> WebFront-Instanz
# - $pushoverID                   -> Pushover-Instanz
# - $varIsDayID                   -> Tag-/Nacht-Erkennung
# - $lichtIDs                     -> Lichtgruppen für Nachtalarm
# - $rollladenIDs                 -> Rollläden für Alarmöffnung
# - $sirenenIDs                   -> optionale Sirenen
# - $varCOAlarmAktivID            -> CO-Alarm aktiv
# - $varCOAlarmQuelleID           -> auslösender CO-Melder
# - $varCOAlarmZeitID             -> Zeitpunkt des CO-Alarms
# - $varCOAlarmStatusID           -> aktueller CO-Alarm-Status
# - $varCOAlarmAnzahlID           -> Anzahl aktiver CO-Melder
# - $varCOAlarmEskalationID       -> CO-Eskalationsstatus
# - $varCOAlarmTimerphaseID       -> interne CO-Timerphase
# - $varCOAlarmHistorieID         -> CO-Alarmhistorie
#
# Ablauf:
# 1. Rauchmelder unterhalb der Suchbereiche automatisch suchen
# 2. Smoke-, Online-, Batterie- und EndOfLife-Zustände auslesen
# 3. Testmodus bei Bedarf in die Alarmlogik einrechnen
# 4. Bei erkanntem Alarm eine kurze Stabilitätsprüfung starten
# 5. Nach bestätigtem Alarm Statusvariablen setzen und Push senden
# 6. Licht, Rollläden und optionale Sirenen auslösen
# 7. Nach definierter Zeit Eskalation prüfen
# 8. Alarmende erkennen und Zustände zurücksetzen
# 9. Wartungs- und Diagnosevariablen aktualisieren
# 10. CO-Alarm erkennen, Status setzen und Sicherheitsaktionen auslösen
# 11. CO-Eskalation getrennt von Rauchalarm prüfen
# 12. CO-Alarmende erkennen und CO-Zustände zurücksetzen
#
# Besonderheiten:
# - Alarm wird nicht sofort ausgelöst, sondern nach kurzer Prüfzeit bestätigt
# - Eskalation erfolgt bei mehreren gleichzeitig aktiven Rauchmeldern
# - Lichtautomatik wird nur nachts ausgeführt
# - Rollläden werden unabhängig von Tag/Nacht immer geöffnet
# - Testmodus löst dieselbe Logik aus, verwendet aber reduzierte Push-Priorität
# - Quittierung verhindert weitere Eskalationsmeldungen, solange Alarm aktiv ist
# - CO-Alarm wird separat zum Rauchalarm geführt
# - Rauchalarm und CO-Alarm verwenden getrennte Timerphasen
# - ein gemeinsamer ScriptTimer wird über Fälligkeiten koordiniert
# - dadurch überschreiben sich Rauch- und CO-Timer nicht gegenseitig
# - eine gemeinsame Quittierung gilt für Rauchalarm und CO-Alarm
#
# Voraussetzungen:
# - IP-Symcon mit eingebundenen X-Sense Rauchmeldern
# - MQTT-/Zigbee2MQTT-Variablen mit passenden Variablennamen
# - zentrale Statusvariablen für Brandalarm und Rauchmelderdiagnose
# - WebFront-Instanz für Push-Benachrichtigungen
# - optional Pushover-Modul
# - optional Sonoff SNZB-01P für Quittierung und Testmodus
#
# Hinweis:
# Dieses Skript wird durch Variablenereignisse, TimerEvents und manuelle
# Ausführung verwendet.
#
# Es erfolgt keine automatische Ausführung ohne Event.
################################################################################

################################################################################
# ID-DOKUMENTATION
#
# $searchRootIDs
# -> Root-Kategorien, unter denen nach Rauchmeldern gesucht wird
#
# $varBrandalarmAktivID
# -> Boolean-Variable für aktiven Brandalarm
#
# $varBrandalarmQuelleID
# -> String-Variable für den ersten auslösenden Rauchmelder
#
# $varBrandalarmZeitID
# -> String-Variable für den Zeitpunkt der Alarmauslösung
#
# $varBrandalarmStatusID
# -> String-Variable für den aktuellen Brandalarm-Status
#
# $varBrandalarmAnzahlID
# -> Integer-Variable für die Anzahl aktiver Rauchmelder
#
# $varBrandalarmEskalationID
# -> Boolean-Variable für Eskalation bei mehreren aktiven Rauchmeldern
#
# $varBrandalarmTimerphaseID
# -> String-Variable für interne Rauchalarm-Timerphasen
#
# Rauchalarm-Timerphasen:
# - ALARM_PRUEFUNG -> Rauchalarm wird nach Verzögerung erneut geprüft
# - ESKALATION     -> Rauchalarm-Eskalation wird nach Verzögerung geprüft
#
# Hinweis:
# Die Timerphase wird als Kombination aus Phase und Fälligkeit gespeichert.
#
# Format:
# PHASE|UnixTimestamp
#
# Beispiel:
# ALARM_PRUEFUNG|1760000000
# ESKALATION|1760000030
#
# $varBrandalarmWartungID
# -> String-Variable für zusammengefassten Wartungsstatus
#
# $varBrandalarmTestmodusID
# -> Boolean-Variable zum Aktivieren des Testmodus
#
# $varBrandalarmTestmelderID
# -> String-Variable für den Namen des simulierten Testmelders
#
# $varSicherheitsalarmQuittiertID
# -> Boolean-Variable für gemeinsam quittierten Rauch- oder CO-Alarm
#
# $varSonoffActionID
# -> String-Variable für Action-Werte des Sonoff SNZB-01P
#
# Sonoff-Aktionen:
# - single -> Sicherheitsalarm quittieren
# - long   -> Testmodus starten
#
# $varRMGesamtstatusID
# -> String-Variable für den Rauchmelder-Gesamtstatus
#
# $varRMBatterieID
# -> String-Variable für Rauchmelder mit Batteriewarnung
#
# $varRMEndOfLifeID
# -> String-Variable für Rauchmelder mit EndOfLife-/Fault-Zustand
#
# $varRMOfflineID
# -> String-Variable für Rauchmelder ohne Verbindung
#
# $varRMLetzterTestID
# -> Integer-Variable mit Unix-Zeitstempel des letzten Testalarms
#
# $varRMLetzterTestTextID
# -> String-Variable mit lesbarem Zeitpunkt des letzten Testalarms
#
# $varBrandalarmHistorieID
# -> String-Variable für Alarmhistorie mit maximal 50 Einträgen
#
# $webfrontID
# -> WebFront-ID für PushNotification
#
# $pushoverID
# -> Pushover-Instanz-ID für externe Push-Nachrichten
#
# $varIsDayID
# -> Boolean-Variable für Tag-/Nacht-Erkennung
#
# Tag-/Nacht-Erkennung:
# - true  -> Tag
# - false -> Nacht
#
# $lichtIDs
# -> Liste der Lichtgruppen, die nachts bei Brandalarm eingeschaltet werden
#
# $rollladenIDs
# -> Liste der Rollläden, die bei Brandalarm geöffnet werden
#
# $sirenenIDs
# -> optionale Liste von Sirenen-Schaltvariablen
#
# $varCOAlarmAktivID
# -> Boolean-Variable für aktiven CO-Alarm
#
# $varCOAlarmQuelleID
# -> String-Variable für den ersten auslösenden CO-Melder
#
# $varCOAlarmZeitID
# -> String-Variable für den Zeitpunkt der CO-Alarmauslösung
#
# $varCOAlarmStatusID
# -> String-Variable für den aktuellen CO-Alarm-Status
#
# $varCOAlarmAnzahlID
# -> Integer-Variable für die Anzahl aktiver CO-Melder
#
# $varCOAlarmEskalationID
# -> Boolean-Variable für CO-Eskalation
#
# $varCOAlarmTimerphaseID
# -> String-Variable für interne CO-Alarm-Timerphasen
#
# CO-Timerphasen:
# - CO_ESKALATION -> CO-Eskalation wird nach Verzögerung geprüft
#
# Hinweis:
# Die CO-Timerphase wird ebenfalls als Kombination aus Phase und Fälligkeit gespeichert.
#
# Format:
# PHASE|UnixTimestamp
#
# Beispiel:
# CO_ESKALATION|1760000030
#
# $varCOAlarmHistorieID
# -> String-Variable für CO-Alarmhistorie mit maximal 50 Einträgen
################################################################################

################################################################################
# FUNKTIONSWEISE BRANDALARM-LOGIK
#
# Dieses Skript:
# - wertet Rauchmelderzustände zentral aus
# - erkennt Alarm, Testalarm und Wartungszustände
# - setzt zentrale Statusvariablen
# - löst Benachrichtigungen und Sicherheitsaktionen aus
#
# Verhalten bei Alarm:
# - Alarm wird erkannt
# - kurze Stabilitätsprüfung wird gestartet
# - nach Bestätigung wird Brandalarm aktiv gesetzt
# - Quelle, Zeit, Anzahl und Status werden gespeichert
# - Push- und Pushover-Nachrichten werden versendet
# - nachts wird Licht eingeschaltet
# - Rollläden werden geöffnet
# - Eskalation wird verzögert geprüft
#
# Verhalten bei Eskalation:
# - mehrere aktive Rauchmelder lösen Eskalation aus
# - Eskalationsstatus wird gesetzt
# - zusätzliche Push-Nachrichten werden versendet
# - Alarmhistorie wird ergänzt
#
# Verhalten bei Quittierung:
# - Alarm bleibt aktiv
# - Quittierstatus wird gesetzt
# - Eskalationsmeldungen werden unterdrückt
# - Sirenen werden ausgeschaltet
#
# Verhalten bei Alarmende:
# - Brandalarm wird zurückgesetzt
# - Quelle, Zeit, Anzahl und Timerphase werden geleert
# - Eskalation und Quittierung werden zurückgesetzt
# - Push-Nachricht "Alarm beendet" wird gesendet
################################################################################

################################################################################
# AUFRUF
#
# Dieses Skript wird:
# -> per Änderungsereignis, TimerEvent oder manuell ausgeführt
#
# Verwendung:
# - bei Änderung von Smoke Status / Smoke
# - bei Änderung von Online Status / Connectivity
# - bei Änderung von Battery Status / Battery
# - bei Änderung von End of Life / Problem
# - bei Änderung der Sonoff SNZB-01P Action-Variable
# - zur manuellen Diagnose und Statusaktualisierung
################################################################################

################################################################################
# KONFIGURATION
#
# Zentrale Parameter:
#
# - $alarmVerzoegerungSekunden -> Verzögerung bis zur Alarmbestätigung
# - $eskalationSekunden        -> Verzögerung bis zur Eskalationsprüfung
# - $nachtPrioritaet           -> Pushover-Priorität bei Nacht
# - $tagPrioritaet             -> Pushover-Priorität bei Tag
#
# Hinweis:
# Neue Rauchmelder müssen nicht einzeln eingetragen werden, solange sie unterhalb
# der Suchbereiche liegen und die erwarteten Variablennamen besitzen.
################################################################################


// -----------------------------------------------------------------------------
// KONFIGURATION
// -----------------------------------------------------------------------------

$searchRootIDs = [
    54758, // KG
    58743, // EG
    49801  // OG
];

// Brandalarm
$varBrandalarmAktivID       = 14820;
$varBrandalarmQuelleID      = 19189;
$varBrandalarmZeitID        = 18951;
$varBrandalarmStatusID      = 26445;
$varBrandalarmAnzahlID      = 44964;
$varBrandalarmEskalationID  = 48844;
$varBrandalarmTimerphaseID  = 40877;
$varBrandalarmWartungID     = 21483;
$varBrandalarmHistorieID    = 43084;

// CO-Alarm
$varCOAlarmAktivID          = 49273;
$varCOAlarmQuelleID         = 10951;
$varCOAlarmZeitID           = 40864;
$varCOAlarmStatusID         = 35766;
$varCOAlarmEskalationID     = 28782;
$varCOAlarmTimerphaseID     = 20674;
$varCOAlarmHistorieID       = 28410;
$varCOAlarmAnzahlID         = 14148;

// Testmodus
$varBrandalarmTestmodusID   = 50536;
$varBrandalarmTestmelderID  = 18184;

// gemeinsame Quittierung für Rauch- und CO-Alarm
$varSicherheitsalarmQuittiertID = 25578;

// Sonoff SNZB-01P Aktion
$varSonoffActionID          = 17763;

// Diagnose / Sammelstatus
$varRMGesamtstatusID        = 47942;
$varRMBatterieID            = 15758;
$varRMEndOfLifeID           = 42441;
$varRMOfflineID             = 15404;
$varRMLetzterTestID         = 45872;
$varRMLetzterTestTextID     = 47476;

// Push
$webfrontID = 10436;
$pushoverID = 36786;

// Tag-/Nacht-Erkennung
// true  = Tag
// false = Nacht
$varIsDayID = 42475;

// Pushover Priorität
$tagPrioritaet   = 1;
$nachtPrioritaet = 2;

// Zeiten
$alarmVerzoegerungSekunden = 5;
$eskalationSekunden        = 30;
$coEskalationSekunden      = 30;


// -----------------------------------------------------------------------------
// LICHT
// -----------------------------------------------------------------------------

$lichtIDs = [
    [
        "name"         => "Gruppe EG Flur",
        "instanzID"    => 26675,
        "statusID"     => 58122,
        "helligkeitID" => 52325
    ],
    [
        "name"         => "Gruppe EG Haupt-Flur",
        "instanzID"    => 39396,
        "statusID"     => 48479,
        "helligkeitID" => 31879
    ],
    [
        "name"         => "Gruppe EG Wohnzimmer",
        "instanzID"    => 47571,
        "statusID"     => 21397,
        "helligkeitID" => 29467
    ],
    [
        "name"         => "Gruppe EG Küche",
        "instanzID"    => 14068,
        "statusID"     => 34672,
        "helligkeitID" => 56904
    ]
];


// -----------------------------------------------------------------------------
// ROLLLÄDEN
// 100 = offen
// 0   = geschlossen
// -----------------------------------------------------------------------------

$rollladenIDs = [
    [
        "name"    => "Fenster EG Küche Vorne",
        "levelID" => 33230
    ],
    [
        "name"    => "Fenster EG Küche Links",
        "levelID" => 12237
    ],
    [
        "name"    => "Fenster EG Wohnzimmer Rechts",
        "levelID" => 53796
    ],
    [
        "name"    => "Fenster EG Wohnzimmer Links",
        "levelID" => 13381
    ],
    [
        "name"    => "Fenster EG Wohnzimmer Garten",
        "levelID" => 24436
    ]
];


// -----------------------------------------------------------------------------
// OPTIONAL: SIRENEN
// -----------------------------------------------------------------------------

$sirenenIDs = [
];


// -----------------------------------------------------------------------------
// X-SENSE VARIABLENNAMEN
// -----------------------------------------------------------------------------

$smokeNames   = ["Smoke Status", "Smoke", "Rauch erkannt"];
$coNames      = ["CO Status", "CO", "CO erkannt"];
$onlineNames  = ["Online Status", "Connectivity", "Erreichbar"];
$batteryNames = ["Battery Status", "Battery", "Batteriewarnung"];
$endlifeNames = ["End of Life", "Problem", "Fehler / EndOfLife"];


// -----------------------------------------------------------------------------
// HILFSFUNKTIONEN
// -----------------------------------------------------------------------------

function RM_GetChildByNames(int $parentID, array $names)
{
    foreach (IPS_GetChildrenIDs($parentID) as $childID) {
        if (in_array(IPS_GetName($childID), $names)) {
            return $childID;
        }
    }

    return false;
}

function RM_FindDevicesRecursive(int $rootID, array $smokeNames, array $coNames)
{
    $found = [];

    foreach (IPS_GetChildrenIDs($rootID) as $childID) {

        $object  = IPS_GetObject($childID);
        $smokeID = RM_GetChildByNames($childID, $smokeNames);
        $coID    = RM_GetChildByNames($childID, $coNames);

        if ($smokeID !== false || $coID !== false) {
            $found[] = $childID;
            continue;
        }

        if ($object['ObjectType'] == 0 || $object['ObjectType'] == 1) {
            $found = array_merge(
                $found,
                RM_FindDevicesRecursive($childID, $smokeNames, $coNames)
            );
        }
    }

    return $found;
}

function RM_Scan(
    array $searchRootIDs,
    array $smokeNames,
    array $coNames,
    array $onlineNames,
    array $batteryNames,
    array $endlifeNames
) {
    $devices = [];

    foreach ($searchRootIDs as $rootID) {
        $devices = array_merge(
            $devices,
            RM_FindDevicesRecursive($rootID, $smokeNames, $coNames)
        );
    }

    $devices = array_unique($devices);

    $result = [
        "alarm"        => false,
        "quelle"       => "",
        "anzahl"       => 0,
        "aktiv"        => [],

        "coalarm"      => false,
        "coquelle"     => "",
        "coanzahl"     => 0,
        "coaktiv"      => [],

        "offline"      => [],
        "batterie"     => [],
        "endlife"      => [],
        "anzahlMelder" => count($devices),
        "testmodus"    => false
    ];

    foreach ($devices as $deviceID) {

        $name = IPS_GetName($deviceID);

        $smokeID   = RM_GetChildByNames($deviceID, $smokeNames);
        $coID      = RM_GetChildByNames($deviceID, $coNames);
        $onlineID  = RM_GetChildByNames($deviceID, $onlineNames);
        $batteryID = RM_GetChildByNames($deviceID, $batteryNames);
        $endlifeID = RM_GetChildByNames($deviceID, $endlifeNames);

        if ($smokeID !== false && GetValueBoolean($smokeID)) {
            $result["alarm"] = true;
            $result["anzahl"]++;
            $result["aktiv"][] = $name;

            if ($result["quelle"] == "") {
                $result["quelle"] = $name;
            }
        }

        if ($coID !== false && GetValueBoolean($coID)) {
            $result["coalarm"] = true;
            $result["coanzahl"]++;
            $result["coaktiv"][] = $name;

            if ($result["coquelle"] == "") {
                $result["coquelle"] = $name;
            }
        }

        if ($onlineID !== false && !GetValueBoolean($onlineID)) {
            $result["offline"][] = $name;
        }

        if ($batteryID !== false && GetValueBoolean($batteryID)) {
            $result["batterie"][] = $name;
        }

        if ($endlifeID !== false && GetValueBoolean($endlifeID)) {
            $result["endlife"][] = $name;
        }
    }

    if (
        $GLOBALS['varBrandalarmTestmodusID'] > 0 &&
        GetValueBoolean($GLOBALS['varBrandalarmTestmodusID'])
    ) {
        $testmelder = "Test Rauchmelder";

        if (
            $GLOBALS['varBrandalarmTestmelderID'] > 0 &&
            GetValueString($GLOBALS['varBrandalarmTestmelderID']) != ""
        ) {
            $testmelder = GetValueString($GLOBALS['varBrandalarmTestmelderID']);
        }

        $result["testmodus"] = true;
        $result["alarm"] = true;
        $result["anzahl"]++;
        $result["aktiv"][] = $testmelder;

        if ($result["quelle"] == "") {
            $result["quelle"] = $testmelder;
        }
    }

    return $result;
}

function RM_Push(int $webfrontID, string $titel, string $text)
{
    if ($webfrontID > 0) {
        @WFC_PushNotification($webfrontID, $titel, $text, "", 0);
    }
}

function RM_Pushover(int $instanceID, string $titel, string $text, int $priority = -1)
{
    if ($instanceID > 0) {
        @TUPO_SendMessage($instanceID, $titel, $text, $priority);
    }
}

function RM_SetValueIfValid(int $id, $value)
{
    if ($id > 0) {
        SetValue($id, $value);
    }
}

function RM_AddHistorie(int $id, string $text)
{
    if ($id <= 0) {
        return;
    }

    $alt = GetValueString($id);
    $neu = date("d.m.Y H:i:s") . " | " . $text;

    if ($alt != "") {
        $neu .= "\n" . $alt;
    }

    $lines = explode("\n", $neu);
    $lines = array_slice($lines, 0, 50);

    SetValueString($id, implode("\n", $lines));
}

function RM_GetBoolIfValid(int $id, bool $default = false)
{
    if ($id > 0) {
        return GetValueBoolean($id);
    }

    return $default;
}

function RM_GetAlarmPriority(
    int $varIsDayID,
    int $tagPrioritaet,
    int $nachtPrioritaet
) {
    if ($varIsDayID > 0) {
        if (GetValueBoolean($varIsDayID)) {
            return $tagPrioritaet;
        }

        return $nachtPrioritaet;
    }

    return $nachtPrioritaet;
}

function RM_SwitchEmergencyLights(array $lichtIDs, int $varIsDayID)
{
    $lichtAutomatikAktiv = true;

    if ($varIsDayID > 0) {
        $lichtAutomatikAktiv = !GetValueBoolean($varIsDayID);
    }

    if (!$lichtAutomatikAktiv) {
        return;
    }

    foreach ($lichtIDs as $licht) {

        if (isset($licht["helligkeitID"]) && $licht["helligkeitID"] > 0) {
            @RequestAction($licht["helligkeitID"], 100);
        }

        if (isset($licht["statusID"]) && $licht["statusID"] > 0) {
            @RequestAction($licht["statusID"], true);
        }
    }
}

function RM_OpenShutters(array $rollladenIDs)
{
    foreach ($rollladenIDs as $rollladen) {
        if (isset($rollladen["levelID"]) && $rollladen["levelID"] > 0) {
            @RequestAction($rollladen["levelID"], 100);
        }
    }
}

function RM_StopSirens(array $sirenenIDs)
{
    foreach ($sirenenIDs as $id) {
        @RequestAction($id, false);
    }
}

function RM_StartSirens(array $sirenenIDs)
{
    foreach ($sirenenIDs as $id) {
        @RequestAction($id, true);
    }
}


// -----------------------------------------------------------------------------
// TIMER-HILFSFUNKTIONEN
// -----------------------------------------------------------------------------

function RM_SetTimerPhase(int $id, string $phase, int $delaySeconds)
{
    if ($id <= 0) {
        return;
    }

    $due = time() + $delaySeconds;
    SetValueString($id, $phase . "|" . $due);
}

function RM_ClearTimerPhase(int $id)
{
    if ($id > 0) {
        SetValueString($id, "");
    }
}

function RM_GetTimerPhaseName(int $id): string
{
    if ($id <= 0) {
        return "";
    }

    $value = GetValueString($id);

    if ($value == "") {
        return "";
    }

    $parts = explode("|", $value);

    return $parts[0] ?? "";
}

function RM_GetTimerPhaseDue(int $id): int
{
    if ($id <= 0) {
        return 0;
    }

    $value = GetValueString($id);

    if ($value == "") {
        return 0;
    }

    $parts = explode("|", $value);

    return isset($parts[1]) ? (int)$parts[1] : 0;
}

function RM_IsTimerPhaseDue(int $id, string $phase): bool
{
    if ($id <= 0) {
        return false;
    }

    if (RM_GetTimerPhaseName($id) !== $phase) {
        return false;
    }

    $due = RM_GetTimerPhaseDue($id);

    if ($due <= 0) {
        return true;
    }

    return time() >= $due;
}

function RM_RefreshScriptTimer(int $scriptID, array $timerPhaseIDs)
{
    $nextDue = null;

    foreach ($timerPhaseIDs as $id) {

        if ($id <= 0) {
            continue;
        }

        $phase = RM_GetTimerPhaseName($id);
        $due   = RM_GetTimerPhaseDue($id);

        if ($phase == "" || $due <= 0) {
            continue;
        }

        if ($nextDue === null || $due < $nextDue) {
            $nextDue = $due;
        }
    }

    if ($nextDue === null) {
        IPS_SetScriptTimer($scriptID, 0);
        return;
    }

    $seconds = max(1, $nextDue - time());

    IPS_SetScriptTimer($scriptID, $seconds);
}


// -----------------------------------------------------------------------------
// TIMER-AUFRUF
// -----------------------------------------------------------------------------

if ($_IPS['SENDER'] == "TimerEvent") {

    $scan = RM_Scan(
        $searchRootIDs,
        $smokeNames,
        $coNames,
        $onlineNames,
        $batteryNames,
        $endlifeNames
    );

    // ------------------------------------------------------------------------
    // Rauchalarm bestätigen
    // ------------------------------------------------------------------------

    if (RM_IsTimerPhaseDue($varBrandalarmTimerphaseID, "ALARM_PRUEFUNG")) {

        RM_ClearTimerPhase($varBrandalarmTimerphaseID);

        if ($scan["alarm"] && !GetValueBoolean($varBrandalarmAktivID)) {

            SetValueBoolean($varBrandalarmAktivID, true);
            SetValueString($varBrandalarmQuelleID, $scan["quelle"]);
            SetValueString($varBrandalarmZeitID, date("d.m.Y H:i:s"));

            if ($scan["testmodus"]) {

                RM_SetValueIfValid(
                    $varRMLetzterTestID,
                    time()
                );

                RM_SetValueIfValid(
                    $varRMLetzterTestTextID,
                    date("d.m.Y H:i:s") . " | " . $scan["quelle"]
                );
            }

            RM_SetValueIfValid($varBrandalarmAnzahlID, $scan["anzahl"]);
            RM_SetValueIfValid($varSicherheitsalarmQuittiertID, false);

            RM_SetValueIfValid(
                $varBrandalarmStatusID,
                $scan["testmodus"]
                    ? "TESTMODUS: Alarm aktiv: " . $scan["quelle"]
                    : "ALARM aktiv: " . $scan["quelle"]
            );

            RM_Push(
                $webfrontID,
                $scan["testmodus"] ? "🧪 Rauchalarm Test" : "🚨 Rauchalarm!",
                "Ausgelöst durch: " . $scan["quelle"]
            );

            $alarmPriority = RM_GetAlarmPriority(
                $varIsDayID,
                $tagPrioritaet,
                $nachtPrioritaet
            );

            RM_Pushover(
                $pushoverID,
                $scan["testmodus"] ? "🧪 Rauchalarm Test" : "🚨 Rauchalarm!",
                "Ausgelöst durch: " . $scan["quelle"],
                $scan["testmodus"] ? -1 : $alarmPriority
            );

            RM_AddHistorie(
                $varBrandalarmHistorieID,
                ($scan["testmodus"] ? "TEST" : "ALARM") . " | " . $scan["quelle"]
            );

            RM_SwitchEmergencyLights($lichtIDs, $varIsDayID);
            RM_OpenShutters($rollladenIDs);
            RM_StartSirens($sirenenIDs);

            RM_SetTimerPhase(
                $varBrandalarmTimerphaseID,
                "ESKALATION",
                $eskalationSekunden
            );
        }
    }

    // ------------------------------------------------------------------------
    // Rauchalarm Eskalation
    // ------------------------------------------------------------------------

    if (RM_IsTimerPhaseDue($varBrandalarmTimerphaseID, "ESKALATION")) {

        RM_ClearTimerPhase($varBrandalarmTimerphaseID);

        if ($scan["alarm"] && GetValueBoolean($varBrandalarmAktivID)) {

            RM_SetValueIfValid($varBrandalarmAnzahlID, $scan["anzahl"]);

            if (!RM_GetBoolIfValid($varSicherheitsalarmQuittiertID, false)) {

                if ($scan["anzahl"] >= 2) {

                    RM_SetValueIfValid($varBrandalarmEskalationID, true);

                    RM_SetValueIfValid(
                        $varBrandalarmStatusID,
                        $scan["testmodus"]
                            ? "TESTMODUS: Eskalation mehrere Rauchmelder aktiv"
                            : "ESKALATION: mehrere Rauchmelder aktiv"
                    );

                    RM_Push(
                        $webfrontID,
                        $scan["testmodus"]
                            ? "🧪 Test Eskalation"
                            : "🔥 BRANDALARM ESKALATION!",
                        $scan["anzahl"] . " Rauchmelder aktiv: " . implode(", ", $scan["aktiv"])
                    );

                    RM_Pushover(
                        $pushoverID,
                        $scan["testmodus"]
                            ? "🧪 Test Eskalation"
                            : "🔥 BRANDALARM ESKALATION!",
                        $scan["anzahl"] . " Rauchmelder aktiv: " . implode(", ", $scan["aktiv"]),
                        $scan["testmodus"] ? -1 : $nachtPrioritaet
                    );

                    RM_AddHistorie(
                        $varBrandalarmHistorieID,
                        "ESKALATION | " . $scan["anzahl"] . " Rauchmelder"
                    );

                } else {

                    RM_SetValueIfValid(
                        $varBrandalarmStatusID,
                        $scan["testmodus"]
                            ? "TESTMODUS: Alarm weiterhin aktiv: " . GetValueString($varBrandalarmQuelleID)
                            : "ALARM weiterhin aktiv: " . GetValueString($varBrandalarmQuelleID)
                    );

                    RM_Push(
                        $webfrontID,
                        $scan["testmodus"]
                            ? "🧪 Rauchalarm Test weiterhin aktiv"
                            : "⚠️ Rauchalarm weiterhin aktiv",
                        "Quelle: " . GetValueString($varBrandalarmQuelleID)
                    );

                    $alarmPriority = RM_GetAlarmPriority(
                        $varIsDayID,
                        $tagPrioritaet,
                        $nachtPrioritaet
                    );

                    RM_Pushover(
                        $pushoverID,
                        $scan["testmodus"]
                            ? "🧪 Rauchalarm Test weiterhin aktiv"
                            : "⚠️ Rauchalarm weiterhin aktiv",
                        "Quelle: " . GetValueString($varBrandalarmQuelleID),
                        $scan["testmodus"] ? -1 : $alarmPriority
                    );
                }

            } else {

                RM_SetValueIfValid(
                    $varBrandalarmStatusID,
                    "Alarm quittiert - weiterhin aktiv: " . GetValueString($varBrandalarmQuelleID)
                );
            }
        }
    }

    // ------------------------------------------------------------------------
    // CO-Alarm Eskalation
    // ------------------------------------------------------------------------

    if (RM_IsTimerPhaseDue($varCOAlarmTimerphaseID, "CO_ESKALATION")) {

        RM_ClearTimerPhase($varCOAlarmTimerphaseID);

        if ($scan["coalarm"] && RM_GetBoolIfValid($varCOAlarmAktivID, false)) {

            RM_SetValueIfValid($varCOAlarmAnzahlID, $scan["coanzahl"]);

            if (!RM_GetBoolIfValid($varSicherheitsalarmQuittiertID, false)) {

                RM_SetValueIfValid($varCOAlarmEskalationID, true);

                RM_SetValueIfValid(
                    $varCOAlarmStatusID,
                    "CO ALARM ESKALATION: " . implode(", ", $scan["coaktiv"])
                );

                RM_Push(
                    $webfrontID,
                    "☠️ CO-ALARM ESKALATION!",
                    $scan["coanzahl"] . " CO-Melder aktiv: " . implode(", ", $scan["coaktiv"])
                );

                RM_Pushover(
                    $pushoverID,
                    "☠️ CO-ALARM ESKALATION!",
                    $scan["coanzahl"] . " CO-Melder aktiv: " . implode(", ", $scan["coaktiv"]),
                    $nachtPrioritaet
                );

                RM_AddHistorie(
                    $varCOAlarmHistorieID,
                    "CO-ESKALATION | " . $scan["coanzahl"] . " CO-Melder"
                );

            } else {

                RM_SetValueIfValid(
                    $varCOAlarmStatusID,
                    "CO Alarm quittiert - weiterhin aktiv: " . GetValueString($varCOAlarmQuelleID)
                );
            }
        }
    }

    RM_RefreshScriptTimer(
        $_IPS['SELF'],
        [
            $varBrandalarmTimerphaseID,
            $varCOAlarmTimerphaseID
        ]
    );

    return;
}


// -----------------------------------------------------------------------------
// NORMALER AUFRUF
// -----------------------------------------------------------------------------

$scan = RM_Scan(
    $searchRootIDs,
    $smokeNames,
    $coNames,
    $onlineNames,
    $batteryNames,
    $endlifeNames
);


// -----------------------------------------------------------------------------
// SONOFF SNZB-01P AUSWERTUNG
// -----------------------------------------------------------------------------

if ($_IPS['SENDER'] == "Variable") {

    if ($_IPS['VARIABLE'] == $varSonoffActionID) {

        $action = GetValueString($varSonoffActionID);

        // --------------------------------------------------------------------
        // SINGLE = Sicherheitsalarm quittieren
        // --------------------------------------------------------------------

        if ($action == "single") {

            if (
                GetValueBoolean($varBrandalarmAktivID) ||
                RM_GetBoolIfValid($varCOAlarmAktivID, false)
            ) {

                RM_SetValueIfValid($varSicherheitsalarmQuittiertID, true);
                RM_SetValueIfValid($varBrandalarmStatusID, "Sicherheitsalarm quittiert");

                if ($varCOAlarmStatusID > 0 && RM_GetBoolIfValid($varCOAlarmAktivID, false)) {
                    RM_SetValueIfValid($varCOAlarmStatusID, "CO Alarm quittiert");
                }

                RM_Pushover(
                    $pushoverID,
                    "🔕 Sicherheitsalarm quittiert",
                    "Alarm wurde manuell quittiert.",
                    -1
                );

                RM_AddHistorie(
                    $varBrandalarmHistorieID,
                    "QUITTIERT"
                );

                if ($varCOAlarmHistorieID > 0 && RM_GetBoolIfValid($varCOAlarmAktivID, false)) {
                    RM_AddHistorie(
                        $varCOAlarmHistorieID,
                        "CO QUITTIERT"
                    );
                }

                RM_StopSirens($sirenenIDs);
            }

            return;
        }

        // --------------------------------------------------------------------
        // LONG = Testmodus starten
        // --------------------------------------------------------------------

        if ($action == "long") {

            SetValueString($varBrandalarmTestmelderID, "SNZB-01P Test");
            SetValueBoolean($varBrandalarmTestmodusID, true);

            RM_Pushover(
                $pushoverID,
                "🧪 Rauchalarm Test",
                "Testmodus über Sonoff SNZB-01P gestartet.",
                -1
            );

            $scan = RM_Scan(
                $searchRootIDs,
                $smokeNames,
                $coNames,
                $onlineNames,
                $batteryNames,
                $endlifeNames
            );
        }
    }
}

RM_SetValueIfValid($varBrandalarmAnzahlID, $scan["anzahl"]);
RM_SetValueIfValid($varCOAlarmAnzahlID, $scan["coanzahl"]);


// -----------------------------------------------------------------------------
// CO-ALARM
// -----------------------------------------------------------------------------

if ($scan["coalarm"]) {

    if (!RM_GetBoolIfValid($varCOAlarmAktivID, false)) {

        RM_SetValueIfValid($varCOAlarmAktivID, true);
        RM_SetValueIfValid($varCOAlarmQuelleID, $scan["coquelle"]);
        RM_SetValueIfValid($varCOAlarmZeitID, date("d.m.Y H:i:s"));
        RM_SetValueIfValid($varCOAlarmAnzahlID, $scan["coanzahl"]);
        RM_SetValueIfValid($varCOAlarmEskalationID, false);
        RM_SetValueIfValid($varSicherheitsalarmQuittiertID, false);

        RM_SetValueIfValid(
            $varCOAlarmStatusID,
            "CO ALARM aktiv: " . $scan["coquelle"]
        );

        RM_Push(
            $webfrontID,
            "☠️ CO-ALARM!",
            "Kohlenmonoxid erkannt: " . $scan["coquelle"]
        );

        $alarmPriority = RM_GetAlarmPriority(
            $varIsDayID,
            $tagPrioritaet,
            $nachtPrioritaet
        );

        RM_Pushover(
            $pushoverID,
            "☠️ CO-ALARM!",
            "Kohlenmonoxid erkannt: " . $scan["coquelle"],
            $alarmPriority
        );

        RM_AddHistorie(
            $varCOAlarmHistorieID,
            "CO-ALARM | " . $scan["coquelle"]
        );

        RM_SwitchEmergencyLights($lichtIDs, $varIsDayID);
        RM_OpenShutters($rollladenIDs);
        RM_StartSirens($sirenenIDs);

        RM_SetTimerPhase(
            $varCOAlarmTimerphaseID,
            "CO_ESKALATION",
            $coEskalationSekunden
        );

        RM_RefreshScriptTimer(
            $_IPS['SELF'],
            [
                $varBrandalarmTimerphaseID,
                $varCOAlarmTimerphaseID
            ]
        );
    }

    return;
}


// -----------------------------------------------------------------------------
// CO-ALARM BEENDET
// -----------------------------------------------------------------------------

if (!$scan["coalarm"] && RM_GetBoolIfValid($varCOAlarmAktivID, false)) {

    RM_SetValueIfValid($varCOAlarmAktivID, false);
    RM_SetValueIfValid($varCOAlarmQuelleID, "");
    RM_SetValueIfValid($varCOAlarmZeitID, "");
    RM_SetValueIfValid($varCOAlarmAnzahlID, 0);
    RM_SetValueIfValid($varCOAlarmEskalationID, false);
    RM_ClearTimerPhase($varCOAlarmTimerphaseID);

    RM_SetValueIfValid(
        $varCOAlarmStatusID,
        "CO Alarm beendet"
    );

    RM_Push(
        $webfrontID,
        "✅ CO Alarm beendet",
        "CO Werte wieder normal."
    );

    RM_Pushover(
        $pushoverID,
        "✅ CO Alarm beendet",
        "CO Werte wieder normal.",
        -1
    );

    RM_AddHistorie(
        $varCOAlarmHistorieID,
        "CO-ALARM ENDE"
    );

    RM_StopSirens($sirenenIDs);

    RM_RefreshScriptTimer(
        $_IPS['SELF'],
        [
            $varBrandalarmTimerphaseID,
            $varCOAlarmTimerphaseID
        ]
    );
}


// -----------------------------------------------------------------------------
// AUFRÄUMEN BEI NORMALZUSTAND
// -----------------------------------------------------------------------------

if (
    !$scan["alarm"] &&
    !$scan["coalarm"] &&
    !GetValueBoolean($varBrandalarmAktivID) &&
    !RM_GetBoolIfValid($varCOAlarmAktivID, false)
) {

    SetValueString($varBrandalarmQuelleID, "");
    SetValueString($varBrandalarmZeitID, "");

    RM_SetValueIfValid($varBrandalarmAnzahlID, 0);
    RM_SetValueIfValid($varBrandalarmEskalationID, false);
    RM_ClearTimerPhase($varBrandalarmTimerphaseID);

    RM_SetValueIfValid($varCOAlarmQuelleID, "");
    RM_SetValueIfValid($varCOAlarmZeitID, "");
    RM_SetValueIfValid($varCOAlarmAnzahlID, 0);
    RM_SetValueIfValid($varCOAlarmEskalationID, false);
    RM_ClearTimerPhase($varCOAlarmTimerphaseID);

    RM_SetValueIfValid($varSicherheitsalarmQuittiertID, false);

    RM_RefreshScriptTimer(
        $_IPS['SELF'],
        [
            $varBrandalarmTimerphaseID,
            $varCOAlarmTimerphaseID
        ]
    );
}


// -----------------------------------------------------------------------------
// RAUCHALARM ERKANNT
// -----------------------------------------------------------------------------

if ($scan["alarm"]) {

    if (!GetValueBoolean($varBrandalarmAktivID)) {

        RM_SetValueIfValid(
            $varBrandalarmStatusID,
            $scan["testmodus"]
                ? "TESTMODUS aktiv, prüfe Stabilität..."
                : "Alarm erkannt, prüfe Stabilität..."
        );

        RM_SetTimerPhase(
            $varBrandalarmTimerphaseID,
            "ALARM_PRUEFUNG",
            $alarmVerzoegerungSekunden
        );

        RM_RefreshScriptTimer(
            $_IPS['SELF'],
            [
                $varBrandalarmTimerphaseID,
                $varCOAlarmTimerphaseID
            ]
        );

        return;
    }

    RM_SetValueIfValid($varBrandalarmAnzahlID, $scan["anzahl"]);

    if (
        $scan["anzahl"] >= 2 &&
        $varBrandalarmEskalationID > 0 &&
        !GetValueBoolean($varBrandalarmEskalationID) &&
        !RM_GetBoolIfValid($varSicherheitsalarmQuittiertID, false)
    ) {

        SetValueBoolean($varBrandalarmEskalationID, true);

        RM_SetValueIfValid(
            $varBrandalarmStatusID,
            $scan["testmodus"]
                ? "TESTMODUS: Eskalation mehrere Rauchmelder aktiv"
                : "ESKALATION: mehrere Rauchmelder aktiv"
        );

        RM_Push(
            $webfrontID,
            $scan["testmodus"] ? "🧪 Test: mehrere Rauchmelder" : "🔥 Mehrere Rauchmelder!",
            $scan["anzahl"] . " Rauchmelder aktiv: " . implode(", ", $scan["aktiv"])
        );

        $alarmPriority = RM_GetAlarmPriority(
            $varIsDayID,
            $tagPrioritaet,
            $nachtPrioritaet
        );

        RM_Pushover(
            $pushoverID,
            $scan["testmodus"] ? "🧪 Test: mehrere Rauchmelder" : "🔥 Mehrere Rauchmelder!",
            $scan["anzahl"] . " Rauchmelder aktiv: " . implode(", ", $scan["aktiv"]),
            $scan["testmodus"] ? -1 : $alarmPriority
        );

        RM_AddHistorie(
            $varBrandalarmHistorieID,
            "ESKALATION | " . $scan["anzahl"] . " Rauchmelder"
        );
    }

    return;
}


// -----------------------------------------------------------------------------
// RAUCHALARM BEENDET
// -----------------------------------------------------------------------------

if (!$scan["alarm"] && GetValueBoolean($varBrandalarmAktivID)) {

    SetValueBoolean($varBrandalarmAktivID, false);
    SetValueString($varBrandalarmQuelleID, "");
    SetValueString($varBrandalarmZeitID, "");

    RM_SetValueIfValid($varBrandalarmAnzahlID, 0);
    RM_SetValueIfValid($varBrandalarmEskalationID, false);
    RM_SetValueIfValid($varBrandalarmStatusID, "Alarm beendet");
    RM_ClearTimerPhase($varBrandalarmTimerphaseID);

    if (!RM_GetBoolIfValid($varCOAlarmAktivID, false)) {
        RM_SetValueIfValid($varSicherheitsalarmQuittiertID, false);
    }

    RM_RefreshScriptTimer(
        $_IPS['SELF'],
        [
            $varBrandalarmTimerphaseID,
            $varCOAlarmTimerphaseID
        ]
    );

    RM_Push(
        $webfrontID,
        "✅ Rauchalarm beendet",
        "Alle Rauchmelder wieder frei."
    );

    RM_Pushover(
        $pushoverID,
        "✅ Rauchalarm beendet",
        "Alle Rauchmelder wieder frei.",
        -1
    );

    RM_AddHistorie(
        $varBrandalarmHistorieID,
        "ENDE"
    );

    RM_StopSirens($sirenenIDs);

    return;
}


// -----------------------------------------------------------------------------
// WARTUNG / DIAGNOSE
// -----------------------------------------------------------------------------

$wartung = [];

if (count($scan["offline"]) > 0) {
    $wartung[] = "Offline: " . implode(", ", $scan["offline"]);
}

if (count($scan["batterie"]) > 0) {
    $wartung[] = "Batterie prüfen: " . implode(", ", $scan["batterie"]);
}

if (count($scan["endlife"]) > 0) {
    $wartung[] = "EndOfLife/Fault: " . implode(", ", $scan["endlife"]);
}


// ---------------------------------------------------------------------------
// Diagnosevariablen
// ---------------------------------------------------------------------------

RM_SetValueIfValid(
    $varRMOfflineID,
    count($scan["offline"]) > 0
        ? implode(", ", $scan["offline"])
        : "Keine Rauchmelder offline"
);

RM_SetValueIfValid(
    $varRMBatterieID,
    count($scan["batterie"]) > 0
        ? implode(", ", $scan["batterie"])
        : "Alle Batterien OK"
);

RM_SetValueIfValid(
    $varRMEndOfLifeID,
    count($scan["endlife"]) > 0
        ? implode(", ", $scan["endlife"])
        : "Keine Fault-/EndOfLife-Melder"
);


// ---------------------------------------------------------------------------
// CO Status
// ---------------------------------------------------------------------------

if (!$scan["coalarm"] && !RM_GetBoolIfValid($varCOAlarmAktivID, false)) {
    RM_SetValueIfValid(
        $varCOAlarmStatusID,
        "Kein CO Alarm"
    );
}


// ---------------------------------------------------------------------------
// Gesamtstatus
// ---------------------------------------------------------------------------

if (
    count($scan["offline"]) == 0 &&
    count($scan["batterie"]) == 0 &&
    count($scan["endlife"]) == 0
) {

    RM_SetValueIfValid(
        $varRMGesamtstatusID,
        "🟢 Alle Rauchmelder OK"
    );

    RM_SetValueIfValid(
        $varBrandalarmStatusID,
        "Alles normal"
    );

    RM_SetValueIfValid(
        $varBrandalarmWartungID,
        "Alle " . $scan["anzahlMelder"] . " Rauchmelder normal"
    );

} else {

    $status = [];

    if (count($scan["offline"]) > 0) {
        $status[] = "🔴 Offline";
    }

    if (count($scan["batterie"]) > 0) {
        $status[] = "🟠 Batterie";
    }

    if (count($scan["endlife"]) > 0) {
        $status[] = "⚫ Fault";
    }

    RM_SetValueIfValid(
        $varRMGesamtstatusID,
        implode(" | ", $status)
    );

    RM_SetValueIfValid(
        $varBrandalarmStatusID,
        "Wartung erforderlich"
    );

    RM_SetValueIfValid(
        $varBrandalarmWartungID,
        implode(" | ", $wartung)
    );
}

Dokumentation dazu:

<?php

################################################################################
# Dokumentation: Rauchmelder Brandalarm Logik
# Version:      1.0.20260511
# Author:       ChatGPT / angepasst für IP-Symcon
#
# Übersicht:
# Dokumentation der zentralen Brandalarm- und Sicherheitslogik für
# X-Sense Rauchmelder in IP-Symcon.
#
# Die Rauchmelder werden über MQTT eingebunden und automatisch überwacht.
################################################################################

################################################################################
# UNTERSTÜTZTE HARDWARE
#
# Rauchmelder:
# - X-Sense XS0B-MR
# - X-Sense XP0A-MR
#
# Anbindung:
# - MQTT
# - Zigbee2MQTT
#
# Alarmquittierung / Testmodus:
# - Sonoff SNZB-01P
#
# Unterstützte Aktionen:
# - single
# - long
################################################################################

################################################################################
# HAUPTFUNKTIONEN
#
# Das Skript erkennt automatisch:
#
# - Rauchalarm
# - Mehrfachauslösung
# - Offline-Melder
# - niedrige Batterien
# - EndOfLife / Fault-Zustände
#
# Zusätzlich verarbeitet das System:
#
# - Push-Benachrichtigungen
# - Pushover-Alarme
# - Lichtsteuerungen
# - Rollladenöffnungen
# - Alarmhistorien
# - Nachtlogik
# - Alarmquittierungen
################################################################################

################################################################################
# RAUCHALARM-ERKENNUNG
#
# Das Skript überwacht automatisch alle gefundenen Rauchmelder.
#
# Verwendete Variablen:
#
# Smoke Status
# -> Rauchalarm aktiv
#
# Online Status
# -> Melder online/offline
#
# Battery Status
# -> Batteriewarnung
#
# End of Life
# -> Fault / Gerätefehler
#
#
# Verhalten bei Rauchalarm:
#
# Rauch erkannt
# -> Stabilitätsprüfung
# -> Alarm aktiv
# -> Push / Pushover
# -> Licht nachts einschalten
# -> Rollläden öffnen
# -> Eskalation starten
################################################################################

################################################################################
# STABILITÄTSPRÜFUNG
#
# Vor echter Alarmierung wird geprüft:
#
# Alarm weiterhin nach 5 Sekunden aktiv?
#
# Dadurch werden reduziert:
#
# - MQTT-Fehler
# - kurze Fehltrigger
# - Kommunikationsspitzen
################################################################################

################################################################################
# ESKALATIONSLOGIK
#
# Nach erfolgreicher Alarmierung startet ein Eskalationstimer:
#
# 30 Sekunden
#
#
# Verhalten:
#
# Ein Rauchmelder aktiv:
# -> Alarm weiterhin aktiv
#
# Zwei oder mehr Rauchmelder aktiv:
# -> ESKALATION
#
#
# Zusätzlich:
#
# - kritische Pushs
# - Historieneintrag
# - Eskalationsstatus
################################################################################

################################################################################
# TAG- / NACHTLOGIK
#
# Verwendete Variable:
#
# Is Day
# -> Variable aus Location Control
#
#
# Verhalten tagsüber:
#
# - kein Licht
# - normale Alarmierung
# - Rollläden öffnen
#
#
# Verhalten nachts:
#
# - Licht einschalten
# - kritische Alarmierung
# - Rollläden öffnen
################################################################################

################################################################################
# TIMERLOGIK
#
# Rauchalarm und CO-Alarm verwenden getrennte Timerphasen.
#
# Beide Systeme teilen sich jedoch nur einen einzigen ScriptTimer.
#
#
# Hintergrund:
#
# IP-Symcon erlaubt pro Skript nur einen aktiven ScriptTimer.
#
#
# Lösung:
#
# Die Timerphasen speichern:
#
# - Phase
# - Fälligkeit
#
#
# Format:
#
# PHASE|UnixTimestamp
#
#
# Beispiel:
#
# ALARM_PRUEFUNG|1760000000
# CO_ESKALATION|1760000030
#
#
# Vorteile:
#
# - Rauchalarm und CO-Alarm blockieren sich nicht gegenseitig
# - beliebig erweiterbar
# - stabil bei parallelen Alarmen
# - saubere Eskalationssteuerung
################################################################################

################################################################################
# TIMERPHASEN
#
# Rauchalarm:
#
# ALARM_PRUEFUNG
# -> Stabilitätsprüfung nach Raucherkennung
#
# ESKALATION
# -> Eskalationsprüfung bei weiterhin aktivem Alarm
#
#
# CO-Alarm:
#
# CO_ESKALATION
# -> Eskalationsprüfung bei weiterhin aktivem CO-Alarm
################################################################################

################################################################################
# LICHTSTEUERUNG
#
# Nachts werden definierte Lichtgruppen aktiviert.
#
# Verhalten:
#
# - Licht EIN
# - Helligkeit 100 %
#
#
# Aktuell eingebunden:
#
# - EG Flur
# - EG Haupt-Flur
# - EG Wohnzimmer
# - EG Küche
################################################################################

################################################################################
# ROLLLADENSTEUERUNG
#
# Bei Brandalarm werden definierte Rollläden geöffnet.
#
# Verhalten:
#
# 100 % = offen
#
#
# Zweck:
#
# - Fluchtwege
# - Sichtbarkeit
# - Feuerwehrzugang
################################################################################

################################################################################
# SIRENENLOGIK
#
# Optional können externe Sirenen eingebunden werden.
#
# Verhalten:
#
# Alarm aktiv:
# -> Sirenen EIN
#
# Alarm quittiert:
# -> Sirenen AUS
#
# Alarm beendet:
# -> Sirenen AUS
#
#
# Hinweis:
#
# Licht und Rollläden bleiben auch nach Quittierung aktiv,
# damit Fluchtwege weiterhin sichtbar bleiben.
################################################################################

################################################################################
# ALARMQUITTIERUNG
#
# Über den Sonoff SNZB-01P kann ein Alarm quittiert werden.
#
#
# SINGLE:
#
# -> Alarm quittieren
#
# Wirkung:
# - Sirenen AUS
# - Eskalation stoppen
# - Alarm bleibt sichtbar
# - Licht bleibt aktiv
# - Rollläden bleiben offen
#
#
# LONG:
#
# -> Testmodus starten
#
# Wirkung:
# - vollständiger Funktionstest
# - ohne echten Rauchalarm
################################################################################

################################################################################
# TESTMODUS
#
# Der Testmodus simuliert einen echten Rauchalarm.
#
# Folgende Funktionen werden vollständig getestet:
#
# - Push
# - Pushover
# - Lichtsteuerung
# - Rollladenöffnung
# - Eskalation
# - Historie
################################################################################

################################################################################
# TESTALARM-PROTOKOLLIERUNG
#
# Der letzte erfolgreiche Testalarm wird gespeichert.
#
# Verwendete Variablen:
#
# Rauchmelder Letzter Test
# -> Unix-Zeitstempel
#
# Rauchmelder Letzter Test Text
# -> lesbarer Zeitstempel mit Quelle
#
#
# Beispiel:
#
# 11.05.2026 20:15:22 | SNZB-01P Test
################################################################################

################################################################################
# WARTUNGSÜBERWACHUNG
#
# Das Skript überwacht automatisch:
#
# Rauchmelder Erreichbarkeit:
# Online Status = false
#
# Rauchmelder Batteriestatus:
# Battery Status = true
#
# Rauchmelder Faultstatus:
# End of Life = true
################################################################################

################################################################################
# SAMMELSTATUS
#
# Die Variable:
#
# Rauchmelder Gesamtstatus
#
# zeigt:
#
# - Gesamtstatus
# - Offline-Zustände
# - Batterieprobleme
# - Fault-Zustände
################################################################################

################################################################################
# ALARMHISTORIE
#
# Alle wichtigen Ereignisse werden gespeichert.
#
# Beispiele:
#
# 10.05.2026 23:14 | ALARM | Rauchmelder EG Flur
# 10.05.2026 23:15 | ESKALATION | 2 Rauchmelder
# 10.05.2026 23:16 | QUITTIERT
# 10.05.2026 23:18 | ENDE
################################################################################

################################################################################
# PUSH-BENACHRICHTIGUNGEN
#
# Unterstützt:
#
# WebFront Push:
# - WFC_PushNotification
#
# Pushover:
# - TUPO_SendMessage
################################################################################

################################################################################
# PRIORITÄTEN
#
# Tagsüber:
# -> Priorität 1
#
# Nachts:
# -> Priorität 2
#
#
# Zweck:
#
# - lautere Warnung
# - Wiederholung
# - höhere Sichtbarkeit
################################################################################

################################################################################
# AUTOMATISCHE RÜCKSETZUNG
#
# Nach Alarmende werden automatisch zurückgesetzt:
#
# - Brandalarm Aktiv
# - Eskalation
# - Quittierung
# - Timerphase
# - Alarmquelle
# - Alarmzeit
################################################################################

################################################################################
# EMPFOHLENE STRUKTUR
#
# Gewerke
# └── Sicherheit
#     └── Brandalarm
#         ├── Brandalarm Aktiv
#         ├── Brandalarm Eskalation
#         ├── Brandalarm Quittiert
#         ├── Brandalarm Status
#         ├── Brandalarm Historie
#         ├── Brandalarm Wartung
#         ├── Rauchmelder Erreichbarkeit
#         ├── Rauchmelder Batteriestatus
#         ├── Rauchmelder Faultstatus
#         ├── Rauchmelder Gesamtstatus
#         └── Rauchmelder Letzter Test
################################################################################

################################################################################
# EMPFOHLENE EVENTS
#
# Rauchmelder:
#
# Bei Änderung:
# - Smoke Status
# - Online Status
# - Battery Status
# - End Of Life
#
# -> Skript ausführen
#
#
# Sonoff SNZB-01P:
#
# Bei Änderung:
# - Action
#
# -> Skript ausführen
#
#
# Wartungsprüfung:
#
# täglich
#
# -> Skript ausführen
################################################################################

################################################################################
# SICHERHEITSKONZEPT
#
# Das System reduziert Fehlalarme durch:
#
# - Stabilitätsprüfung
# - Quittierungslogik
# - Eskalationslogik
#
#
# Echter Alarm wird maximal sichtbar durch:
#
# - Lichtsteuerung
# - Rollladenöffnung
# - Pushs
# - Pushover
# - Historie
# - Eskalation
################################################################################

################################################################################
# ERWEITERUNGSMÖGLICHKEITEN
#
# Später möglich:
#
# - TTS / Sprachdurchsagen
# - Wasseralarm
# - Kameraintegration
# - Feuerwehrmodus
# - Außensirenen
# - Alarmzentrale
# - Notfallbeleuchtung
# - Anwesenheitslogik
# - Alarmweiterleitung
################################################################################

################################################################################
# CO-ALARM
#
# Zusätzlich zum Rauchalarm unterstützt das System CO-Alarm-Erkennung.
#
# Verwendete Variablen:
#
# CO Status
# -> Kohlenmonoxid erkannt
#
#
# Verhalten bei CO-Alarm:
#
# CO erkannt
# -> CO Alarm aktiv
# -> Push / Pushover
# -> Licht nachts einschalten
# -> Rollläden öffnen
# -> Sirenen aktivieren
# -> CO-Eskalation starten
#
#
# Unterschiede zum Rauchalarm:
#
# - keine Stabilitätsprüfung
# - sofortige Alarmierung
# - eigene Historie
# - eigener Statusbereich
# - eigene Eskalationslogik
################################################################################

Angelegte Variablen:

Angelegte Events: